prototype: explore global context with one operation at a time
This commit is contained in:
@@ -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 },
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user