prototype: explore global context with one operation at a time

This commit is contained in:
Codex
2026-09-12 11:53:52 +02:00
parent 4a67d60233
commit 36bf659ea9
11 changed files with 720 additions and 0 deletions
+1
View File
@@ -7,6 +7,7 @@
"prototype:database-management": "vite --config vite.database-management-prototype.config.ts",
"prototype:administration-pages": "vite --config vite.administration-pages-prototype.config.ts",
"prototype:administration-review": "vite --config vite.administration-review.config.ts",
"prototype:application-context": "vite --config vite.application-context.config.ts",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
@@ -2,6 +2,10 @@
Throwaway prototype, 2026-09-10. **No variant selected yet.**
Historical scope: the 2026-09-12 [full application context review V3](../application-context/README.md)
supersedes the independent administrative workspace assumption below. This V2
prototype and all three variants are preserved for comparison.
Question: which page structure best supports the five existing administrative
workflows inside the space allocated by Omics Portal?
@@ -0,0 +1,160 @@
import { useEffect, useRef, useState } from "react";
import { ArrowLeft, ArrowRight, ArrowUpRight, BookOpen, Check, ChevronDown, ChevronRight, Cpu, Database, FileText, FolderClock, Layers, LayoutDashboard, LockKeyhole, Menu, MessageSquare, Play, Search, Settings2, ShieldCheck, SlidersHorizontal, Terminal, Users, X } from "lucide-react";
import { Content, type ContentProps, type ConnectionValues } from "./Content";
import { Button } from "./ui";
import { inventory, type Item, type Scenario, type Surface, type Variant } from "../administration-review/model";
import { models, modules, readLocation, sessions, variants, workspaces, type Job, type Page, type Route, type Session } from "./model";
// Three complete-app proposals on the established namespaced routes. V3 is a
// disposable simulation, with no API, persistence, authorization or server writes.
const icons = { core: MessageSquare, sessions: FolderClock, database: Database, memory: BookOpen, evidence: FileText, workspace: Layers, pi: Terminal };
const titles: Record<Page, string> = { core: "Analytical workspace", sessions: "Sessions", database: "Database", memory: "Memory", evidence: "Evidence", workspace: "Workspace", pi: "Pi runtime" };
type Availability = "available" | "unavailable" | "no-workspaces" | "no-models" | "needs-preprocessing";
export function App() {
const [route, setRoute] = useState<Route>(readLocation);
const { page, workspace, variant } = route;
const [modelId, setModelId] = useState("");
const [availability, setAvailability] = useState<Availability>("available");
const [width, setWidth] = useState("full");
const [portalOpen, setPortalOpen] = useState(false);
const [navOpen, setNavOpen] = useState(false);
const [shelfOpen, setShelfOpen] = useState(true);
const [notice, setNotice] = useState("");
const [dirty, setDirty] = useState(false);
const [editing, setEditing] = useState(false);
const [section, setSection] = useState(0);
const [focused, setFocused] = useState(false);
const [selection, setSelection] = useState<Record<string, string>>({});
const [filters, setFilters] = useState<Record<string, string>>({});
const [cards, setCards] = useState<Record<string, Item[]>>({});
const [connections, setConnections] = useState<Record<string, ConnectionValues>>({});
const [operation, setOperation] = useState<Job | null>(null);
const busy = operation?.state === "running";
const busyRef = useRef(false); busyRef.current = busy;
const [active, setActive] = useState<Session | null>(null);
const [resumeTarget, setResumeTarget] = useState<Session | null>(null);
const [question, setQuestion] = useState("");
const [artifact, setArtifact] = useState("Question");
const [scenario, setScenario] = useState<Scenario>("ready");
const model = models.find(m => m.id === modelId);
const currentWorkspace = workspaces.find(w => w.id === workspace);
const ready = !!currentWorkspace && !!model && !["unavailable", "no-workspaces", "no-models"].includes(availability);
const admin = page !== "core" && page !== "sessions";
const scope = `${page}:${page === "pi" ? "installation" : workspace}`;
const dirtyRef = useRef(false); dirtyRef.current = dirty;
const lastUrl = useRef(location.href);
const mainRef = useRef<HTMLElement>(null);
const canLeave = () => !dirtyRef.current || window.confirm("Discard unsaved changes? Saved records and running operations will be kept.");
const resetDetail = () => { setDirty(false); setEditing(false); setSection(0); setFocused(false); setScenario("ready"); };
function writeRoute(next: Route, replace = false) {
const url = new URL(location.href);
url.searchParams.set("thoth_route", `${next.page === "core" || next.page === "sessions" ? "analysis" : "administration"}/${next.page}`);
url.searchParams.set("variant", next.variant);
if (next.workspace) url.searchParams.set("thoth_workspace", next.workspace); else url.searchParams.delete("thoth_workspace");
history[replace ? "replaceState" : "pushState"](null, "", url);
lastUrl.current = url.href; setRoute(next);
}
function navigate(next: Page) {
if (!ready || !canLeave()) return;
writeRoute({ ...route, page: next }); resetDetail(); setQuestion(""); setNavOpen(false); setResumeTarget(null); setNotice("");
requestAnimationFrame(() => mainRef.current?.focus());
}
function changeWorkspace(id: string) {
if (busy || id === workspace || !canLeave()) return;
setOperation(null); writeRoute({ ...route, workspace: id }); resetDetail(); setActive(null); setQuestion(""); setResumeTarget(null);
setNotice("Workspace changed across ThothII. Your model selection is unchanged. No operation is running.");
}
function changeModel(id: string) {
if (busy) return;
setOperation(null); setModelId(id);
setNotice("Global model updated for analytical work, administration and session resume.");
}
function cycle(delta: number) {
if (!canLeave()) return;
const keys: Variant[] = ["A", "B", "C"];
writeRoute({ ...route, variant: keys[(keys.indexOf(variant) + delta + 3) % 3] }, true);
resetDetail(); setQuestion(""); setShelfOpen(!ready);
}
useEffect(() => {
const pop = () => {
if (busyRef.current) { history.pushState(null, "", lastUrl.current); setNotice("Finish the current operation before following browser history."); return; }
if (!canLeave()) { history.pushState(null, "", lastUrl.current); return; }
setRoute(readLocation()); lastUrl.current = location.href; resetDetail(); setActive(null); setQuestion(""); setResumeTarget(null);
};
const unload = (e: BeforeUnloadEvent) => { if (dirtyRef.current) { e.preventDefault(); e.returnValue = ""; } };
window.addEventListener("popstate", pop); window.addEventListener("beforeunload", unload);
return () => { window.removeEventListener("popstate", pop); window.removeEventListener("beforeunload", unload); };
}, []);
useEffect(() => {
const key = (e: KeyboardEvent) => {
if ((e.target as HTMLElement).closest("input,textarea,select,button,[contenteditable],[role=tablist]")) return;
if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); cycle(e.key === "ArrowRight" ? 1 : -1); }
};
window.addEventListener("keydown", key); return () => window.removeEventListener("keydown", key);
});
function startJob(label: string, kind: "llm" | "embedding" | "system" = "system") {
if (busy || !ready || (kind === "llm" && !model?.metadata && admin && page !== "pi")) return;
setOperation({ id: Date.now(), label, workspace: page === "pi" || label.includes("repository") ? "installation" : workspace, model: kind === "llm" ? modelId : kind === "embedding" ? "Installation embedding model" : "No LLM required", state: "running" });
setResumeTarget(null);
setNotice("Operation started. Workspace, model and other operations are locked until it finishes.");
}
function resume(s: Session) {
if (busy || !ready || s.status === "archived" || availability === "needs-preprocessing" || !canLeave()) return;
setActive(s); setResumeTarget(null); resetDetail(); setQuestion(""); setArtifact("Question");
writeRoute({ ...route, workspace: s.workspace, page: "core" });
setOperation({ id: Date.now(), label: `Resume: ${s.title}`, workspace: s.workspace, model: modelId, state: "running" });
setNotice("Proposed resume: the original workspace and revision are retained; this run uses the globally selected model. Backend changes are required.");
}
const activeJob = busy && operation.workspace === workspace && (operation.label.startsWith("Resume:") || operation.label.startsWith("Analysis:")) ? operation : null;
const selectedContext = <div className="gc-selectors" aria-label="Global working context">
<label className="gc-selector"><span><Layers size={15}/>Workspace</span><select id="global-workspace" aria-label="Global workspace" value={workspace} onChange={e => changeWorkspace(e.target.value)} aria-describedby={busy ? "operation-lock" : undefined} disabled={busy || availability === "no-workspaces"}><option value="">Choose a workspace</option>{workspaces.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}</select><small>{active ? "Session origin retained when resuming" : "Core, sessions and administration"}</small></label>
<label className="gc-selector"><span><Cpu size={15}/>AI interaction model</span><select id="global-model" aria-label="Global AI model" value={modelId} onChange={e => changeModel(e.target.value)} aria-describedby={busy ? "operation-lock" : undefined} disabled={busy || availability === "no-models"}><option value="">Choose a model</option>{models.map(m => <option key={m.id} value={m.id}>{m.name}{m.metadata ? "" : " · Core only"}</option>)}</select><small>{model ? `${model.provider} · ${model.metadata ? "Core + administration" : "Core only"} · Next AI operation` : "Independent of the workspace"}</small></label>
</div>;
const contextBar = <div className="gc-contextbar"><div className="gc-context-label"><SlidersHorizontal size={20}/><div><strong>Working context</strong><small>One selection, all of ThothII</small></div></div>{selectedContext}<span className={`gc-context-state ${ready ? "is-ready" : ""}`}>{ready ? <Check size={15}/> : <LockKeyhole size={15}/>} {busy ? "Locked during operation" : ready ? "Ready" : "Required"}</span></div>;
const contextShelf = <section className="gc-shelf"><button className="gc-shelf-trigger" aria-expanded={shelfOpen || !ready} aria-controls="context-shelf" onClick={() => { if (ready) setShelfOpen(!shelfOpen); }}><SlidersHorizontal size={18}/><span><small>GLOBAL CONTEXT</small><strong>{currentWorkspace?.name ?? "Choose workspace"}<span className="gc-separator">/</span>{model?.name ?? "Choose AI model"}</strong></span><span className="gc-shelf-edit">{!ready ? "Selection required" : shelfOpen ? "Close" : "Change"}</span><ChevronDown size={17}/></button>{(shelfOpen || !ready) && <div id="context-shelf" className="gc-shelf-body">{selectedContext}<p>Changing workspace updates every page. Both selectors are locked while a core or administrative operation is running.</p>{ready && <Button onClick={() => setShelfOpen(false)}>Done</Button>}</div>}</section>;
const nav = (p: Page) => {
const Icon = icons[p];
return <a key={p} href={`?thoth_route=${p === "core" || p === "sessions" ? "analysis" : "administration"}/${p}&variant=${variant}&thoth_workspace=${workspace}`} aria-current={page === p && ready ? "page" : undefined} aria-disabled={!ready} tabIndex={ready ? 0 : -1} onClick={e => { e.preventDefault(); navigate(p); }}><Icon size={17}/><span>{titles[p]}</span>{p === page && ready && <ChevronRight size={14}/>}</a>;
};
const gate = <section className="gc-gate"><span className="gc-gate-icon"><LockKeyhole size={28}/></span><p className="rv-eyebrow">ThothII · Start here</p><h1>Choose your working context</h1><p>Select a workspace and an AI model in the {variant === "B" ? "context panel" : "bar above"}. Both are required before opening analytical work or administration.</p><ol><li className={currentWorkspace && availability !== "no-workspaces" ? "done" : ""}><span>{currentWorkspace ? <Check size={16}/> : "1"}</span><div><strong>Where you work</strong><small>{currentWorkspace?.name ?? "Workspace, data, knowledge and sessions"}</small></div></li><li className={model && availability !== "no-models" ? "done" : ""}><span>{model ? <Check size={16}/> : "2"}</span><div><strong>Which AI you use</strong><small>{model?.name ?? "A single model preference for the application"}</small></div></li></ol>{["unavailable","no-models","no-workspaces"].includes(availability) && <div className="gc-warning" role="alert"><strong>{availability === "unavailable" ? "The selected provider is unavailable" : availability === "no-models" ? "No usable model is configured" : "No workspace is available"}</strong><p>No activity can start. Ask the installation operator to check the catalog, credentials or workspace registry. No automatic model fallback.</p></div>}<p className="gc-gate-foot">Your data stays inside the selected workspace. AI models belong to the installation.</p></section>;
const pageHeader = (title: string, subtitle: string, eyebrow = "Analysis") => <header className="gc-page-heading"><p className="rv-eyebrow">{eyebrow} <span>/ {page === "pi" ? "Installation" : currentWorkspace?.name}</span></p><h1>{title}</h1><p>{subtitle}</p></header>;
const sessionList = <>{pageHeader("Your analytical sessions", "Resume unfinished work or inspect the retained artifacts of a completed analysis.")}<div className="gc-session-list">{sessions.filter(s => s.workspace === workspace).map(s => <article key={s.id}><div><span className={`rv-tag ${s.status === "paused" ? "rv-tag-warning" : ""}`}>{s.status === "paused" ? "Paused · Can resume" : "Archived · Read-only"}</span><h2 lang="it">{s.title}</h2><p>Phase {s.phase} of 8 · Workspace revision <code>{s.revision}</code></p><small>Last model: {models.find(m => m.id === s.lastModel)?.name}</small></div><Button disabled={busy} primary={s.status === "paused"} onClick={() => { if (s.status === "paused") setResumeTarget(s); else { setActive(s); writeRoute({ ...route, page: "core" }); } }}>{s.status === "paused" ? "Review resume" : "View artifacts"}<ArrowUpRight size={15}/></Button></article>)}{!sessions.some(s => s.workspace === workspace) && <div className="rv-empty"><h2>No sessions in this workspace</h2><p>Start an analytical question to create the first session.</p><Button primary onClick={() => navigate("core")}>Start analytical work</Button></div>}</div></>;
const resumeReview = resumeTarget && <section className="gc-resume" role="region" aria-label="Resume review"><p className="rv-eyebrow">Before resuming</p><h2 lang="it">{resumeTarget.title}</h2><dl><div><dt>Original workspace, retained</dt><dd>{workspaces.find(w => w.id === resumeTarget.workspace)?.name} · <code>{resumeTarget.revision}</code></dd></div><div><dt>Model for this resume</dt><dd>{model?.name} <small>From your global selection, not the session’s previous model</small></dd></div></dl>{resumeTarget.workspace !== workspace && <p className="gc-warning">Opening this session will switch the global workspace to its original workspace. Your model selection will not change.</p>}{availability === "needs-preprocessing" && <p className="gc-warning">Workspace preprocessing is required before resume.</p>}<div className="rv-actions"><Button primary disabled={busy || !ready || availability === "needs-preprocessing"} onClick={() => resume(resumeTarget)}>Resume with {model?.name ?? "selected model"}</Button><Button onClick={() => setResumeTarget(null)}>Cancel</Button></div></section>;
function submitQuestion() {
if (!ready || !question.trim() || availability === "needs-preprocessing" || busy) return;
const title = question.trim(); setActive({ id: `demo-${Date.now()}`, title, workspace, revision: "a71c9f2", status: "paused", lastModel: modelId, phase: 1 });
setQuestion(""); setDirty(false); startJob(`Analysis: ${title}`, "llm");
}
const core = <>{pageHeader(active ? active.title : "From a question to a trusted answer", active ? "Persisted artifacts and reviewer decisions guide this session." : "Ask about your data. Review the evidence, approve the interpretation, then build the query.")}
{availability === "needs-preprocessing" && <div className="gc-warning"><strong>Prepare this workspace before starting the core</strong><p>Workspace and model are selected. Administration remains available so you can complete preprocessing.</p><Button onClick={() => navigate("workspace")}>Open workspace readiness</Button></div>}
{active ? <div className="gc-analysis"><div className="gc-session-context"><LockKeyhole size={15}/><strong>{currentWorkspace?.name}</strong><span>Session revision {active.revision}</span><span>{active.status === "archived" ? "Read-only archive" : activeJob ? `Run model: ${models.find(m => m.id === activeJob.model)?.name}` : `Next AI operation: ${model?.name}`}</span></div><div className="gc-workflow"><ol>{["Question", "Schema linking", "SQL draft", "Validation"].map((t,i) => <li key={t} className={i === 1 ? "current" : ""}><span>{String(i+1).padStart(2,"0")}</span>{t}</li>)}</ol><small>Workflow excerpt · 8 phases in production</small></div><div className="gc-artifact-layout"><section className="gc-artifact"><nav aria-label="Session artifacts">{["Question","Schema","SQL"].map(t => <button key={t} aria-current={artifact === t ? "page" : undefined} onClick={() => setArtifact(t)}>{t}</button>)}</nav><div><p className="rv-eyebrow">Persisted artifact · example</p><h2>{artifact === "Question" ? "Domanda analitica" : artifact === "Schema" ? "Collegamento allo schema" : "Query proposta"}</h2>{artifact === "SQL" ? <pre><code>{`SELECT COUNT(DISTINCT patient_id)\nFROM ${currentWorkspace?.schema}.visits\nWHERE visited_at >= DATE '2025-01-01'\n AND visited_at < DATE '2026-01-01';`}</code></pre> : <><p lang="it">{artifact === "Question" ? active.title : "La relazione tra pazienti e visite definisce la popolazione osservata. Il conteggio deve mantenere distinti i pazienti."}</p><div className="gc-evidence-line"><FileText size={16}/><span>Evidence: popolazione di riferimento</span></div><p className="rv-small">Synthetic illustration, not generated from a database.</p></>}</div></section><section className="gc-review"><p className="rv-eyebrow">Human review</p><h2>{active.status === "archived" ? "Retained decisions" : activeJob ? "AI operation in progress" : "Confirm the interpretation"}</h2><p>{active.status === "archived" ? "This archive can be inspected, but its workflow cannot currently be resumed." : activeJob ? "Workspace and model are locked until this operation finishes. No other operation can start in the meantime." : "Check the proposed population and time range before proceeding."}</p>{active.status !== "archived" && <Button primary disabled={busy} onClick={() => startJob(`Analysis: ${active.title}`, "llm")}>{activeJob ? "Waiting for result" : `Approve & continue with ${model?.name}`}</Button>}<p className="rv-small">{active.status === "archived" ? "Archived and finalized sessions remain read-only." : "Model output proposes. The reviewer decides."}</p></section></div></div> : <div className="gc-core-start"><section className="gc-question"><div className="gc-question-heading"><span className="gc-symbol"><MessageSquare size={23}/></span><div><h2>Start an analytical question</h2><p>{currentWorkspace?.database} · Evidence and Memory in this workspace</p></div></div><form onSubmit={e => { e.preventDefault(); submitQuestion(); }}><label htmlFor="analysis-question" className="rv-eyebrow">Your question</label><textarea id="analysis-question" rows={4} value={question} placeholder="Quanti pazienti hanno almeno una visita di follow-up nel 2025?" onChange={e => { setQuestion(e.target.value); setDirty(!!e.target.value); }}/><div className="gc-composer-footer"><span><Cpu size={15}/>{model?.name}<small>Global model</small></span><button className="rv-button rv-primary" type="submit" disabled={!question.trim() || availability === "needs-preprocessing" || busy}>Start analysis<ArrowRight size={16}/></button></div></form></section><section className="gc-recent"><div className="gc-section-heading"><h2>Continue where you left off</h2><Button onClick={() => navigate("sessions")}>All sessions<ArrowUpRight size={14}/></Button></div>{sessions.filter(s => s.workspace === workspace && s.status === "paused").map(s => <button className="gc-recent-session" key={s.id} disabled={busy} onClick={() => setResumeTarget(s)}><span className="gc-session-number">03</span><span><strong lang="it">{s.title}</strong><small>Paused at schema linking · Original workspace retained</small></span><ArrowUpRight size={19}/></button>)}{!sessions.some(s => s.workspace === workspace && s.status === "paused") && <p className="rv-small">No unfinished sessions in this workspace.</p>}</section><section className="gc-method"><p className="rv-eyebrow">A reviewable path</p><div><span><strong>01</strong>Ask a question</span><ChevronRight size={15}/><span><strong>02</strong>Review context</span><ChevronRight size={15}/><span><strong>03</strong>Validate the query</span></div><p>Every analytical result is backed by persisted artifacts and explicit review decisions.</p></section></div>}
</>;
const rows = page === "memory" ? cards[workspace] ?? inventory.memory : admin ? inventory[page] : [];
const selected = selection[scope] ?? rows[0]?.id;
const item = rows.find(r => r.id === selected);
const hasList = ["database","memory","evidence"].includes(page);
const list = <div className="rv-index"><div className="rv-index-title"><h2>{admin ? modules[page].noun : ""}</h2><span>{rows.length}</span></div><label className="rv-search"><Search size={16}/><input aria-label="Search records" placeholder="Search records" value={filters[scope] ?? ""} onChange={e => setFilters(f => ({ ...f, [scope]: e.target.value }))}/></label><div className="rv-records">{rows.filter(r => r.title.toLowerCase().includes((filters[scope] ?? "").toLowerCase())).map(r => <button key={r.id} className="rv-record" aria-current={selected === r.id ? "true" : undefined} onClick={() => { if (!canLeave()) return; setSelection(s => ({ ...s, [scope]: r.id })); setFocused(true); setEditing(false); setDirty(false); }}><small>{r.category}</small><strong>{r.title}</strong><span>{r.meta}</span><ChevronRight size={15}/></button>)}</div></div>;
function changeSection(n: number) { if (!canLeave()) return; setSection(n); setEditing(false); setDirty(false); setFocused(true); }
const sections = admin && <nav className={variant === "C" ? "rv-step-nav" : "rv-tabs"} aria-label="Management sections">{modules[page].sections.map((s,i) => <button key={s} aria-current={section === i ? "page" : undefined} onClick={() => changeSection(i)}>{variant === "C" && <span>{String(i+1).padStart(2,"0")}</span>}{s}</button>)}</nav>;
const contentProps: ContentProps | null = admin && ready && currentWorkspace && model ? {
surface: page, workspace: currentWorkspace, modelName: model.name, providerName: model.provider, scenario, item, section, editing,
onEdit: () => setEditing(true), onDirty: setDirty, onCancel: () => { if (canLeave()) { setEditing(false); setDirty(false); } },
onSave: updated => { if (updated) { setCards(c => ({ ...c, [workspace]: selected === "new" ? [updated, ...rows] : rows.map(r => r.id === updated.id ? updated : r) })); setSelection(s => ({ ...s, [scope]: updated.id })); } setDirty(false); setEditing(false); setNotice("Saved in this workspace’s demo data only."); },
onDelete: () => { const rest = rows.filter(r => r.id !== selected); setCards(c => ({ ...c, [workspace]: rest })); setSelection(s => ({ ...s, [scope]: rest[0]?.id ?? "" })); },
connectionValues: connections[workspace], onConnectionSave: values => { setConnections(c => ({ ...c, [workspace]: values })); setEditing(false); setDirty(false); setNotice("Connection saved in memory only. No real credentials were used."); },
onNavigate: navigate, onSection: changeSection, onScenario: setScenario, onNotice: setNotice,
onOperation: label => startJob(label, /generation|sensitivity|model test/i.test(label) ? "llm" : /preprocessing|index/i.test(label) ? "embedding" : "system"),
} : null;
const incompatible = page === "database" && section === 2 && !model?.metadata;
const detail = <>{incompatible && <div className="gc-warning" role="alert"><strong>{model?.name} is not enabled for metadata AI</strong><p>Select a model enabled for core and administration in the global selector. No other model will be used automatically.</p><Button onClick={() => { setShelfOpen(true); requestAnimationFrame(() => document.getElementById("global-model")?.focus()); }}>Change global model</Button></div>}<fieldset className="gc-content-fieldset" disabled={busy || incompatible}>{contentProps && <Content key={`${scope}:${selected}:${section}:${editing}`} {...contentProps}/>}</fieldset></>;
const administration = admin && <>{pageHeader(modules[page].title, modules[page].subtitle, "Administration")}<div className="gc-admin-context"><span>{page === "pi" ? "Installation scope: applies to all workspaces" : `Workspace: ${currentWorkspace?.name}`}</span><span>{page === "pi" ? "Global LLM selection is not configuration authoring" : `Global LLM: ${model?.name}`}</span>{page === "memory" && <Button primary disabled={busy} onClick={() => { if (busy || !canLeave()) return; setSelection(s => ({ ...s, [scope]: "new" })); setEditing(true); setFocused(true); setDirty(false); setSection(0); }}>New card</Button>}</div>
{page === "workspace" && <p className="gc-footnote">Preprocessing uses the installation embedding model. Selecting an interaction LLM does not change index compatibility.</p>}
{variant === "A" && <div className={`gc-management-split ${hasList ? "has-list" : ""}`}>{hasList && list}<div>{sections}{detail}</div></div>}
{variant === "B" && <div className="rv-focus">{sections}{hasList && !focused && section === 0 ? list : <>{hasList && <Button className="rv-back" onClick={() => { if (!canLeave()) return; resetDetail(); }}><ArrowLeft size={15}/>Back to records</Button>}{detail}</>}</div>}
{variant === "C" && <div className="rv-operations"><div className="gc-operation-intro"><ShieldCheck size={19}/><span>{page === "pi" ? "Installation diagnostics" : `${currentWorkspace?.name} · Operational context`}</span><strong>{scenario === "ready" ? "Ready for review" : scenario}</strong></div><div className="rv-operation-body">{sections}<div className="rv-operation-detail">{hasList && section === 0 && <details className="rv-choose-record"><summary>Choose record<span>{item?.title ?? "New record"}</span></summary>{list}</details>}{detail}</div></div></div>}
</>;
return <><div className={`rv-host rv-host-${width} gc-host ${portalOpen ? "gc-portal-open" : ""}`}><header className="rv-topbar"><button aria-label="Toggle portal navigation" onClick={() => setPortalOpen(!portalOpen)}><Menu size={20}/></button><strong>Omics <span>Portal</span></strong><span className="rv-topbar-divider"/><span className="rv-topbar-location">Clinical data & research</span><span className="rv-topbar-user">Administrator <span className="rv-avatar">AD</span></span></header><div className="gc-host-body"><aside className="gc-portal" aria-label="Illustrative portal navigation"><p className="rv-eyebrow">Portal</p>{[[LayoutDashboard,"Overview"],[Users,"Patients"],[Database,"Clinical data"],[Settings2,"ThothII"]].map(([Icon,label]) => { const I = Icon as typeof Menu; return <div key={String(label)} className={label === "ThothII" ? "current" : ""}><I size={18}/><span>{String(label)}</span></div>; })}<small>Host shell preview</small></aside><div className="gc-mount"><div className="gc-brandline"><span className="gc-brand">Thoth<span>II</span></span><span>Clinical analytics workbench</span><small>Interactive design study · V3</small></div><div className={`gc-app gc-variant-${variant}`}><div className="gc-global-top">{variant === "A" ? contextBar : variant === "C" ? contextShelf : null}</div>{operation && <section className="gc-operation-strip" aria-label="Current operation"><div><p className="rv-eyebrow">{busy ? "Operation running" : `Operation ${operation.state}`}</p><strong>{operation.label}</strong><small>{workspaces.find(w => w.id === operation.workspace)?.name ?? "Installation"} · {models.find(m => m.id === operation.model)?.name ?? operation.model}</small><p id="operation-lock">{busy ? "Workspace and model are locked. Finish this operation before starting another." : "Workspace and model are available again."}</p></div>{busy && <div className="rv-actions"><Button onClick={() => setOperation({ ...operation, state: "complete" })}>Demo: finish</Button><Button onClick={() => setOperation({ ...operation, state: "cancelled" })}>Demo: stop</Button><Button onClick={() => setOperation({ ...operation, state: "failed" })}>Demo: fail</Button></div>}</section>}<div className="gc-body"><aside className="gc-rail">{variant === "B" && <section className="gc-rail-context"><p className="rv-eyebrow">Working context</p>{selectedContext}<span className={`gc-context-state ${ready ? "is-ready" : ""}`}>{ready ? <Check size={14}/> : <LockKeyhole size={14}/>} {busy ? "Locked during operation" : ready ? "Ready across ThothII" : "Both selections required"}</span></section>}<button className="gc-mobile-nav" onClick={() => setNavOpen(!navOpen)} aria-expanded={navOpen}><Menu size={17}/>Application navigation<ChevronDown size={16}/></button><div className={`gc-navigation ${navOpen ? "open" : ""}`}><p className="rv-eyebrow">Analytical work</p><nav aria-label="Core navigation">{nav("core")}{nav("sessions")}</nav><p className="rv-eyebrow">Administration</p><nav aria-label="Administration navigation">{(["database","memory","evidence","workspace","pi"] as Page[]).map(nav)}</nav><div className="gc-rail-caption"><ShieldCheck size={18}/><p>One workspace at a time.<br/>One model preference.<br/>One operation at a time.</p></div></div></aside><main ref={mainRef} tabIndex={-1} className={`gc-main rv-variant-${variant}`} aria-label={ready ? titles[page] : "Context required"}>{notice && <div className="rv-notice" role="status"><span>{notice}</span><button aria-label="Dismiss notice" onClick={() => setNotice("")}><X size={16}/></button></div>}{dirty && <div className="rv-dirty" role="status">Unsaved draft. Save or discard before changing workspace or page.</div>}{!ready ? gate : <>{resumeReview}{page === "core" && active && <div className="gc-new-question"><Button disabled={busy} onClick={() => { if (busy || !canLeave()) return; setActive(null); setQuestion(""); setDirty(false); setNotice("Ready for a new question in the selected workspace."); }}>New question<ArrowRight size={15}/></Button></div>}{page === "core" ? core : page === "sessions" ? sessionList : administration}</>}<footer className="gc-page-footer"><span>Prototype · Synthetic content only</span><span>{ready ? "Context applies across ThothII" : "Core and administration are locked"}</span></footer></main></div></div></div></div></div>
{import.meta.env.DEV && <aside className="rv-lab gc-lab" aria-label="Prototype review controls"><div className="rv-lab-main"><span className="rv-lab-tag">FULL APP · V3</span><button aria-label="Previous variant" onClick={() => cycle(-1)}><ArrowLeft size={17}/></button><select aria-label="Layout variant" value={variant} onChange={e => { if (canLeave()) { writeRoute({ ...route, variant: e.target.value as Variant }, true); resetDetail(); setQuestion(""); setShelfOpen(!ready); } }}>{Object.entries(variants).map(([k,v]) => <option key={k} value={k}>{k} · {v}</option>)}</select><button aria-label="Next variant" onClick={() => cycle(1)}><ArrowRight size={17}/></button><span className="rv-lab-help">No server writes</span></div><details><summary>Review scenarios & state</summary><div className="rv-lab-options"><label>Host width<select aria-label="Host width" value={width} onChange={e => setWidth(e.target.value)}><option value="full">Available width</option><option value="compact">960 px host</option><option value="phone">390 px host</option></select></label><label>Availability<select aria-label="Availability scenario" disabled={busy} value={availability} onChange={e => { if (canLeave()) { setAvailability(e.target.value as Availability); resetDetail(); } }}><option value="available">Available</option><option value="unavailable">Provider unavailable</option><option value="no-workspaces">No workspaces</option><option value="no-models">No models</option><option value="needs-preprocessing">Needs preprocessing</option></select></label><Button disabled={busy} onClick={() => { if (busy || !canLeave()) return; setOperation(null); writeRoute({ ...route, workspace: "" }); setModelId(""); resetDetail(); setActive(null); setQuestion(""); setResumeTarget(null); setShelfOpen(true); }}>Reset selections</Button><Button disabled={busy || !ready} onClick={() => { if (!busy && canLeave()) { setResumeTarget(sessions[2]); setNavOpen(false); } }}>Demo: Genetics session link</Button><p>Page: {page} · Workspace: {workspace || "none"} · Model: {model?.name ?? "none"} · Gate: {ready ? "open" : "closed"} · Draft: {dirty ? "unsaved" : "clean"} · Operation: {operation?.state ?? "idle"}</p><p>In-memory simulation. Reload resets the model, drafts and the simulated operation. URL retains page, workspace and variant. Legacy A/B/C remain on ports 5174 and 5175.</p></div></details></aside>}
</>;
}
@@ -0,0 +1,95 @@
import { useState, type ReactNode } from "react";
import { ArrowUpRight, Check, CircleAlert, Clock3, Copy, Database, FileText, GitBranch, KeyRound, Play, RefreshCw, ShieldCheck } from "lucide-react";
import { Button } from "./ui";
import { type Item, type Scenario, type Surface, type workspaces } from "../administration-review/model";
export function Status({ scenario }: { scenario: Scenario }) {
const Icon = scenario === "ready" ? Check : scenario === "running" ? Clock3 : CircleAlert;
return <span className={`rv-status rv-status-${scenario}`}><Icon size={14} />{{ ready: "Ready", attention: "Needs attention", running: "In progress", empty: "Not configured", error: "Unavailable" }[scenario]}</span>;
}
function Section({ title, children, label }: { title: string; children: ReactNode; label?: string }) {
return <section className="rv-section">{label && <p className="rv-eyebrow">{label}</p>}<h3>{title}</h3>{children}</section>;
}
function Facts({ data }: { data: Array<[string, ReactNode]> }) { return <dl className="rv-facts">{data.map(([name,value]) => <div key={name}><dt>{name}</dt><dd>{value}</dd></div>)}</dl>; }
function CopyValue({ value, label }: { value: string; label: string }) {
const [copied, setCopied] = useState(false);
return <div className="rv-code"><code>{value}</code><button aria-label={`Copy ${label}`} onClick={async () => { try { await navigator.clipboard.writeText(value); setCopied(true); } catch { setCopied(false); } }}>{copied ? <Check size={16} /> : <Copy size={16} />}</button></div>;
}
export type ConnectionValues = { host: string; port: string; database: string; user: string };
export type ContentProps = {
modelName: string; providerName: string; surface: Surface; workspace: typeof workspaces[number]; scenario: Scenario; item?: Item; section: number; editing: boolean;
onEdit: () => void; onDirty: (v: boolean) => void; onCancel: () => void; onSave: (item?: Item) => void;
onDelete: () => void; onNavigate: (s: Surface) => void; onOperation: (label: string) => void;
onNotice: (text: string) => void; onSection: (section: number) => void; onScenario: (s: Scenario) => void;
connectionValues?: ConnectionValues; onConnectionSave: (values: ConnectionValues) => void;
};
export function Content(p: ContentProps) {
if (p.scenario === "error") return <div className="rv-content"><div className="rv-empty rv-error" role="alert"><CircleAlert size={28}/><h2>This page could not be loaded</h2><p>The service is unavailable. Your last saved data has not been changed.</p><Button primary onClick={() => p.onScenario("ready")}><RefreshCw size={15}/>Retry loading</Button></div></div>;
if (p.scenario === "empty" && !p.editing) return <div className="rv-content"><div className="rv-empty"><FileText size={30}/><h2>{p.surface === "memory" ? "No Memory cards yet" : p.surface === "evidence" ? "No Evidence files in this workspace" : p.surface === "database" ? "Database not configured" : p.surface === "workspace" ? "Workspace preparation required" : "No available session models"}</h2><p>{p.surface === "memory" ? "Create a card to record a reusable clarification or an approved SQL rule." : p.surface === "evidence" ? "Create a Markdown file in the curated archive, then run consolidation to activate it." : p.surface === "database" ? "Configure one catalog binding for this workspace. The source database remains read-only." : p.surface === "workspace" ? "Check the source and configure the database before running preprocessing." : "Add eligible models to the Installation Model Catalog and reload the runtime."}</p>{p.surface === "database" && <Button primary onClick={() => { p.onScenario("ready"); p.onSection(1); }}>Configure catalog</Button>}{p.surface === "workspace" && <Button onClick={() => p.onNavigate("database")}>Open Database management</Button>}{p.surface === "evidence" && <CopyValue value={`evidence/curated/domain/new-rule.md`} label="example file path"/>}</div></div>;
return <div className="rv-content">
{p.scenario === "running" && <div className="rv-operation-banner" role="status"><Clock3 size={18}/><div><strong>Operation in progress</strong><p>The job belongs to {p.surface === "pi" ? "this installation" : p.workspace.name}. Changing pages does not cancel it.</p><div className="rv-progress" role="progressbar" aria-label="Illustrative operation progress" aria-valuenow={55} aria-valuemin={0} aria-valuemax={100}><span/></div><Button onClick={() => { p.onScenario("ready"); p.onNotice("Demonstration completed. The server was not contacted."); }}>Demo: complete operation</Button></div></div>}
{p.surface === "workspace" ? <Workspace {...p}/> : p.surface === "database" ? <Catalog {...p}/> : p.surface === "evidence" ? <Evidence {...p}/> : p.surface === "memory" ? <Memory {...p}/> : <Pi {...p}/>}
</div>;
}
function Workspace(p: ContentProps) {
const [clear, setClear] = useState(false);
const [secret, setSecret] = useState("");
return <><div className="rv-record-heading"><div><p className="rv-eyebrow">Selected workspace</p><h2>{p.workspace.name}</h2><p className="rv-mono">{p.workspace.id}</p></div><LayersMark/></div>
{p.section === 0 && <>
<Section title="Preparation for analysis" label="Readiness"><p>Preprocessing builds the reference indexes for this workspace from its catalog snapshot and Evidence. Memory has its own independent index.</p><ol className="rv-checklist"><li><Check/><div><strong>Workspace source</strong><span>Active Git revision · <code>a71c9f2</code></span></div></li><li><Check/><div><strong>Database catalog</strong><span>{p.workspace.database} · Metadata revision 12</span></div><button onClick={() => p.onNavigate("database")} aria-label="Open database prerequisite"><ArrowUpRight size={17}/></button></li><li>{p.scenario === "attention" ? <CircleAlert/> : <Check/>}<div><strong>Reference indexes</strong><span>{p.scenario === "attention" ? "Built from revision 11. Run preprocessing to use revision 12." : "Current for metadata revision 12"}</span></div></li></ol></Section>
<Section title="Preprocessing"><div className="rv-callout"><ShieldCheck size={20}/><div><strong>Workspace-scoped operation</strong><p>Uses the catalog snapshot, prepares schema and reference indexes, and includes configured Evidence. It does not modify the source database.</p></div></div><Facts data={[["Catalog revision", "12"],["Preprocessed revision", p.scenario === "attention" ? "11" : "12"],["Reference collection", `${p.workspace.id}_reference`],["Last successful run", "Today · 09:42 (sample)"]]}/><div className="rv-actions"><Button primary disabled={p.scenario === "running"} onClick={() => p.onOperation("Workspace preprocessing")}><Play size={15}/>Run preprocessing</Button><Button onClick={() => p.onNavigate("database")}><Database size={15}/>Open database</Button></div></Section>
<details className="rv-disclosure"><summary>Clear derived data</summary><p>Remove schema/Evidence vectors, LSH and derived checkpoints. Keep the curated files, catalog, source database, Memory and sessions.</p>{clear ? <div className="rv-confirm"><strong>Clear derived data for {p.workspace.name}?</strong><div className="rv-actions"><Button onClick={() => { setClear(false); p.onScenario("attention"); p.onNotice("Derived data cleared in the simulation. Authoritative data was kept."); }}>Confirm clear</Button><Button onClick={() => setClear(false)}>Cancel</Button></div></div> : <Button disabled={p.scenario === "running"} onClick={() => setClear(true)}>Clear derived data…</Button>}</details>
</>}
{p.section === 1 && <><Section title="Source identity" label="Git-managed"><p>Change identity and Evidence source declarations in the workspace repository. Fetch and validate the candidate revision before activating it.</p><Facts data={[["Source file", `${p.workspace.id}/workspace.yaml`],["Active Git revision", "a71c9f2"],["Language", "Italian"],["Database binding", <button className="rv-text-button" onClick={() => p.onNavigate("database")}>{p.workspace.database} <ArrowUpRight size={14}/></button>]]}/><div className="rv-actions"><Button onClick={() => p.onNotice("Simulated source validation passed. No repository files were changed.")}>Validate workspace source</Button><Button onClick={() => p.onNotice("Simulated connection check passed.")}>Test workspace connections</Button></div></Section><Section title="Shared workspace repository"><p>Repository updates apply to all workspaces listed in the registry. An invalid candidate leaves the previous revision active.</p><Button disabled={p.scenario === "running"} onClick={() => p.onOperation("Shared workspace repository update")}><GitBranch size={15}/>Update workspace repository</Button></Section></>}
{p.section === 2 && <Section title="Runtime secrets" label="Workspace requirements"><p>Only non-database runtime requirements belong here. Database credentials are maintained in Database management. Stored values are never revealed.</p><label className="rv-field">Evidence source access token <span className="rv-small">Illustrative optional connector requirement</span><input type="password" autoComplete="new-password" placeholder="Enter a replacement value" value={secret} onChange={e => { setSecret(e.target.value); p.onDirty(!!e.target.value); }}/></label><div className="rv-actions"><Button primary disabled={!secret} onClick={() => { setSecret(""); p.onSave(); }}>Save entered secret</Button><Button onClick={() => p.onNavigate("database")}>Database credentials</Button></div></Section>}
</>;
}
function LayersMark() { return <span className="rv-record-emblem"><GitBranch size={24}/></span>; }
function Catalog(p: ContentProps) {
const [tab, setTab] = useState("Columns");
const [connection, setConnection] = useState(p.connectionValues ?? { host: "warehouse.internal", port: "5432", database: p.workspace.database, user: "readonly_analyst" });
const [password, setPassword] = useState("");
return <><div className="rv-record-heading"><div><p className="rv-eyebrow">{p.workspace.name} / Catalog</p><h2>{p.section === 0 ? `${p.workspace.schema}.${p.item?.title ?? "patients"}` : p.workspace.database}</h2><p>{p.section === 0 ? p.item?.detail : "One workspace binding · PostgreSQL · Read-only source access"}</p></div><span className="rv-record-emblem"><Database size={24}/></span></div>
<div className="rv-crosslink"><span>Workspace preparation {p.scenario === "attention" ? "needs a refresh" : "uses metadata revision 12"}.</span><button onClick={() => p.onNavigate("workspace")}>Open workspace <ArrowUpRight size={14}/></button></div>
{p.section === 0 && <><nav className="rv-subtabs" aria-label="Schema detail">{["Columns","Relationships"].map(t => <button key={t} aria-current={tab === t ? "page" : undefined} onClick={() => setTab(t)}>{t}</button>)}</nav>{tab === "Columns" ? <><div className="rv-table-scroll"><table><caption>Catalog columns · illustrative excerpt</caption><thead><tr><th>Column</th><th>Type</th><th>Description</th><th>Sensitivity</th></tr></thead><tbody>{[["patient_id","uuid","Identificativo del paziente","Identifier"],["recorded_at","timestamp","Data di registrazione","Not marked"],["status","varchar","Stato del record","Not marked"],["source_code","varchar","Codice del sistema origine","Needs review"]].map(([name,type,description,sensitivity]) => <tr key={name}><td><code>{name}</code></td><td><code>{type}</code></td><td>{description}</td><td><span className={sensitivity === "Needs review" ? "rv-tag rv-tag-warning" : "rv-tag"}>{sensitivity}</span></td></tr>)}</tbody></table></div><p className="rv-small">Column descriptions and sensitivity are catalog metadata. Source values are not displayed in this prototype.</p></> : <div className="rv-table-scroll"><table><caption>Catalog relationships</caption><thead><tr><th>From</th><th>To</th><th>Kind</th></tr></thead><tbody><tr><td><code>visits.patient_id</code></td><td><code>patients.patient_id</code></td><td>Foreign key</td></tr><tr><td><code>samples.patient_id</code></td><td><code>patients.patient_id</code></td><td>Foreign key</td></tr></tbody></table></div>}<div className="rv-actions"><Button disabled={p.scenario === "running"} onClick={() => p.onOperation("Schema synchronization")}><RefreshCw size={15}/>Synchronize schema</Button><Button onClick={() => p.onSection(2)}>Review metadata</Button></div></>}
{p.section === 1 && <Section title="Connection settings"><p>Connection and credentials are scoped to this workspace binding. Saving a connection does not synchronize the schema.</p><form onSubmit={e => { e.preventDefault(); p.onConnectionSave(connection); }}><div className="rv-form-grid">{Object.entries(connection).map(([key,value]) => <label key={key} className="rv-field">{key[0].toUpperCase()+key.slice(1)}<input required disabled={!p.editing} value={value} onChange={e => { setConnection(s => ({ ...s, [key]: e.target.value })); p.onDirty(true); }}/></label>)}<label className="rv-field rv-span-all">Replacement password<input type="password" autoComplete="new-password" placeholder="Stored password remains hidden" disabled={!p.editing} value={password} onChange={e => { setPassword(e.target.value); p.onDirty(true); }}/></label></div><div className="rv-actions">{p.editing ? <><button className="rv-button rv-primary" type="submit">Save connection</button><Button onClick={p.onCancel}>Cancel</Button></> : <><Button primary onClick={p.onEdit}>Edit connection</Button><Button onClick={() => p.onNotice("Connection test passed in the simulation.")}>Test connection</Button></>}</div></form></Section>}
{p.section === 2 && <><Section title="Metadata descriptions"><p>Generate draft descriptions from the catalog and permitted source samples. Review and consolidate them using the existing catalog workflow.</p><Facts data={[["Tables described", "4 of 4"],["Columns described", "62 of 70"],["Selected LLM", p.modelName],["Scope", p.workspace.schema]]}/><Button primary disabled={p.scenario === "running"} onClick={() => p.onOperation("Description generation")}>Generate descriptions</Button></Section><Section title="Sensitivity analysis"><p>Review column sensitivity before sampling and analytical use. Source synchronization, description generation and sensitivity analysis remain distinct operations.</p><div className="rv-actions"><Button disabled={p.scenario === "running"} onClick={() => p.onOperation("Sensitivity analysis")}>Analyze sensitivity</Button><Button onClick={() => p.onNotice("Sample review: source_code requires an administrator’s sensitivity classification.")}>Review suggestions</Button></div></Section></>}
</>;
}
function Evidence(p: ContentProps) {
const [decision, setDecision] = useState("");
return <>
{p.section === 0 && <><div className="rv-record-heading"><div><p className="rv-eyebrow">{p.item?.category ?? "Domain knowledge"}</p><h2>{p.item?.title}</h2><p><span className="rv-tag">Active locally</span> <span className="rv-small">Canonical Markdown archive</span></p></div><span className="rv-record-emblem"><FileText size={24}/></span></div><Section title="Content"><p className="rv-reading" lang="it">{p.item?.detail}</p></Section><Section title="Scope"><Facts data={[["Workspace", p.workspace.name],["Table", `${p.workspace.schema}.patients`],["Purpose", "Domain clarification · Schema linking"],["Source", "Manual declaration (sample)"]]}/></Section><Section title="File & provenance"><p>Edit this document with your preferred external editor. Consolidation activates valid local changes; Git review and push are manual.</p><CopyValue value={`evidence/curated/domain/${p.item?.id}.md`} label="relative Evidence path"/><div className="rv-actions"><Button onClick={() => p.onSection(2)}>Maintenance instructions</Button><Button onClick={() => p.onSection(1)}>Sources & comparisons</Button></div></Section></>}
{p.section === 1 && <><div className="rv-record-heading"><div><p className="rv-eyebrow">Source maintenance</p><h2>Refresh deliberately, review changes</h2><p>External sources are reacquired only when requested. A manual correction remains in force until you resolve the comparison.</p></div></div><div className="rv-actions"><Button primary disabled={p.scenario === "running"} onClick={() => p.onOperation("Evidence source refresh")}>Import or refresh sources</Button></div><Section title="Source comparison" label="Illustrative review"><div className="rv-comparison"><div><p className="rv-eyebrow">Current manual correction</p><p lang="it">Contare i pazienti distinti con almeno una visita valida.</p></div><div><p className="rv-eyebrow">Refreshed source proposal</p><p lang="it">Contare i record delle visite nel periodo selezionato.</p></div></div><p>Replacing the content requires an explicit decision. Refresh alone does not overwrite it.</p>{decision ? <p role="status" className="rv-callout">{decision} (prototype only)</p> : <div className="rv-actions"><Button primary onClick={() => setDecision("Current correction kept")}>Keep current correction</Button><Button onClick={() => setDecision("Source proposal selected for activation")}>Use source proposal</Button></div>}</Section></>}
{p.section === 2 && <><div className="rv-record-heading"><div><p className="rv-eyebrow">External editing</p><h2>Maintain the Evidence archive</h2><p>The browser helps you locate and verify knowledge. Your editor maintains the Markdown files.</p></div></div><ol className="rv-instructions"><li><strong>Edit the canonical files</strong><p>Use the installation host path shown by the real application. This demonstration shows relative paths only.</p><CopyValue value={`${p.workspace.id}/evidence/curated/`} label="archive directory"/></li><li><strong>Validate and consolidate</strong><p>Correct validation errors, then activate the local index. Source refresh is a separate operation.</p><CopyValue value={`tht --installation /path/to/thothii-installation.yaml workspace evidence consolidate --workspace ${p.workspace.id}`} label="consolidation command"/></li><li><strong>Review and publish with Git</strong><p>From the Evidence working tree, inspect the status and diff, then commit and push manually.</p><CopyValue value="git status\ngit diff" label="Git review commands"/></li></ol><Button onClick={() => p.onNotice("File list refreshed in the demonstration. No host files were read.")}><RefreshCw size={15}/>Refresh files</Button></>}
</>;
}
function Memory(p: ContentProps) {
const [title,setTitle] = useState(p.item?.title ?? "");
const [detail,setDetail] = useState(p.item?.detail ?? "");
const [category,setCategory] = useState(p.item?.category ?? "Domain clarification");
const [confirmDelete,setConfirmDelete] = useState(false);
if (p.editing) return <form className="rv-memory-form" onSubmit={e => { e.preventDefault(); p.onSave({ id: p.item?.id ?? `demo-${Date.now()}`, title, detail, category, meta: "Manual correction · Indexed (demo)" }); }}><div className="rv-record-heading"><div><p className="rv-eyebrow">{p.workspace.name}</p><h2>{p.item ? "Edit Memory card" : "New Memory card"}</h2><p>Save an explicit, reusable rule. Changes stay in this prototype.</p></div></div><label className="rv-field">Title<input required value={title} onChange={e => {setTitle(e.target.value);p.onDirty(true);}}/></label><label className="rv-field">Family<select value={category} onChange={e => {setCategory(e.target.value);p.onDirty(true);}}>{["Domain clarification","SQL rule","Solved question","Explained error"].map(v=><option key={v}>{v}</option>)}</select></label><label className="rv-field">Content<textarea required rows={6} value={detail} onChange={e=>{setDetail(e.target.value);p.onDirty(true);}}/></label><div className="rv-actions"><button type="submit" className="rv-button rv-primary">Save card</button><Button onClick={p.onCancel}>Cancel</Button></div></form>;
if (!p.item) return <div className="rv-empty"><h2>No cards to display</h2><p>Create a new card using the action above.</p></div>;
return <><div className="rv-record-heading"><div><p className="rv-eyebrow">{p.item.category}</p><h2>{p.item.title}</h2><p><span className="rv-tag">{p.scenario === "attention" ? "Index update pending" : "Indexed"}</span> <span className="rv-small">Authoritative card available</span></p></div><Button onClick={p.onEdit}>Edit card</Button></div>
{p.section === 0 && <><Section title="Reusable knowledge"><p className="rv-reading" lang="it">{p.item.detail}</p></Section><Section title="Rationale"><p lang="it">Il criterio rende esplicita l’unità di analisi e impedisce che le relazioni uno-a-molti alterino il risultato.</p></Section><Facts data={[["Origin", "Approved analytical session (sample)"],["Scope", `${p.workspace.schema}.patients`],["Workspace", p.workspace.name],["Retrieval projection", p.scenario === "attention" ? "Retry required" : "Current"]]}/><div className="rv-actions"><Button onClick={() => p.onSection(1)}>Dependencies & links</Button><Button onClick={() => setConfirmDelete(true)}>Delete card…</Button></div>{confirmDelete && <div className="rv-confirm"><strong>Delete “{p.item.title}” and its links?</strong><p>Other cards and Evidence are kept.</p><div className="rv-actions"><Button onClick={p.onDelete}>Confirm deletion</Button><Button onClick={()=>setConfirmDelete(false)}>Keep card</Button></div></div>}</>}
{p.section === 1 && <><Section title="Schema dependencies"><p>Structured dependencies connect this card to the catalog. A successful physical schema synchronization can remove cards whose dependencies no longer exist.</p><Facts data={[["Database", p.workspace.database],["Schema", p.workspace.schema],["Table", "patients"],["Column", "patient_id"]]}/></Section><Section title="Linked knowledge"><div className="rv-linked"><FileText size={18}/><div><strong>Evitare il doppio conteggio nei join</strong><p>Related explained error · sample link</p></div></div><p>Links belong to the Memory archive. They do not make this card an Evidence unit.</p></Section></>}
{p.section === 2 && <Section title="Retrieval index updates"><div className="rv-callout"><ShieldCheck size={20}/><div><strong>The authoritative card is preserved</strong><p>A failed projection does not delete saved content. Retry the retrieval-index update explicitly.</p></div></div>{p.scenario === "attention" ? <div className="rv-actions"><Button primary onClick={() => p.onOperation("Memory index retry")}>Retry index update</Button></div> : <p><Check size={16}/> No pending updates for this demonstration.</p>}</Section>}
</>;
}
function Pi(p: ContentProps) {
const [logs,setLogs] = useState(false);
const [platform,setPlatform] = useState("Linux");
return <>
<div className="rv-record-heading"><div><p className="rv-eyebrow">Installation scope</p><h2>{["Runtime & selected model","Available session models","Sanitized diagnostics","Host maintenance"][p.section]}</h2><p>These settings apply across workspaces. The browser does not maintain a second model configuration.</p></div><span className="rv-record-emblem"><KeyRound size={24}/></span></div>
{p.section === 0 && <><div className="rv-runtime"><div><p className="rv-eyebrow">Bundled Pi</p><strong>0.80.3 <small>sample version</small></strong></div><Status scenario={p.scenario}/></div><Section title="Selected interaction model"><Facts data={[["Provider", p.providerName],["Model", p.modelName],["Selection", "Global interaction preference"],["Credentials", p.scenario === "attention" ? "Missing" : "Present · values hidden"]]}/><p>This is the global selection, not a second Pi preference. Available models and installation defaults remain authored in the installation YAML.</p><div className="rv-actions"><Button primary disabled={p.scenario === "running"} onClick={() => p.onOperation("Selected model test")}>Test selected model</Button><Button onClick={() => p.onSection(3)}>Host instructions</Button></div></Section></>}
{p.section === 1 && <div className="rv-model-list">{[["GLM 5.3","zai / glm-5.3","Default"],["DeepSeek V4 Pro","deepseek / deepseek-v4-pro","Available"],["Local Qwen","local-qwen / qwen3.6-35b-a3b","Available"]].map(([name,id,status]) => <div key={id}><div><strong>{name}</strong><code>{id}</code></div><span className="rv-tag">{status}</span></div>)}<p className="rv-small">Illustrative catalog. Availability in production is read from the installation.</p></div>}
{p.section === 2 && <Section title="Request a diagnostic excerpt"><p>Fetch up to 200 sanitized lines only when requested. Credentials and declared secret values are removed.</p><Button onClick={() => setLogs(true)}>Show sanitized logs</Button>{logs && <pre className="rv-terminal" aria-label="Sanitized diagnostic example">{`[sample] Runtime ready\n[sample] Catalog default resolved\n[sample] Provider credential: [REDACTED]\n[sample] No active diagnostic errors`}</pre>}</Section>}
{p.section === 3 && <><nav className="rv-subtabs" aria-label="Host platform">{["Linux","macOS","Windows"].map(v=><button key={v} onClick={()=>setPlatform(v)} aria-current={platform===v?"page":undefined}>{v}</button>)}</nav><Section title={`Maintain Pi on ${platform}`}><p>Run these commands on the installation host. Review active sessions before a restart or version update.</p><ol className="rv-instructions"><li><strong>Edit the single configuration source</strong><CopyValue value={platform === "Windows" ? "deploy\\<installation-id>\\thothii-installation.yaml" : "deploy/<installation-id>/thothii-installation.yaml"} label="installation path"/></li><li><strong>Reload and verify</strong><CopyValue value="tht pi restart --yes --drain\ntht pi doctor\ntht pi test" label="Pi maintenance commands"/></li></ol><p className="rv-small">Commands are examples to review and copy; nothing is executed from this prototype.</p></Section></>}
</>;
}
@@ -0,0 +1,170 @@
# Full application context review V3
Throwaway prototype, 2026-09-12. **No V3 variant selected. Specification and ticket
publication have not started; production implementation remains paused.**
Question: where should the single workspace selector and independent global AI
model selector live when analytical work and all five administrative pages share
one application context inside Omics Portal?
## Run and compare
From `frontend/`, run `npm run prototype:application-context`.
- A: <http://127.0.0.1:5176/?thoth_route=analysis/core&variant=A>
- B: <http://127.0.0.1:5176/?thoth_route=analysis/core&variant=B>
- C: <http://127.0.0.1:5176/?thoth_route=analysis/core&variant=C>
Choose a workspace and a model to unlock the application. No model is selected
automatically. The bottom review bar switches variants without a reload. Left and
right arrow keys also switch variants outside interactive controls. Its expandable
review controls expose host widths, availability scenarios, a cross-workspace
session link, and the current relevant state. This bar is not production UI.
| Variant | Global selectors | Content strategy | Trade-off |
| --- | --- | --- | --- |
| A: Context bar | Visible horizontal band above core and navigation | Workbench, resource list beside detail when space permits | Clearest scope; consumes vertical space |
| B: Context rail | Vertical controls above the right navigation | Full-width list followed by focused record detail | More vertical room for work; a wider right rail |
| C: Context shelf | Compact context summary expands an inline selection panel | Operational sections and a collapsible record chooser | Compact during work; changing context takes one extra action |
The selection controls occur at one location only, never duplicated between core
and administration. Read-only context labels and operation provenance are not
additional selectors. On narrow containers the right rail moves above content;
application navigation becomes an inline disclosure, not an overlay. The portal
navigation is an illustrative host, not a deployed Django template.
## User-directed changes to the previous review
The user explicitly replaced V2's independent administrative workspace with one
global workspace for the entire ThothII interface. The global model selection is
independent of workspace and applies to both core and administrative LLM work.
Neither core nor administration can be activated without both selections.
V1 and V2 are retained without deleting their source or variants. Their executable
files remain in `administration-pages/` and `administration-review/`, with the
previous run commands and ports 5174/5175. V3 reuses V2's synthetic inventory and
base visual styles, but has its own shell and adapted administrative content.
The old independent-workspace rule is historical, not the current design mandate.
The previously started implementation and suspended specification remain untouched.
Do not mistake their earlier A selection or ticket breakdown for approval of V3.
## Proposed context rules
1. The current user's application has one selected workspace, shared by analytical
work, sessions, Database, Evidence, Memory and Workspace management. Changing
it updates the whole interface, clears the active session view and keeps the
chosen model. Unsaved drafts require explicit discard confirmation.
2. “Global” means within this user's ThothII experience, not overwriting every
operator's preference or authoring installation defaults. Cross-tab/device
synchronization and durable preference storage are decisions for the spec.
3. The model is the preference for the next LLM operation or session resume. Each
operation uses the selected workspace and model. **User correction on
2026-09-12: allow only one running core or administrative operation. Disable
both global selectors and prevent starting any other operation until the current
one completes, fails or is stopped.** Navigation does not release this lock.
There is no concurrent-workspace workflow or background-job collection.
4. Resuming a paused session retains its original workspace identity and revision,
switches the global workspace to that identity, and uses the current global
LLM. A cross-workspace session link shows a review before switching. The prior
model is historical provenance, not an override of the current preference.
5. Selecting context is necessary but not sufficient for core readiness. If
preprocessing is required, core remains blocked, while administration is
available to prepare the selected workspace.
6. The global selector can expose installation models with different capabilities.
A core-only model blocks metadata AI actions with a direct explanation and
a link back to the same selector. There is no silent fallback or separate
metadata model preference. Model/provider identities in this prototype are
synthetic examples, not verified current installation availability.
7. Embedding is not the selected interaction LLM. Embedding identity stays tied
to the installation catalog and index compatibility. System operations such
as schema synchronization may require no LLM at all, although the requested
application-wide entry gate still applies.
8. Pi diagnostics and shared workspace-repository updates retain installation
scope. Choosing a workspace does not falsely make them workspace-local.
They are still behind the global entry gate, as requested.
9. There is no implicit bypass when the catalog, credentials or workspace registry
is unavailable. The gate explains that the installation operator must repair
configuration. No usable models/no workspaces are demonstrable scenarios.
## Concrete production constraints found in the audit
- `backend/src/models/runtime-model-catalog.ts` filters session and metadata models
independently. Eligibility and adapters must be checked for each operation;
visual unification alone does not make every model valid for every use.
- `backend/src/routes/sessions.ts` currently reads the saved provider/model during
resume, validates that saved model and launches with it. Using the current
global model requires an explicit API/runtime/provenance change and tests.
The prototype shows the proposed behavior; it does not implement that change.
- The same resume route rejects archived or finalized sessions with 409 before
resolving their historical workspace. Retention can prune their old snapshots.
**Open question asked of the user:** does “archived” mean a paused session in
history, or should genuinely archived sessions become restorable? Until that
answer, the prototype resumes paused sessions only and shows true archives as
read-only. Do not silently enable archived/finalized resume in production.
- Context changes during a live operation are out of scope and intentionally
forbidden. Release the lock only after the operation has actually terminated,
not when a stop request is sent or a page is closed. Existing server-owned
operation state must restore the lock on reconnect; a visual disabled control
alone is not the production concurrency guarantee. The prototype is in-memory.
- Metadata generation currently has installation-wide sequential execution. The
proposed interaction rule is simpler and broader: only one core or administrative
operation at a time in this user's application, even within the same workspace.
- Backend authorization remains authoritative. The preview depicts an
administrator, not a proposal to give administration access to all users.
- Final assembly/integration must happen on the server, per the user's explicit
request. This change does not touch `/Users/mp/Chirone/omics_portal` or deploy.
## Suggested walkthrough
1. Open any variant without selections. Confirm core and administration are locked.
2. Choose only the workspace. They remain locked. Choose GLM 5.3 as well to enter.
3. Compare A/B/C using the bottom bar, then visit every administrative page using
the right navigation. Workspace management has no second workspace picker.
4. In Database, open Descriptions & sensitivity. Change the global model and
observe the read-only selected model. Choose Local Qwen to see the capability
block, then choose a common model to continue.
5. Start description generation. Both global selectors become disabled and an
operation banner explains why. Navigate to core or another administrative page:
no second operation can start. Use Demo: finish, Demo: stop or Demo: fail to
simulate a terminal state; both selectors become available again.
6. Edit a Memory card, try to change workspace and reject the discard dialog.
The workspace and unsaved draft remain unchanged. Save explicitly.
7. Open Sessions and review a paused session before resuming. Its original model
can differ from the current global model. Open an archived session read-only.
8. Use “Demo: Genetics session link” in review controls while PSD is selected.
Confirm the workspace switch on resume and unchanged global model. During the
simulated run both selectors are locked. Complete the operation before trying
a different workspace or model.
9. Choose Needs preprocessing: administrative repair remains possible, while new
analytical work and resume are blocked. Try unavailable/no-model/no-workspace.
10. Test 960 px and 390 px host widths inside a wide browser, not only a resized
viewport. In B the selectors move above navigation, in C the shelf expands
inline. Tables and management tabs can scroll locally.
## Verification and limitations
- Isolated strict TypeScript check passed for the prototype entry.
- Chromium: 84 combinations of 3 variants, 7 pages and viewport widths
1600/1280/960/390, without root or main-container horizontal overflow or runtime
errors. Additional 390 px host inside a 1600 px viewport checked.
- The initial 17 interaction checks covered the entry gate, independent selections,
metadata eligibility, drafts, archive and resume. Their former concurrent-job
scenarios were superseded by the user's explicit single-operation rule.
After restarting the preview, A/B/C all passed checks for core, administrative
and resume selector locks, prevention of a second operation across pages,
unlocking after completion/stop/failure, and independent selection once idle.
The 84 layout combinations were rerun and passed after this simplification.
- Desktop A/B/C and mobile renders visually inspected. Display headings retain
Fraunces; Manrope controls and warm neutral surfaces continue the V2 direction.
- Synthetic data and in-memory actions only. Reload resets model, drafts, record
edits and the simulated operation; URL retains page/workspace/variant. No real backend/provider
calls, persistence, credentials, clinical data, authorization or server jobs.
- Core is a representative question/session/artifact/review flow, not a complete
reimplementation of all eight phases, SSE chat, widgets or runtime behavior.
- No physical-device, Safari/WebKit, actual Omics/Bootstrap or server integration
acceptance is claimed. The development-only Vite configuration refuses builds.
Next: collect V3 layout preference and the archive clarification, then resume the
agreed to-spec / to-tickets workflow on **Gitea**, with ticket breakdown approval
before publishing. Do not start implementation agents while this review is open.
@@ -0,0 +1,229 @@
/* A daylight research workstation, with a warm reading surface inside the red
Omics host. Restrained color; Fraunces titles, Manrope controls. The container,
not the viewport, determines when the global controls and navigation reflow. */
.gc-host { min-height: 100dvh; margin-bottom: 120px; }
.gc-host-body { display: grid; grid-template-columns: minmax(0,1fr); }
.gc-portal { display: none; padding: 26px 14px; background: var(--rv-worktop); border-right: 1px solid var(--rv-line); }
.gc-portal > div { display: flex; gap: 12px; align-items: center; padding: 13px 12px; margin-block: 5px; font-size: .8rem; border: 1px solid transparent; border-radius: 6px; }
.gc-portal .current { border-color: oklch(84% .035 23); background: var(--rv-red-soft); color: var(--rv-red-dark); font-weight: 800; }
.gc-portal > small { display: block; color: var(--rv-muted); font-size: .65rem; margin: 32px 12px; }
.gc-portal > p { margin: 0 12px 18px; }
.gc-portal-open .gc-portal { display: flex; flex-wrap: wrap; padding: 10px; border-bottom: 1px solid var(--rv-line); }
.gc-portal-open .gc-portal > :is(p,small) { display: none; }
.gc-mount { min-width: 0; padding: 0 10px 24px; }
.gc-brandline { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px; padding: 17px 3px; font-size: .7rem; color: var(--rv-muted); }
.gc-brand { font: 600 1.4rem var(--rv-serif); letter-spacing: -.035em; color: var(--rv-ink); }
.gc-brand > span { color: var(--rv-red-dark); }
.gc-brandline > small { margin-left: auto; font: .6rem var(--rv-mono); }
.gc-app { container: application / inline-size; background: var(--rv-paper); border: 1px solid var(--rv-line); border-radius: 8px; }
.gc-body { display: grid; grid-template-columns: minmax(0,1fr); }
.gc-main { grid-row: 2; min-width: 0; container: page / inline-size; outline: none; }
.gc-global-top:empty { display: none; }
.gc-contextbar { display: flex; align-items: center; flex-wrap: wrap; gap: 18px 28px; padding: 20px; border-bottom: 1px solid var(--rv-line); background: var(--rv-worktop); border-radius: 8px 8px 0 0; }
.gc-context-label { display: flex; align-items: center; gap: 11px; }
.gc-context-label > svg { color: var(--rv-red-dark); }
.gc-context-label strong { display: block; font-size: .85rem; }
.gc-context-label small { display: block; margin-top: 5px; font-size: .65rem; color: var(--rv-muted); }
.gc-selectors { display: grid; grid-template-columns: minmax(0,1fr); gap: 18px; width: 100%; min-width: 0; }
.gc-selector { display: grid; gap: 7px; min-width: 0; }
.gc-selector > span { display: flex; align-items: center; gap: 7px; font-size: .72rem; font-weight: 800; }
.gc-selector > span > svg { color: var(--rv-muted); }
.gc-selector select { width: 100%; min-width: 0; border: 1px solid oklch(77% .012 20); border-radius: 5px; background: var(--rv-paper); color: var(--rv-ink); min-height: 44px; padding: 10px 27px 10px 10px; font-size: .83rem; font-weight: 650; text-overflow: ellipsis; }
.gc-selector select:disabled { background: var(--rv-sunken); color: var(--rv-muted); }
.gc-selector small { font-size: .63rem; color: var(--rv-muted); line-height: 1.6; }
.gc-context-state { display: inline-flex; align-items: center; gap: 5px; color: var(--rv-amber); font-size: .68rem; font-weight: 750; }
.gc-context-state.is-ready { color: var(--rv-green); }
.gc-shelf { border-bottom: 1px solid var(--rv-line); }
.gc-shelf-trigger { width: 100%; display: flex; align-items: center; gap: 13px; background: var(--rv-worktop); border: 0; padding: 15px 20px; text-align: left; border-radius: 8px 8px 0 0; }
.gc-shelf-trigger > span:nth-child(2) { flex: 1; min-width: 0; }
.gc-shelf-trigger small { display: block; font: .6rem var(--rv-mono); letter-spacing: .07em; color: var(--rv-muted); margin-bottom: 5px; }
.gc-shelf-trigger strong { display: block; font-size: .85rem; overflow-wrap: anywhere; }
.gc-separator { padding: 0 12px; color: var(--rv-muted); font-weight: 400; }
.gc-shelf-edit { font-size: .7rem; font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
.gc-shelf-body { padding: 22px; background: var(--rv-worktop); }
.gc-shelf-body > p { margin: 15px 0; font-size: .75rem; line-height: 1.7; color: var(--rv-muted); }
.gc-rail { grid-row: 1; min-width: 0; background: var(--rv-worktop); border-bottom: 1px solid var(--rv-line); }
.gc-rail-context { padding: 20px; border-bottom: 1px solid var(--rv-line); }
.gc-rail-context > p { margin-bottom: 20px; }
.gc-rail-context > .gc-context-state { margin-top: 17px; }
.gc-mobile-nav { display: flex; align-items: center; gap: 10px; border: 0; background: transparent; width: 100%; padding: 15px 18px; text-align: left; font-size: .8rem !important; font-weight: 700 !important; }
.gc-mobile-nav > svg:last-child { margin-left: auto; }
.gc-navigation { display: none; padding: 18px; }
.gc-navigation.open { display: block; }
.gc-navigation > p { margin: 0 9px 13px; }
.gc-navigation > p:not(:first-child) { margin-top: 29px; }
.gc-navigation nav { display: flex; flex-wrap: wrap; gap: 5px; }
.gc-navigation a { text-decoration: none; display: flex; align-items: center; gap: 10px; padding: 11px 10px; border: 1px solid transparent; border-radius: 5px; color: var(--rv-ink); font-size: .8rem; min-height: 44px; }
.gc-navigation a:hover:not([aria-disabled=true]) { background: var(--rv-sunken); }
.gc-navigation a[aria-current] { border-color: oklch(84% .035 23); background: var(--rv-red-soft); color: var(--rv-red-dark); font-weight: 750; }
.gc-navigation a > svg:last-child:not(:first-child) { margin-left: auto; }
.gc-navigation a[aria-disabled=true] { color: var(--rv-muted); cursor: not-allowed; opacity: .65; }
.gc-rail-caption { display: flex; gap: 10px; margin: 36px 8px 10px; color: var(--rv-muted); }
.gc-rail-caption p { font-size: .65rem; line-height: 1.8; }
.gc-rail-caption svg { color: var(--rv-red-dark); }
.gc-page-heading { padding: 30px 22px 24px; }
.gc-host .gc-page-heading h1 { font: 500 1.875rem/1.2 var(--rv-serif); letter-spacing: -.025em; margin: 10px 0 14px; text-wrap: balance; }
.gc-page-heading > p:last-child { font-size: .85rem; color: var(--rv-muted); line-height: 1.8; max-width: 68ch; }
.gc-page-heading .rv-eyebrow span { color: var(--rv-muted); font-weight: 450; }
.gc-page-footer { display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; border-top: 1px solid var(--rv-line); padding: 18px 22px; font-size: .63rem; color: var(--rv-muted); }
.gc-new-question { display: flex; justify-content: flex-end; padding: 20px 22px 0; }
.gc-operation-strip { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px; padding: 20px 24px; background: var(--rv-amber-bg); border-bottom: 1px solid var(--rv-line); }
.gc-operation-strip > div { min-width: 0; }
.gc-operation-strip strong { display: block; font-size: .85rem; margin: 8px 0; overflow-wrap: anywhere; }
.gc-operation-strip small, .gc-operation-strip #operation-lock { display: block; font-size: .75rem; line-height: 1.8; }
.gc-operation-strip .rv-actions { margin-top: 0; }
.gc-gate { padding: 44px 24px 60px; max-width: 680px; margin: auto; }
.gc-gate-icon { display: grid; place-items: center; width: 60px; height: 60px; border-radius: 12px; background: var(--rv-red-soft); color: var(--rv-red-dark); margin-bottom: 26px; }
.gc-gate > .rv-eyebrow { margin-bottom: 10px; }
.gc-host .gc-gate h1 { font: 500 2rem/1.2 var(--rv-serif); margin-bottom: 20px; letter-spacing: -.025em; }
.gc-gate > p:not(.rv-eyebrow) { font-size: .9rem; line-height: 1.8; color: var(--rv-muted); }
.gc-gate ol { list-style: none; padding: 0; margin: 26px 0; }
.gc-gate li { display: flex; gap: 16px; align-items: center; padding: 17px 0; border-bottom: 1px solid var(--rv-line); }
.gc-gate li > span { display: grid; place-items: center; width: 32px; height: 32px; border: 1px solid var(--rv-line); border-radius: 50%; font: .75rem var(--rv-mono); }
.gc-gate li.done > span { background: var(--rv-green-bg); color: var(--rv-green); border-color: oklch(80% .04 156); }
.gc-gate li strong { display: block; font-size: .85rem; }
.gc-gate li small { display: block; font-size: .75rem; margin-top: 6px; color: var(--rv-muted); }
.gc-host .gc-gate p.gc-gate-foot { font-size: .75rem; }
.gc-core-start { padding: 0 22px 34px; }
.gc-question { border: 1px solid oklch(80% .025 23); border-radius: 8px; background: var(--rv-paper); }
.gc-question-heading { display: flex; gap: 15px; padding: 24px 20px 22px; }
.gc-symbol { color: var(--rv-red-dark); }
.gc-question-heading h2 { font-size: 1.05rem; }
.gc-question-heading p { font-size: .73rem; color: var(--rv-muted); margin-top: 8px; }
.gc-question form { padding: 0 20px 18px; }
.gc-question textarea { display: block; width: 100%; min-width: 0; margin: 12px 0 16px; padding: 14px; resize: vertical; border: 1px solid var(--rv-line); border-radius: 5px; font-size: 1rem; line-height: 1.8; background: var(--rv-worktop); color: var(--rv-ink); }
.gc-question textarea::placeholder { color: var(--rv-muted); }
.gc-composer-footer { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 15px; }
.gc-composer-footer > span { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; font-size: .75rem; font-weight: 700; }
.gc-composer-footer small { font-weight: 450; font-size: .65rem; color: var(--rv-muted); }
.gc-recent { margin-top: 36px; }
.gc-section-heading { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 15px; }
.gc-section-heading h2 { font-size: 1.05rem; }
.gc-recent-session { display: flex; gap: 15px; align-items: center; padding: 21px 0; border: 0; border-block: 1px solid var(--rv-line); width: 100%; background: transparent; text-align: left; }
.gc-recent-session:hover { background: var(--rv-worktop); }
.gc-session-number { font: 1.25rem var(--rv-serif); color: var(--rv-red-dark); }
.gc-recent-session strong { display: block; font-size: .9rem; line-height: 1.6; }
.gc-recent-session small { display: block; margin-top: 7px; color: var(--rv-muted); font-size: .7rem; }
.gc-recent-session > svg { margin-left: auto; }
.gc-method { padding-top: 36px; }
.gc-method > div { display: flex; align-items: center; flex-wrap: wrap; gap: 15px; padding-block: 18px; }
.gc-method > div > span { font-size: .75rem; }
.gc-method strong { font: .7rem var(--rv-mono); color: var(--rv-muted); margin-right: 8px; }
.gc-method > p:last-child { font-size: .75rem; line-height: 1.8; color: var(--rv-muted); }
.gc-session-list { padding: 0 24px 40px; }
.gc-session-list article { display: flex; flex-wrap: wrap; align-items: center; gap: 20px; justify-content: space-between; padding: 26px 0; border-top: 1px solid var(--rv-line); }
.gc-session-list h2 { margin: 12px 0 8px; font-size: 1.1rem; }
.gc-session-list p, .gc-session-list small { font-size: .75rem; line-height: 1.8; color: var(--rv-muted); }
.gc-resume { margin: 24px; padding: 24px; border: 1px solid oklch(77% .04 23); border-radius: 7px; background: var(--rv-red-soft); }
.gc-resume h2 { margin-top: 10px; font: 500 1.4rem/1.4 var(--rv-serif); }
.gc-resume dl { display: grid; gap: 20px; margin: 23px 0; }
.gc-resume dt { font-size: .72rem; color: var(--rv-muted); margin-bottom: 6px; }
.gc-resume dd { margin: 0; font-weight: 700; font-size: .85rem; }
.gc-resume dd small { display: block; margin-top: 6px; font-weight: 450; font-size: .7rem; }
.gc-warning { padding: 18px; margin: 18px 22px; background: var(--rv-amber-bg); border: 1px solid oklch(80% .065 83); border-radius: 5px; font-size: .8rem; line-height: 1.8; color: var(--rv-amber); }
.gc-warning p { margin-block: 8px 12px; }
.gc-resume .gc-warning, .gc-gate .gc-warning { margin-inline: 0; }
.gc-session-context { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; background: var(--rv-worktop); padding: 14px 22px; border-block: 1px solid var(--rv-line); font-size: .7rem; }
.gc-session-context > svg { color: var(--rv-red-dark); }
.gc-workflow { padding: 22px; }
.gc-workflow ol { list-style: none; display: flex; flex-wrap: wrap; padding: 0; gap: 10px; margin: 0 0 8px; }
.gc-workflow li { display: flex; gap: 8px; padding: 9px 11px; border: 1px solid var(--rv-line); border-radius: 4px; font-size: .73rem; }
.gc-workflow li.current { background: var(--rv-red-soft); color: var(--rv-red-dark); border-color: oklch(82% .04 23); }
.gc-workflow li span { font-family: var(--rv-mono); }
.gc-workflow > small { font-size: .63rem; color: var(--rv-muted); }
.gc-artifact-layout { display: grid; grid-template-columns: minmax(0,1fr); border-top: 1px solid var(--rv-line); }
.gc-artifact { min-width: 0; }
.gc-artifact nav { display: flex; gap: 24px; border-bottom: 1px solid var(--rv-line); padding: 0 24px; }
.gc-artifact nav button { padding: 16px 0; border: 0; border-bottom: 2px solid transparent; background: transparent; font-size: .8rem; }
.gc-artifact nav button[aria-current] { color: var(--rv-red-dark); border-bottom-color: var(--rv-red); }
.gc-artifact > div { padding: 25px; }
.gc-artifact h2 { font: 500 1.5rem/1.3 var(--rv-serif); margin: 10px 0 20px; }
.gc-artifact p[lang] { font-size: 1rem; line-height: 1.9; }
.gc-artifact pre { padding: 16px; background: var(--rv-worktop); white-space: pre-wrap; overflow-wrap: anywhere; }
.gc-evidence-line { display: flex; gap: 10px; font-size: .75rem; color: var(--rv-muted); padding: 19px 0; border-block: 1px solid var(--rv-line); margin-block: 24px; }
.gc-review { padding: 24px; border-top: 1px solid var(--rv-line); background: var(--rv-worktop); }
.gc-review h2 { margin: 12px 0; font-size: 1.1rem; }
.gc-review > p:not(.rv-eyebrow) { font-size: .85rem; line-height: 1.8; color: var(--rv-muted); margin-block: 15px; }
.gc-admin-context { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 20px; padding: 14px 22px; border-block: 1px solid var(--rv-line); font-size: .72rem; color: var(--rv-muted); }
.gc-admin-context > .rv-button { margin-left: auto; }
.gc-footnote { padding: 15px 22px 0; font-size: .72rem; line-height: 1.8; color: var(--rv-muted); }
.gc-management-split { display: grid; grid-template-columns: minmax(0,1fr); }
.gc-management-split > div { min-width: 0; }
.gc-management-split > .rv-index { background: var(--rv-worktop); border-bottom: 1px solid var(--rv-line); }
.gc-content-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.gc-content-fieldset:disabled { opacity: .65; }
.gc-operation-intro { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 17px; background: var(--rv-worktop); border-block: 1px solid var(--rv-line); font-size: .75rem; }
.gc-operation-intro strong { color: var(--rv-green); margin-left: auto; }
.gc-jobs { border-top: 1px solid var(--rv-line); background: var(--rv-worktop); padding: 16px 20px; }
.gc-jobs summary { display: flex; flex-wrap: wrap; gap: 10px; justify-content: space-between; cursor: pointer; font-size: .75rem; font-weight: 750; }
.gc-jobs summary::before { content: "+"; }
.gc-jobs[open] summary::before { content: "−"; }
.gc-jobs summary > span:last-child { margin-left: auto; font-size: .65rem; font-weight: 450; color: var(--rv-muted); }
.gc-job { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; padding: 20px 0; border-bottom: 1px solid var(--rv-line); }
.gc-job > div { flex: 1 1 240px; min-width: 0; }
.gc-job strong { display: block; font-size: .8rem; overflow-wrap: anywhere; }
.gc-job small { display: block; margin-top: 6px; font-size: .7rem; color: var(--rv-muted); }
.gc-lab { bottom: max(10px,env(safe-area-inset-bottom)); }
.gc-lab > details[open] { max-height: 48dvh; overflow-y: auto; }
.gc-lab .rv-lab-options { max-width: 720px; }
.gc-lab .rv-lab-options > p { flex-basis: 100%; font-size: .68rem; }
@container portal (min-width: 44rem) {
.gc-host-body { grid-template-columns: 62px minmax(0,1fr); }
.gc-portal, .gc-portal-open .gc-portal { display: block; padding: 24px 7px; border-bottom: 0; }
.gc-portal :is(span,p,small) { display: none; }
.gc-portal > div { padding-inline: 12px; }
.gc-mount { padding-inline: 20px; }
}
@container portal (min-width: 73rem) {
.gc-host-body { grid-template-columns: 178px minmax(0,1fr); }
.gc-portal, .gc-portal-open .gc-portal { padding-inline: 12px; }
.gc-portal :is(span,p,small) { display: block; }
.gc-portal-open .gc-portal > :is(p,small) { display: block; }
}
@container application (min-width: 40rem) {
.gc-contextbar > .gc-selectors { grid-template-columns: repeat(2,minmax(0,1fr)); flex: 1 1 480px; }
.gc-shelf-body > .gc-selectors { grid-template-columns: repeat(2,minmax(0,1fr)); max-width: 850px; }
.gc-contextbar { padding: 20px 24px; }
}
@container application (min-width: 55rem) {
.gc-global-top { position: sticky; top: 0; z-index: 5; background: var(--rv-paper); }
.gc-rail-context { position: sticky; top: 0; z-index: 5; background: var(--rv-worktop); }
.gc-body { grid-template-columns: minmax(0,1fr) 216px; }
.gc-variant-B .gc-body { grid-template-columns: minmax(0,1fr) 276px; }
.gc-main { grid-column: 1; grid-row: 1; }
.gc-rail { grid-column: 2; grid-row: 1; border-bottom: 0; border-left: 1px solid var(--rv-line); }
.gc-navigation { display: block; padding: 24px 14px; }
.gc-navigation nav { display: grid; }
.gc-mobile-nav { display: none; }
.gc-context-label { max-width: 190px; }
.gc-variant-B .gc-rail-context { padding-inline: 22px; }
}
@container page (min-width: 43rem) {
.gc-page-heading { padding: 36px 32px 28px; }
.gc-core-start { padding: 0 32px 38px; }
.gc-question-heading { padding: 26px 26px 24px; }
.gc-question form { padding-inline: 26px; }
.gc-management-split.has-list { grid-template-columns: 216px minmax(0,1fr); }
.gc-management-split > .rv-index { border-right: 1px solid var(--rv-line); border-bottom: 0; }
.gc-artifact-layout { grid-template-columns: minmax(0,1fr) 250px; }
.gc-review { border-top: 0; border-left: 1px solid var(--rv-line); }
.gc-resume dl { grid-template-columns: repeat(2,minmax(0,1fr)); }
}
@container application (max-width: 39.999rem) {
.gc-brandline > small { display: none; }
.gc-contextbar > .gc-context-state { display: none; }
.gc-selectors select { font-size: 1rem; }
.gc-shelf-edit { display: none; }
.gc-shelf-trigger strong { font-size: .8rem; }
.gc-separator { padding-inline: 7px; }
.gc-rail-context > p { margin-bottom: 12px; }
.gc-management-split > .rv-index { max-height: 290px; overflow-y: auto; }
.gc-host .gc-page-heading h1 { font-size: 1.65rem; }
.gc-page-heading { padding: 26px 18px 22px; }
.gc-core-start { padding-inline: 16px; }
.gc-question-heading { flex-direction: column; padding-inline: 16px; }
.gc-question form { padding-inline: 16px; }
.gc-resume { margin: 16px; padding: 18px; }
}
@media (pointer: coarse) { .gc-host button, .gc-host select, .gc-host summary, .gc-lab button { min-height: 44px; } }
@media (prefers-reduced-motion: reduce) { .gc-host *, .gc-lab * { transition: none !important; animation: none !important; scroll-behavior: auto !important; } }
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>ThothII · Global context design review</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,500;9..144,600&display=swap" rel="stylesheet" />
</head>
<body><div id="root"></div><script type="module" src="/main.tsx"></script></body>
</html>
@@ -0,0 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "../administration-review/review.css";
import "./context.css";
createRoot(document.getElementById("root")!).render(<StrictMode><App /></StrictMode>);
@@ -0,0 +1,25 @@
import { modules, workspaces, type Surface, type Variant } from "../administration-review/model";
export { modules, workspaces };
export type Page = "core" | "sessions" | Surface;
export const variants = { A: "Context bar", B: "Context rail", C: "Context shelf" };
// Synthetic capability fixtures, not a claim about the installed model catalog.
export const models = [
{ id: "zai/glm-5.3", name: "GLM 5.3", provider: "Z.ai", metadata: true },
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", provider: "DeepSeek", metadata: true },
{ id: "local/qwen", name: "Local Qwen", provider: "Local", metadata: false },
];
export type Session = { id: string; title: string; workspace: string; revision: string; status: "paused" | "archived"; lastModel: string; phase: number };
export const sessions: Session[] = [
{ id: "psd-followup", title: "Pazienti con follow-up nel 2025", workspace: "psd-clinical", revision: "a71c9f2", status: "paused", lastModel: "deepseek/deepseek-v4-pro", phase: 3 },
{ id: "psd-archive", title: "Visite ripetute per paziente", workspace: "psd-clinical", revision: "7bc9e10", status: "archived", lastModel: "zai/glm-5.3", phase: 8 },
{ id: "genetics-cohort", title: "Campioni validati per coorte", workspace: "genetics", revision: "8de4a21", status: "paused", lastModel: "zai/glm-5.3", phase: 3 },
];
export type Route = { page: Page; workspace: string; variant: Variant };
export function readLocation(): Route {
const q = new URLSearchParams(location.search);
const page = q.get("thoth_route")?.split("/").at(-1) ?? "core";
const variant = q.get("variant") ?? "A";
const workspace = q.get("thoth_workspace") ?? "";
return { page: page === "core" || page === "sessions" || page in modules ? page as Page : "core", workspace: workspaces.some(w => w.id === workspace) ? workspace : "", variant: variant in variants ? variant as Variant : "A" };
}
export type Job = { id: number; label: string; workspace: string; model: string; state: "running" | "complete" | "failed" | "cancelled" };
@@ -0,0 +1,5 @@
import type { ReactNode } from "react";
export function Button({ children, primary, onClick, disabled, className = "" }: { children: ReactNode; primary?: boolean; onClick?: () => void; disabled?: boolean; className?: string }) {
return <button type="button" className={`rv-button ${primary ? "rv-primary" : ""} ${className}`} onClick={onClick} disabled={disabled}>{children}</button>;
}
@@ -0,0 +1,12 @@
import path from "node:path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig(({ command }) => {
if (command !== "serve") throw new Error("Application-context prototype is development-only.");
return {
root: path.resolve(__dirname, "prototypes/application-context"),
plugins: [react()],
server: { host: "127.0.0.1", port: 5176, strictPort: true },
};
});