Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useI18n, translate } from "../i18n";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import { getSessionDocuments } from "../api/sessions";
|
||||
@@ -17,9 +18,9 @@ interface Props {
|
||||
}
|
||||
|
||||
function statusLabel(s: SessionSummary): string {
|
||||
if (s.archived) return "Archived";
|
||||
if (s.status === "finalized") return "Completed";
|
||||
return `In progress · ${s.status}`;
|
||||
if (s.archived) return translate("Archived");
|
||||
if (s.status === "finalized") return translate("Completed");
|
||||
return translate("In progress · {status}", { status: translate(s.status) });
|
||||
}
|
||||
|
||||
type MemoryItem = {
|
||||
@@ -46,6 +47,7 @@ const HIDDEN_DECISION_TYPES = new Set([
|
||||
]);
|
||||
|
||||
function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
const { t: translate } = useI18n();
|
||||
if (doc.format === "sql") return <SqlViewer blocks={[{ name: doc.title, sql: doc.content }]} />;
|
||||
if (doc.format === "markdown") return <MarkdownView source={doc.content} />;
|
||||
if (doc.format === "schema-linking") {
|
||||
@@ -71,10 +73,10 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
})
|
||||
: [];
|
||||
if (items.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No memories recorded.</p>;
|
||||
return <p className="text-sm text-muted-foreground">{translate("No memories recorded.")}</p>;
|
||||
}
|
||||
return (
|
||||
<ul aria-label="Memories" className="flex flex-col divide-y divide-border/50">
|
||||
<ul aria-label={translate("Memories")} className="flex flex-col divide-y divide-border/50">
|
||||
{items.map((item, index) => (
|
||||
<li key={`${item.status}:${item.subject}:${index}`} className="py-4 first:pt-0">
|
||||
<span className={[
|
||||
@@ -83,7 +85,7 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
? "bg-[oklch(var(--success)/0.15)] text-[oklch(0.47_0.12_165)]"
|
||||
: "bg-muted text-muted-foreground",
|
||||
].join(" ")}>
|
||||
{item.status === "approved" ? "Approved" : "Declined"}
|
||||
{item.status === "approved" ? translate("Approved") : translate("Declined")}
|
||||
</span>
|
||||
<div className="mt-2 font-medium text-foreground">
|
||||
<MarkdownView source={item.subject} />
|
||||
@@ -114,13 +116,13 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
.filter((d): d is { type?: string; subject?: string; detail?: string } => (
|
||||
d !== null && !HIDDEN_DECISION_TYPES.has(d.type ?? "")
|
||||
));
|
||||
if (items.length === 0) return <p className="text-sm text-muted-foreground">No decisions recorded.</p>;
|
||||
if (items.length === 0) return <p className="text-sm text-muted-foreground">{translate("No decisions recorded.")}</p>;
|
||||
return (
|
||||
<ul className="flex flex-col divide-y divide-border/50">
|
||||
{items.map((d, i) => (
|
||||
<li key={i} className="py-3 first:pt-0">
|
||||
<span className={`inline-block rounded-md px-2 py-0.5 text-[0.62rem] font-semibold uppercase tracking-wide ${decisionChipClass(d.type ?? "")}`}>
|
||||
{(d.type ?? "decision").replace(/_/g, " ")}
|
||||
{translate((d.type ?? "decision").replace(/_/g, " "))}
|
||||
</span>
|
||||
{d.subject && (
|
||||
<div className="mt-1.5 text-sm font-medium text-foreground">
|
||||
@@ -152,7 +154,8 @@ function decisionChipClass(type: string): string {
|
||||
}
|
||||
|
||||
export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit = false, hidden = false }: Props) {
|
||||
const { data: docs = [], isLoading } = useQuery<SessionDocument[]>({
|
||||
const { t: translate } = useI18n();
|
||||
const { data: docs = [], isLoading, isError, refetch } = useQuery<SessionDocument[]>({
|
||||
queryKey: ["session-documents", session.id],
|
||||
queryFn: () => getSessionDocuments(session.id),
|
||||
});
|
||||
@@ -160,7 +163,7 @@ export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label="Session summary"
|
||||
aria-label={translate("Session summary")}
|
||||
aria-hidden={hidden || undefined}
|
||||
hidden={hidden}
|
||||
className={[
|
||||
@@ -179,16 +182,21 @@ export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{resumable && (
|
||||
<Button size="sm" variant="outline" onClick={() => onResume(session.id)}>Resume</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => onResume(session.id)}>{translate("Resume")}</Button>
|
||||
)}
|
||||
<Button size="icon-sm" variant="ghost" aria-label="Close panel" onClick={onClose}>
|
||||
<Button size="icon-sm" variant="ghost" aria-label={translate("Close panel")} onClick={onClose}>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-3 py-5">
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-muted-foreground">Loading…</p>
|
||||
<p className="text-xs text-muted-foreground">{translate("Loading…")}</p>
|
||||
) : isError ? (
|
||||
<div role="alert" className="space-y-2 text-sm text-destructive">
|
||||
<p>{translate("Session documents could not be loaded.")}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => void refetch()}>{translate("Retry")}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-7">
|
||||
{docs.map((doc, idx) => (
|
||||
@@ -201,7 +209,7 @@ export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="thot-label">
|
||||
{doc.title}
|
||||
{translate(doc.title)}
|
||||
</h3>
|
||||
{doc.phase !== "—" && (
|
||||
<span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[0.6rem] font-medium text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user