Add Architect Thesis Workshop UI (v0.1.0:49)
Frontend: ThesisWorkshopPage / ThesisWorkshopNode / ThesisWorkshopOptions — the collaborative iteration screen where partners generate a variable number of competing thesis options (1, 2, 3, A1/A2/A3 ...) for any node, give feedback, and regenerate. Reuses the shared api() helper; flexible option count is the core UX constraint. Backend Architect agent (architect_agent.py) + routes shipped in dd25bbc; this completes the user-facing surface and bumps the StartOS package to 0.1.0:49 (anthropic dep already in the image, key loaded from /data/secrets/anthropic-api-key — self-disabling until present). Also lands thesis seed iterations v3 and v5 (voice/messaging corrections). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
# Ten31 Thesis — Seed v3
|
||||||
|
|
||||||
|
*Revision after Grant's v2 feedback. Changes: (1) removed ALL "bet" language — we invest with conviction, we don't gamble; (2) added freedom-oriented technologies to the core themes (complementary alongside bitcoin/AI/energy); (3) cut the generic "we back special founders" pillar (too hand-wavy / everyone claims it / hard to prove) and refold the real, substantiable point — a decade of access — into the edge pillar; (4) fixed the segment angles: don't lead with "since 2013" for bitcoin-native HNWIs (they may have been in longer), but DO use our long history for institutions/family offices, where it's a genuine credibility edge.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The one-liner
|
||||||
|
|
||||||
|
> **Ten31 invests in the critical infrastructure behind bitcoin, AI, energy, and freedom technologies — four long-term trends that run on the same scarce inputs, where we've had conviction and a front-row seat since 2013.**
|
||||||
|
|
||||||
|
## The throughline
|
||||||
|
|
||||||
|
Bitcoin adoption, the AI buildout, and energy demand are all growing for the long haul — and freedom-oriented technologies are the complementary layer that ties them together. They aren't separate themes: they run on the same scarce inputs — **energy, compute, and sound money**. The company powering a bitcoin mine, the one cooling an AI datacenter, and the one custodying digital capital are solving versions of the same problem.
|
||||||
|
|
||||||
|
**Ten31 invests, with conviction, in that infrastructure — the picks and shovels.** We've been in the bitcoin ecosystem since 2013, longer than almost anyone, so we have the conviction, the relationships, and the first look at the best companies these markets will need.
|
||||||
|
|
||||||
|
## What we invest in (3 pillars + the proof)
|
||||||
|
|
||||||
|
**1. Four connected trends, one set of scarce inputs.**
|
||||||
|
Bitcoin adoption, AI buildout, energy demand — plus **freedom-oriented technologies** as the complementary layer (sovereign computing, open protocols, the tools that keep individuals and businesses in control). All of it is bottlenecked on the same scarce things: cheap energy, computing power, and sound money. We invest in the companies that supply them.
|
||||||
|
|
||||||
|
**2. Real infrastructure with real revenue — not speculation.**
|
||||||
|
Picks and shovels: mining, energy, custody, payments, compute, security, and freedom tech. Companies that generate revenue regardless of market sentiment. We're investing in businesses, not chasing tokens.
|
||||||
|
|
||||||
|
**3. We saw the connection first — and we have the access to act on it.**
|
||||||
|
The overlap of bitcoin, energy, AI, and freedom tech is under-invested today, and our decade in the ecosystem is the edge: it gives us conviction generalist funds don't have, first look at the strongest founders, and relationships and access others simply can't get. *This is substantiable* — it shows up in the companies we've backed and the deals we see.
|
||||||
|
|
||||||
|
**The proof:** $200M+ deployed across two funds into 30+ of the best companies in the space — Strike (bitcoin financial services), Start9 (personal datacenters / edge AI), and energy + mining infrastructure (e.g. Giga Energy, Upstream Data). Fund III continues the same strategy.
|
||||||
|
|
||||||
|
## Why it's hard to refute (no hand-waving)
|
||||||
|
|
||||||
|
- *Are these growing markets?* Obviously — bitcoin, AI, and energy are three of the biggest long-term trends there are.
|
||||||
|
- *Do they share scarce inputs?* Yes — energy and compute are the literal bottleneck for AI and mining; sound money is the settlement layer.
|
||||||
|
- *Has anyone else been investing across all of it, with this history?* Almost no one — we've been at it since 2013.
|
||||||
|
- *Are these real businesses?* Yes — revenue-generating infrastructure, not speculation.
|
||||||
|
|
||||||
|
## Per-segment angle (one line each, same core)
|
||||||
|
|
||||||
|
- **Bitcoin-native HNWI** — you already have conviction in bitcoin; we give you exposure to the entire infrastructure buildout around it, and access to the best companies in the space. *(Lead with shared conviction + access — NOT our tenure; many of these investors have been in as long as or longer than us.)*
|
||||||
|
- **Institution** — durable, revenue-generating exposure to the bitcoin/AI/energy buildout, through a manager who has been in this ecosystem since 2013 — longer than almost anyone, and a real credibility edge. *(Tenure is a key selling point here.)*
|
||||||
|
- **Family office (diversified, curious)** — a long-horizon allocation to four connected trends, grounded in real businesses, from a team with a decade-plus track record in the space. *(Tenure matters here too.)*
|
||||||
|
- **Smaller accredited ($100k)** — the same thesis our largest, most-convicted investors back, at an accessible entry point.
|
||||||
|
- **AI / energy operators** — you live the energy-and-compute scarcity every day; we invest across the stack that supplies it.
|
||||||
|
|
||||||
|
## Voice
|
||||||
|
|
||||||
|
Direct, concrete, confident, conviction-driven. **Never** "bet"/"betting"/"gamble" — we invest behind things we have conviction in. Lead with what we buy and why it grows; plain English an engineer or a serious LP can verify in their head; real examples and numbers. Avoid abstract philosophy and anything that makes eyes glaze over.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Next: react again — especially on pillar 3 (is "access/first look" the right framing for the edge?), the freedom-tech wording, and whether the segment angles land. The deeper essays stay as supporting material, not the pitch.*
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Ten31 Thesis — Seed v5
|
||||||
|
|
||||||
|
*Captures Grant's v4 edits. The Option A vs Option B framing decision (scarcity-forward vs "freedom tech" as the banner) stays open for Grant and his partner to debate, and the wording of each option may keep refining. This is the version to seed into the CRM as the starting point.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Framing (still being debated by the partners)
|
||||||
|
|
||||||
|
- **Option A (scarcity-forward):** Ten31 invests in the infrastructure of scarcity. We back the bitcoin, energy, and AI companies that produce and secure the scarce resources these markets are built on.
|
||||||
|
- **Option B (freedom tech as the banner):** Ten31 invests in freedom technology. We back the bitcoin, energy, and AI companies building the foundation for a more sovereign, less centralized economy.
|
||||||
|
|
||||||
|
*(Both descriptions are themselves still being refined.)*
|
||||||
|
|
||||||
|
## The throughline
|
||||||
|
|
||||||
|
Bitcoin, AI, and energy are three of the largest growth markets of the next decade, and they depend on the same scarce resources: cheap energy and computing power. We believe that energy, compute, and AI infrastructure will settle on money that is hard to produce. That is not the case today, and connecting these markets to bitcoin is the part of the thesis that very few others are making, even as broader crypto tries to attach itself to AI and energy. Ten31 invests in that infrastructure with strong conviction.
|
||||||
|
|
||||||
|
## Pillars
|
||||||
|
|
||||||
|
**1. Scarcity is the whole opportunity.**
|
||||||
|
Every one of these markets is bottlenecked on something scarce. AI and bitcoin both compete for cheap energy and compute. And we believe energy, compute, and AI infrastructure will increasingly settle on money that is hard to produce, which points directly at bitcoin. The companies that own and supply the scarce side of that equation capture the value as demand grows. That is where we invest. *(The bitcoin connection is a forward-looking conviction, not a description of today. That gap is the opportunity.)*
|
||||||
|
|
||||||
|
**2. We invest in foundational infrastructure with real revenue.**
|
||||||
|
The companies that generate energy, secure capital, and power computation. Real businesses earning real money from real demand today.
|
||||||
|
|
||||||
|
**3. Founders seek us out, and we lead deals others never see.**
|
||||||
|
Founders come to us because of our experience and our genuine alignment with bitcoin. We pursue and lead opportunities exclusive to us. People in this ecosystem know our track record and want us on their side.
|
||||||
|
|
||||||
|
## The proof
|
||||||
|
|
||||||
|
$200M+ deployed across two funds into 30+ of the strongest companies in the space (Strike, Start9, energy and mining infrastructure). A six-year track record that includes large-scale M&A and public-markets activity that is unmatched by others in this space. Fund III continues the same strategy.
|
||||||
|
|
||||||
|
## Per-segment angle
|
||||||
|
|
||||||
|
- **Bitcoin-native HNWIs (OGs).** Bitcoin only wins if people build on it. Holding is not enough. You care about making bitcoin succeed, and so do we. We put capital behind the companies that turn bitcoin into a working economy.
|
||||||
|
- **Institutions.** Exposure to the bitcoin, energy, and AI buildout through a team with a six-year institutional track record, including large-scale M&A and public-markets activity unmatched by others in this space (and Grant's prior institutional experience on top of that).
|
||||||
|
- **Family offices.** A long-horizon allocation grounded in real businesses, run by a team with deep credibility and a real track record.
|
||||||
|
- **Smaller accredited ($100k).** The same thesis our most convicted investors back, at an accessible entry point.
|
||||||
|
- **AI and energy operators.** You may not be focused on bitcoin today, and that is exactly the point. We believe bitcoin becomes a larger component of energy and compute over time, and most operators in your space are not yet positioned for it. We are, and we invest across the stack that connects them.
|
||||||
|
|
||||||
|
## Voice
|
||||||
|
|
||||||
|
Direct, concrete, conviction-driven. No "betting" language, no em dashes, no "X, not Y" phrasing, no kitchen-sink lists. Plain sentences a serious LP can verify in their head.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Open decisions for the partners: Option A vs B (and the wording of each); whether pillar 3's "sought out / exclusive deals" is the edge to lead with. This seed goes into the CRM and the Architect helps iterate from here.*
|
||||||
@@ -1280,6 +1280,190 @@
|
|||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Thesis Workshop ----------------------------------------------------- */
|
||||||
|
.thesis-ws-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
background-color: #1a2233;
|
||||||
|
border: 1px solid #35506a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #c7d3e0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-banner.ready {
|
||||||
|
background-color: #10b9810f;
|
||||||
|
border-color: #1f6f54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-banner-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-node {
|
||||||
|
border: 1px solid #263548;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #0d1622;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-node.selected {
|
||||||
|
border-color: #3b82c4;
|
||||||
|
box-shadow: inset 0 0 0 1px #3b82c455;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-node-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 400;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-node-head:hover {
|
||||||
|
background-color: #152233;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-node-type {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #8ea2b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-node-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e5edf5;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-children {
|
||||||
|
padding-left: 18px;
|
||||||
|
border-left: 1px solid #1d2a3a;
|
||||||
|
margin: 4px 0 4px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-body {
|
||||||
|
padding: 4px 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #93c5fd;
|
||||||
|
background-color: #3b82c422;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-option {
|
||||||
|
border: 1px solid #263548;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #111a27;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-option-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-option-num {
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8ea2b7;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-option-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #e5edf5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-rationale {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background-color: #0d1622;
|
||||||
|
border-left: 2px solid #3b82c4;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #8ea2b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-rationale-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #93c5fd;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-controls {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px solid #263548;
|
||||||
|
padding-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 760px) {
|
||||||
|
.thesis-ws-controls {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.thesis-ws-control-col {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-control-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thesis-ws-n-input {
|
||||||
|
min-width: 0;
|
||||||
|
width: 80px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.index-action-status {
|
.index-action-status {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
@@ -9264,6 +9448,610 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Friendly label for the various node types in a thesis tree.
|
||||||
|
const THESIS_NODE_TYPE_LABEL = {
|
||||||
|
thesis_root: 'Thesis',
|
||||||
|
throughline: 'Throughline',
|
||||||
|
section: 'Section',
|
||||||
|
claim: 'Claim',
|
||||||
|
proof_point: 'Proof Point',
|
||||||
|
objection: 'Objection',
|
||||||
|
segment_cut: 'Segment Cut'
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNodeType = (t) => THESIS_NODE_TYPE_LABEL[t] || formatThesisStatus(t);
|
||||||
|
|
||||||
|
// Detect the server's "Architect not connected" signal so we can show a
|
||||||
|
// calm connect-the-Architect message instead of a raw 502.
|
||||||
|
const isArchitectMissingKeyError = (err) => {
|
||||||
|
const status = err?.status;
|
||||||
|
const payload = err?.payload || {};
|
||||||
|
const haystack = `${err?.message || ''} ${payload.error || ''} ${payload.reason || ''} ${payload.detail || ''}`.toLowerCase();
|
||||||
|
return status === 502 || haystack.includes('anthropic_api_key') || haystack.includes('api key') || haystack.includes('not configured');
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARCHITECT_CONNECT_HINT = "The Architect isn't connected yet — add your Anthropic API key on the server to enable generation. You can still view and edit the thesis.";
|
||||||
|
|
||||||
|
// Renders a single node's competing options (its variant group). The
|
||||||
|
// number of options is whatever the server returns — could be 0, 1, or
|
||||||
|
// many — and is rendered as a dynamic list, never a fixed A/B layout.
|
||||||
|
const ThesisWorkshopOptions = ({ token, node, lineKey, isAdmin, architectReady, onShowToast }) => {
|
||||||
|
const nodeId = node.id;
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [genN, setGenN] = useState(3);
|
||||||
|
const [guidance, setGuidance] = useState('');
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [feedbackN, setFeedbackN] = useState(2);
|
||||||
|
const [feedback, setFeedback] = useState('');
|
||||||
|
const [sendingFeedback, setSendingFeedback] = useState(false);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await api(`/api/thesis/nodes/${nodeId}/variants`, {}, token);
|
||||||
|
setData(result);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(getErrorMessage(err, 'Failed to load options'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [token, nodeId]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
const handleGenerate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isAdmin || generating) return;
|
||||||
|
const n = Math.max(1, Math.min(8, parseInt(genN, 10) || 1));
|
||||||
|
try {
|
||||||
|
setGenerating(true);
|
||||||
|
const result = await api(`/api/thesis/nodes/${nodeId}/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ n, guidance: guidance.trim() })
|
||||||
|
}, token);
|
||||||
|
const count = result?.data?.generated ?? (result?.data?.options || []).length;
|
||||||
|
onShowToast(`Generated ${count} option${count === 1 ? '' : 's'}`, 'success');
|
||||||
|
setGuidance('');
|
||||||
|
await load();
|
||||||
|
} catch (err) {
|
||||||
|
if (isArchitectMissingKeyError(err)) {
|
||||||
|
onShowToast(ARCHITECT_CONNECT_HINT, 'error');
|
||||||
|
} else {
|
||||||
|
onShowToast(getErrorMessage(err, 'Failed to generate options'), 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeedback = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isAdmin || sendingFeedback) return;
|
||||||
|
if (!feedback.trim()) {
|
||||||
|
onShowToast('Add some feedback for the Architect to address', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Math.max(1, Math.min(8, parseInt(feedbackN, 10) || 1));
|
||||||
|
try {
|
||||||
|
setSendingFeedback(true);
|
||||||
|
const result = await api(`/api/thesis/nodes/${nodeId}/feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ feedback: feedback.trim(), n })
|
||||||
|
}, token);
|
||||||
|
const count = result?.data?.generated ?? (result?.data?.options || []).length;
|
||||||
|
onShowToast(`Revised — ${count} new option${count === 1 ? '' : 's'}`, 'success');
|
||||||
|
setFeedback('');
|
||||||
|
await load();
|
||||||
|
} catch (err) {
|
||||||
|
if (isArchitectMissingKeyError(err)) {
|
||||||
|
onShowToast(ARCHITECT_CONNECT_HINT, 'error');
|
||||||
|
} else {
|
||||||
|
onShowToast(getErrorMessage(err, 'Failed to send feedback'), 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSendingFeedback(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <div style={{ padding: '4px 0' }}><SkeletonBlock lines={3} /></div>;
|
||||||
|
if (error) return <div className="toast error" style={{ position: 'static' }}>{error}</div>;
|
||||||
|
|
||||||
|
const options = Array.isArray(data?.variants) ? data.variants : [];
|
||||||
|
const busy = generating || sendingFeedback;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{options.length === 0 ? (
|
||||||
|
<div className="empty-state" style={{ padding: '18px 0' }}>
|
||||||
|
No options yet for this part.
|
||||||
|
{architectReady ? ' Generate some below.' : ' Connect the Architect to generate drafts, or edit the thesis directly.'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="thesis-ws-options">
|
||||||
|
{options.map((opt, i) => {
|
||||||
|
const meta = opt.meta || {};
|
||||||
|
const rationale = meta.rationale || meta.reason || meta.notes || '';
|
||||||
|
const text = opt.body || opt.title || '';
|
||||||
|
return (
|
||||||
|
<div key={opt.id ?? i} className="thesis-ws-option">
|
||||||
|
<div className="thesis-ws-option-head">
|
||||||
|
<span className="thesis-ws-option-num">Option {i + 1} of {options.length}</span>
|
||||||
|
{opt.status && (
|
||||||
|
<span className={`badge ${thesisStatusClass(opt.status)}`}>{formatThesisStatus(opt.status)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{opt.title && opt.body && opt.title !== opt.body && (
|
||||||
|
<div className="thesis-ws-node-title" style={{ marginBottom: '6px' }}>{opt.title}</div>
|
||||||
|
)}
|
||||||
|
<div className="thesis-ws-option-text">{text || '—'}</div>
|
||||||
|
{rationale && (
|
||||||
|
<div className="thesis-ws-rationale">
|
||||||
|
<div className="thesis-ws-rationale-label">Architect's rationale</div>
|
||||||
|
{rationale}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAdmin ? (
|
||||||
|
<div className="thesis-ws-controls">
|
||||||
|
<form className="thesis-ws-control-col" onSubmit={handleGenerate}>
|
||||||
|
<div className="thesis-block-label">Generate options</div>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="text-input thesis-ws-n-input"
|
||||||
|
min="1"
|
||||||
|
max="8"
|
||||||
|
value={genN}
|
||||||
|
onChange={(e) => setGenN(e.target.value)}
|
||||||
|
aria-label="Number of options to generate"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<span className="form-help" style={{ marginTop: 0 }}>option(s)</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className="text-input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Optional guidance — angle, tone, what to emphasize…"
|
||||||
|
value={guidance}
|
||||||
|
onChange={(e) => setGuidance(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<button type="submit" disabled={busy || !architectReady}>
|
||||||
|
{generating ? 'Architect is drafting…' : 'Generate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{!architectReady && (
|
||||||
|
<div className="form-help">Generation needs the Architect connected.</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form className="thesis-ws-control-col" onSubmit={handleFeedback}>
|
||||||
|
<div className="thesis-block-label">Give feedback</div>
|
||||||
|
<textarea
|
||||||
|
className="text-input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Tell the Architect what to change, then it revises into new option drafts."
|
||||||
|
value={feedback}
|
||||||
|
onChange={(e) => setFeedback(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="text-input thesis-ws-n-input"
|
||||||
|
min="1"
|
||||||
|
max="8"
|
||||||
|
value={feedbackN}
|
||||||
|
onChange={(e) => setFeedbackN(e.target.value)}
|
||||||
|
aria-label="Number of revised options"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={busy || !architectReady}>
|
||||||
|
{sendingFeedback ? 'Architect is revising…' : 'Revise'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="form-help" style={{ marginTop: '14px' }}>Only admins can generate or revise options.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// One node in the tree. Renders its header (click to select + reveal its
|
||||||
|
// options), its options when selected, then recurses into children.
|
||||||
|
const ThesisWorkshopNode = ({ token, node, lineKey, depth, isAdmin, architectReady, selectedId, onSelect, onShowToast }) => {
|
||||||
|
const isSelected = selectedId === node.id;
|
||||||
|
const children = Array.isArray(node.children) ? node.children : [];
|
||||||
|
const variantGroup = node.variant_group;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`thesis-ws-node ${isSelected ? 'selected' : ''}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="thesis-ws-node-head"
|
||||||
|
onClick={() => onSelect(isSelected ? null : node.id)}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div className="thesis-ws-node-type">{formatNodeType(node.node_type)}</div>
|
||||||
|
<div className="thesis-ws-node-title">{node.title || node.body || formatNodeType(node.node_type)}</div>
|
||||||
|
{node.body && node.title && node.body !== node.title && (
|
||||||
|
<div className="thesis-line-meta">{node.body}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="thesis-ws-count">{isSelected ? 'Hide options' : 'Options'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<div className="thesis-ws-body">
|
||||||
|
<ThesisWorkshopOptions
|
||||||
|
key={variantGroup || node.id}
|
||||||
|
token={token}
|
||||||
|
node={node}
|
||||||
|
lineKey={lineKey}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
architectReady={architectReady}
|
||||||
|
onShowToast={onShowToast}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children.length > 0 && (
|
||||||
|
<div className="thesis-ws-children">
|
||||||
|
{children.map((child) => (
|
||||||
|
<ThesisWorkshopNode
|
||||||
|
key={child.id}
|
||||||
|
token={token}
|
||||||
|
node={child}
|
||||||
|
lineKey={lineKey}
|
||||||
|
depth={(depth || 0) + 1}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
architectReady={architectReady}
|
||||||
|
selectedId={selectedId}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onShowToast={onShowToast}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThesisWorkshopPage = ({ token, user, onShowToast }) => {
|
||||||
|
const isAdmin = user?.role === 'admin';
|
||||||
|
const [lines, setLines] = useState([]);
|
||||||
|
const [linesLoading, setLinesLoading] = useState(true);
|
||||||
|
const [linesError, setLinesError] = useState('');
|
||||||
|
const [selectedLineKey, setSelectedLineKey] = useState(null);
|
||||||
|
|
||||||
|
const [tree, setTree] = useState(null);
|
||||||
|
const [treeLoading, setTreeLoading] = useState(false);
|
||||||
|
const [treeError, setTreeError] = useState('');
|
||||||
|
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
||||||
|
const [treeTick, setTreeTick] = useState(0);
|
||||||
|
|
||||||
|
const [architect, setArchitect] = useState(null);
|
||||||
|
|
||||||
|
// Seed-a-line form (shown when there are no lines).
|
||||||
|
const [showNewLine, setShowNewLine] = useState(false);
|
||||||
|
const [newLine, setNewLine] = useState({ line_key: '', name: '', is_core: false, description: '' });
|
||||||
|
const [creatingLine, setCreatingLine] = useState(false);
|
||||||
|
|
||||||
|
// Seed-a-node controls (shown when a line has an empty tree).
|
||||||
|
const [seedTitle, setSeedTitle] = useState('');
|
||||||
|
const [seedType, setSeedType] = useState('throughline');
|
||||||
|
const [seeding, setSeeding] = useState(false);
|
||||||
|
|
||||||
|
// Architect status — non-blocking; failure just means "unknown",
|
||||||
|
// we still let the partner view/edit the thesis.
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const result = await api('/api/architect/status', {}, token);
|
||||||
|
if (!cancelled) setArchitect(result.data || result);
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) setArchitect({ ready: false, reason: getErrorMessage(err, 'Status unavailable') });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const loadLines = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLinesLoading(true);
|
||||||
|
const result = await api('/api/thesis/lines', {}, token);
|
||||||
|
const lineList = result.lines || [];
|
||||||
|
setLines(lineList);
|
||||||
|
setLinesError('');
|
||||||
|
setSelectedLineKey((prev) => {
|
||||||
|
if (prev && lineList.some((l) => l.line_key === prev)) return prev;
|
||||||
|
return lineList[0]?.line_key ?? null;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setLinesError(getErrorMessage(err, 'Failed to load thesis lines'));
|
||||||
|
} finally {
|
||||||
|
setLinesLoading(false);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => { loadLines(); }, [loadLines]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLineKey) { setTree(null); return undefined; }
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
setTreeLoading(true);
|
||||||
|
const result = await api(`/api/thesis/${selectedLineKey}/tree`, {}, token);
|
||||||
|
if (cancelled) return;
|
||||||
|
setTree(result);
|
||||||
|
setTreeError('');
|
||||||
|
setSelectedNodeId(null);
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) setTreeError(getErrorMessage(err, 'Failed to load thesis tree'));
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setTreeLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [token, selectedLineKey, treeTick]);
|
||||||
|
|
||||||
|
const handleCreateLine = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isAdmin || creatingLine) return;
|
||||||
|
if (!newLine.line_key.trim() || !newLine.name.trim()) {
|
||||||
|
onShowToast('A key and name are required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setCreatingLine(true);
|
||||||
|
await api('/api/thesis/lines', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
line_key: newLine.line_key.trim(),
|
||||||
|
name: newLine.name.trim(),
|
||||||
|
is_core: !!newLine.is_core,
|
||||||
|
description: newLine.description.trim()
|
||||||
|
})
|
||||||
|
}, token);
|
||||||
|
onShowToast('Thesis line created', 'success');
|
||||||
|
const createdKey = newLine.line_key.trim();
|
||||||
|
setNewLine({ line_key: '', name: '', is_core: false, description: '' });
|
||||||
|
setShowNewLine(false);
|
||||||
|
await loadLines();
|
||||||
|
setSelectedLineKey(createdKey);
|
||||||
|
} catch (err) {
|
||||||
|
onShowToast(getErrorMessage(err, 'Failed to create thesis line'), 'error');
|
||||||
|
} finally {
|
||||||
|
setCreatingLine(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeedNode = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isAdmin || seeding || !selectedLineKey) return;
|
||||||
|
if (!seedTitle.trim()) {
|
||||||
|
onShowToast('Give the new part a title', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setSeeding(true);
|
||||||
|
await api(`/api/thesis/lines/${selectedLineKey}/nodes`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ node_type: seedType, title: seedTitle.trim(), body: '' })
|
||||||
|
}, token);
|
||||||
|
onShowToast('Added to the thesis', 'success');
|
||||||
|
setSeedTitle('');
|
||||||
|
setTreeTick((t) => t + 1);
|
||||||
|
} catch (err) {
|
||||||
|
onShowToast(getErrorMessage(err, 'Failed to add part'), 'error');
|
||||||
|
} finally {
|
||||||
|
setSeeding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const architectReady = !!architect?.ready;
|
||||||
|
const nodes = Array.isArray(tree?.tree) ? tree.tree : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container">
|
||||||
|
<h2 className="section-title" style={{ marginBottom: '20px' }}>Thesis Workshop</h2>
|
||||||
|
|
||||||
|
{architect && (
|
||||||
|
<div className={`thesis-ws-banner ${architectReady ? 'ready' : ''}`}>
|
||||||
|
<span className="thesis-ws-banner-icon">{architectReady ? '◆' : 'ⓘ'}</span>
|
||||||
|
<div>
|
||||||
|
{architectReady ? (
|
||||||
|
<>The Architect is connected{architect.model ? ` (${architect.model})` : ''} and ready to draft options with you.</>
|
||||||
|
) : (
|
||||||
|
<>{ARCHITECT_CONNECT_HINT}{architect.reason ? ` (${architect.reason})` : ''}</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="thesis-layout">
|
||||||
|
<div className="thesis-col">
|
||||||
|
<div className="section">
|
||||||
|
<div className="section-title">Thesis Lines</div>
|
||||||
|
{linesLoading ? (
|
||||||
|
<SkeletonBlock lines={4} />
|
||||||
|
) : linesError ? (
|
||||||
|
<div className="toast error" style={{ position: 'static' }}>{linesError}</div>
|
||||||
|
) : lines.length === 0 ? (
|
||||||
|
<div className="empty-state" style={{ padding: '20px 0' }}>
|
||||||
|
<div className="empty-state-icon">§</div>
|
||||||
|
No thesis lines yet.
|
||||||
|
{isAdmin ? ' Seed one to start working with the Architect.' : ' An admin needs to create one.'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="thesis-line-list">
|
||||||
|
{lines.map((line) => (
|
||||||
|
<button
|
||||||
|
key={line.id}
|
||||||
|
className={`thesis-version-row ${selectedLineKey === line.line_key ? 'active' : ''}`}
|
||||||
|
onClick={() => setSelectedLineKey(line.line_key)}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div className="thesis-line-name">
|
||||||
|
{line.name}
|
||||||
|
{line.is_core && <span className="badge badge-investor" style={{ marginLeft: '8px' }}>Core</span>}
|
||||||
|
</div>
|
||||||
|
<div className="thesis-line-meta">{line.line_key}{line.segment_key ? ` · ${line.segment_key}` : ''}</div>
|
||||||
|
</div>
|
||||||
|
<span className={`badge ${thesisStatusClass(line.status)}`}>{formatThesisStatus(line.status)}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAdmin && !linesLoading && (
|
||||||
|
<div style={{ marginTop: '14px' }}>
|
||||||
|
{showNewLine ? (
|
||||||
|
<form onSubmit={handleCreateLine} className="thesis-review-form" style={{ marginTop: 0 }}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Key</label>
|
||||||
|
<input
|
||||||
|
className="text-input"
|
||||||
|
placeholder="e.g. bitcoin_energy"
|
||||||
|
value={newLine.line_key}
|
||||||
|
onChange={(e) => setNewLine((f) => ({ ...f, line_key: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="text-input"
|
||||||
|
placeholder="Human-friendly name"
|
||||||
|
value={newLine.name}
|
||||||
|
onChange={(e) => setNewLine((f) => ({ ...f, name: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Description</label>
|
||||||
|
<textarea
|
||||||
|
className="text-input"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Optional one-liner"
|
||||||
|
value={newLine.description}
|
||||||
|
onChange={(e) => setNewLine((f) => ({ ...f, description: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label className="form-help" style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newLine.is_core}
|
||||||
|
onChange={(e) => setNewLine((f) => ({ ...f, is_core: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
Mark as a core thesis line
|
||||||
|
</label>
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" className="button-secondary" onClick={() => setShowNewLine(false)} disabled={creatingLine}>Cancel</button>
|
||||||
|
<button type="submit" disabled={creatingLine}>{creatingLine ? 'Creating…' : 'Create line'}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<button className="button-secondary" style={{ width: '100%' }} onClick={() => setShowNewLine(true)}>+ New thesis line</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="thesis-col">
|
||||||
|
<div className="section">
|
||||||
|
<div className="section-title">Workshop</div>
|
||||||
|
{!selectedLineKey ? (
|
||||||
|
<div className="empty-state" style={{ padding: '24px 0' }}>
|
||||||
|
<div className="empty-state-icon">◷</div>
|
||||||
|
Pick a thesis line on the left to start iterating on it.
|
||||||
|
</div>
|
||||||
|
) : treeLoading ? (
|
||||||
|
<SkeletonBlock lines={6} />
|
||||||
|
) : treeError ? (
|
||||||
|
<div className="toast error" style={{ position: 'static' }}>{treeError}</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{nodes.length === 0 ? (
|
||||||
|
<div className="empty-state" style={{ padding: '20px 0' }}>
|
||||||
|
This line has no content yet.
|
||||||
|
{isAdmin ? ' Add a throughline or a pillar to seed it.' : ' An admin needs to seed it.'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<ThesisWorkshopNode
|
||||||
|
key={node.id}
|
||||||
|
token={token}
|
||||||
|
node={node}
|
||||||
|
lineKey={selectedLineKey}
|
||||||
|
depth={0}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
architectReady={architectReady}
|
||||||
|
selectedId={selectedNodeId}
|
||||||
|
onSelect={setSelectedNodeId}
|
||||||
|
onShowToast={onShowToast}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
|
<form onSubmit={handleSeedNode} className="thesis-review-form">
|
||||||
|
<div className="thesis-block-label">Add a part</div>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||||
|
<div className="form-group" style={{ marginBottom: 0, flex: '0 0 auto' }}>
|
||||||
|
<label className="form-label">Type</label>
|
||||||
|
<select
|
||||||
|
className="select-input"
|
||||||
|
value={seedType}
|
||||||
|
onChange={(e) => setSeedType(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="throughline">Throughline</option>
|
||||||
|
<option value="section">Section / Pillar</option>
|
||||||
|
<option value="claim">Claim</option>
|
||||||
|
<option value="proof_point">Proof Point</option>
|
||||||
|
<option value="objection">Objection</option>
|
||||||
|
<option value="segment_cut">Segment Cut</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group" style={{ marginBottom: 0, flex: '1 1 200px' }}>
|
||||||
|
<label className="form-label">Title</label>
|
||||||
|
<input
|
||||||
|
className="text-input"
|
||||||
|
placeholder="Short name for this part"
|
||||||
|
value={seedTitle}
|
||||||
|
onChange={(e) => setSeedTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={seeding}>{seeding ? 'Adding…' : 'Add'}</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-help" style={{ marginTop: '8px' }}>
|
||||||
|
Each part holds its own set of competing options — one or many — that you grow with the Architect.
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SystemStatusPage = ({ token, user, onShowToast }) => {
|
const SystemStatusPage = ({ token, user, onShowToast }) => {
|
||||||
const isAdmin = user?.role === 'admin';
|
const isAdmin = user?.role === 'admin';
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@@ -9770,6 +10558,9 @@
|
|||||||
<button className={`nav-item ${page === 'thesis' ? 'active' : ''}`} onClick={() => setPage('thesis')}>
|
<button className={`nav-item ${page === 'thesis' ? 'active' : ''}`} onClick={() => setPage('thesis')}>
|
||||||
<span className="nav-item-icon">§</span> Thesis
|
<span className="nav-item-icon">§</span> Thesis
|
||||||
</button>
|
</button>
|
||||||
|
<button className={`nav-item ${page === 'thesis-workshop' ? 'active' : ''}`} onClick={() => setPage('thesis-workshop')}>
|
||||||
|
<span className="nav-item-icon">◆</span> Thesis Workshop
|
||||||
|
</button>
|
||||||
<button className={`nav-item ${page === 'system-status' ? 'active' : ''}`} onClick={() => setPage('system-status')}>
|
<button className={`nav-item ${page === 'system-status' ? 'active' : ''}`} onClick={() => setPage('system-status')}>
|
||||||
<span className="nav-item-icon">◉</span> System Status
|
<span className="nav-item-icon">◉</span> System Status
|
||||||
</button>
|
</button>
|
||||||
@@ -9799,6 +10590,7 @@
|
|||||||
{page === 'pipeline' && 'Pipeline'}
|
{page === 'pipeline' && 'Pipeline'}
|
||||||
{page === 'communications' && 'Communications'}
|
{page === 'communications' && 'Communications'}
|
||||||
{page === 'thesis' && 'Thesis'}
|
{page === 'thesis' && 'Thesis'}
|
||||||
|
{page === 'thesis-workshop' && 'Thesis Workshop'}
|
||||||
{page === 'system-status' && 'System Status'}
|
{page === 'system-status' && 'System Status'}
|
||||||
{page === 'feature-requests' && 'Feature Requests'}
|
{page === 'feature-requests' && 'Feature Requests'}
|
||||||
{page === 'instructions' && 'Instructions'}
|
{page === 'instructions' && 'Instructions'}
|
||||||
@@ -9829,6 +10621,7 @@
|
|||||||
{page === 'pipeline' && <PipelinePage token={token} onShowToast={showToast} />}
|
{page === 'pipeline' && <PipelinePage token={token} onShowToast={showToast} />}
|
||||||
{page === 'communications' && <CommunicationsPage token={token} onShowToast={showToast} />}
|
{page === 'communications' && <CommunicationsPage token={token} onShowToast={showToast} />}
|
||||||
{page === 'thesis' && <ThesisPage token={token} user={user} onShowToast={showToast} />}
|
{page === 'thesis' && <ThesisPage token={token} user={user} onShowToast={showToast} />}
|
||||||
|
{page === 'thesis-workshop' && <ThesisWorkshopPage token={token} user={user} onShowToast={showToast} />}
|
||||||
{page === 'system-status' && <SystemStatusPage token={token} user={user} onShowToast={showToast} />}
|
{page === 'system-status' && <SystemStatusPage token={token} user={user} onShowToast={showToast} />}
|
||||||
{page === 'feature-requests' && <FeatureRequestsPage token={token} onShowToast={showToast} user={user} />}
|
{page === 'feature-requests' && <FeatureRequestsPage token={token} onShowToast={showToast} user={user} />}
|
||||||
{page === 'instructions' && <InstructionsPage />}
|
{page === 'instructions' && <InstructionsPage />}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
|||||||
// * 0.1.0:45 (Phase-1 thesis system; dual approval; merge review; in-app index)
|
// * 0.1.0:45 (Phase-1 thesis system; dual approval; merge review; in-app index)
|
||||||
// * 0.1.0:46 (packaging fix: ship full backend so migrations run + endpoints work)
|
// * 0.1.0:46 (packaging fix: ship full backend so migrations run + endpoints work)
|
||||||
// * 0.1.0:47 (soft-delete instead of hard-delete; source-count diagnostics)
|
// * 0.1.0:47 (soft-delete instead of hard-delete; source-count diagnostics)
|
||||||
// * Current: 0.1.0:48 (entity model: investors vs people; fixes double-count)
|
// * 0.1.0:48 (entity model: investors vs people; fixes double-count)
|
||||||
export const PACKAGE_VERSION = '0.1.0:48'
|
// * Current: 0.1.0:49 (Architect: Claude thesis generation + Thesis Workshop screen)
|
||||||
|
export const PACKAGE_VERSION = '0.1.0:49'
|
||||||
|
|
||||||
export const DATA_MOUNT_PATH = '/data'
|
export const DATA_MOUNT_PATH = '/data'
|
||||||
export const WEB_PORT = 8080
|
export const WEB_PORT = 8080
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { v_0_1_0_45 } from './v0.1.0.45'
|
|||||||
import { v_0_1_0_46 } from './v0.1.0.46'
|
import { v_0_1_0_46 } from './v0.1.0.46'
|
||||||
import { v_0_1_0_47 } from './v0.1.0.47'
|
import { v_0_1_0_47 } from './v0.1.0.47'
|
||||||
import { v_0_1_0_48 } from './v0.1.0.48'
|
import { v_0_1_0_48 } from './v0.1.0.48'
|
||||||
|
import { v_0_1_0_49 } from './v0.1.0.49'
|
||||||
|
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_0_1_0_48,
|
current: v_0_1_0_49,
|
||||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47],
|
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// The Architect. Adds the Claude-powered thesis generation agent (backend/mcp/
|
||||||
|
// architect_agent.py) and a "Thesis Workshop" screen where partners iterate on
|
||||||
|
// the thesis: generate any number of competing options for any part, give
|
||||||
|
// feedback, and regenerate. Runs on Claude; drop your Anthropic API key at
|
||||||
|
// /data/secrets/anthropic-api-key to enable generation (self-disabling until
|
||||||
|
// then). `anthropic` added to the image. No data migration.
|
||||||
|
export const v_0_1_0_49 = VersionInfo.of({
|
||||||
|
version: '0.1.0:49',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: [
|
||||||
|
'Adds the Architect: a Thesis Workshop screen where you and your partner',
|
||||||
|
'iterate on the thesis with Claude, generating any number of competing',
|
||||||
|
'options for any part and giving feedback to regenerate. Add your Anthropic',
|
||||||
|
'API key at /data/secrets/anthropic-api-key on the server to switch it on.',
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
migrations: { up: async () => {}, down: async () => {} },
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user