import { useI18n, translate } from "../i18n";
import { useQuery } from "@tanstack/react-query";
import { X } from "lucide-react";
import { getSessionDocuments } from "../api/sessions";
import type { SessionDocument, SessionSummary } from "../api/types";
import { Button } from "../components/ui/button";
import { ErrorBoundary } from "../components/ErrorBoundary";
import { SqlViewer } from "../viewers/SqlViewer";
import { SchemaLinkingViewer } from "../viewers/SchemaLinkingViewer";
import { MarkdownView } from "../viewers/MarkdownView";
interface Props {
session: SessionSummary;
onClose: () => void;
onResume: (id: string) => void;
desktopSplit?: boolean;
hidden?: boolean;
}
function statusLabel(s: SessionSummary): string {
if (s.archived) return translate("Archived");
if (s.status === "finalized") return translate("Completed");
return translate("In progress · {status}", { status: translate(s.status) });
}
type MemoryItem = {
status: "approved" | "declined";
subject: string;
detail: string;
rationale: string;
};
const HIDDEN_DECISION_TYPES = new Set([
"concept_clarified",
"memory_promoted",
"memory_promotion_declined",
"memory_rejected",
"phase_approved",
"phase_auto_approved",
"table_approved",
"table_promoted",
"column_promoted",
"cte_approved",
"sql_approved",
"datamart_requested",
"datamart_declined",
]);
function DocBody({ doc }: { doc: SessionDocument }) {
const { t: translate } = useI18n();
if (doc.format === "sql") return ;
if (doc.format === "markdown") return ;
if (doc.format === "schema-linking") {
try {
const linking = JSON.parse(doc.content);
return ;
} catch {
return
{doc.content};
}
}
if (doc.format === "memories") {
const raw = JSON.parse(doc.content) as unknown;
const items = Array.isArray(raw)
? raw.filter((item): item is MemoryItem => {
if (!item || typeof item !== "object") return false;
const candidate = item as Partial;
return (
(candidate.status === "approved" || candidate.status === "declined")
&& typeof candidate.subject === "string"
&& typeof candidate.detail === "string"
&& typeof candidate.rationale === "string"
);
})
: [];
if (items.length === 0) {
return {translate("No memories recorded.")}
;
}
return (
{items.map((item, index) => (
-
{item.status === "approved" ? translate("Approved") : translate("Declined")}
{item.detail && (
)}
{item.rationale && (
)}
))}
);
}
if (doc.format === "decisions") {
const items = doc.content
.split("\n")
.filter(Boolean)
.map((line) => {
try { return JSON.parse(line) as { type?: string; subject?: string; detail?: string }; }
catch { return null; }
})
.filter((d): d is { type?: string; subject?: string; detail?: string } => (
d !== null && !HIDDEN_DECISION_TYPES.has(d.type ?? "")
));
if (items.length === 0) return {translate("No decisions recorded.")}
;
return (
{items.map((d, i) => (
-
{translate((d.type ?? "decision").replace(/_/g, " "))}
{d.subject && (
)}
{d.detail && (
)}
))}
);
}
return ;
}
/** Semantic tint for a decision-type chip: approvals/promotions read as progress
* (green), exclusions as neutral, everything else carries the brand accent. */
function decisionChipClass(type: string): string {
const t = type.toLowerCase();
if (t.includes("approved") || t.includes("promoted"))
return "bg-[oklch(var(--success)/0.15)] text-[oklch(0.47_0.12_165)]";
if (t.includes("excluded") || t.includes("rejected") || t.includes("reopened"))
return "bg-muted text-muted-foreground";
return "bg-[oklch(var(--primary)/0.1)] text-primary";
}
export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit = false, hidden = false }: Props) {
const { t: translate } = useI18n();
const { data: docs = [], isLoading, isError, refetch } = useQuery({
queryKey: ["session-documents", session.id],
queryFn: () => getSessionDocuments(session.id),
});
const resumable = session.status !== "finalized" && !session.archived;
return (
);
}