From fc974009013d2c15669b105ef9535a8a9cfaf3a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 22:04:02 -0700 Subject: [PATCH 01/12] feat: add Ask Agent evidence workspace component --- frontend/src/components/AskAgentWorkspace.tsx | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 frontend/src/components/AskAgentWorkspace.tsx diff --git a/frontend/src/components/AskAgentWorkspace.tsx b/frontend/src/components/AskAgentWorkspace.tsx new file mode 100644 index 000000000..444fdc560 --- /dev/null +++ b/frontend/src/components/AskAgentWorkspace.tsx @@ -0,0 +1,320 @@ +import { + useEffect, + useRef, + useState, + type FormEvent, + type KeyboardEvent, +} from "react"; +import { + askAgent, + BackendError, + type AskAgentResponse, +} from "../api"; +import { t } from "../i18n"; +import "./AskAgentWorkspace.css"; + +export const GLOBAL_ASK_SESSION_STORAGE_KEY = "lineageweave.globalAskSessionId"; + +const CHAT_EVIDENCE_KIND_LABELS: Record = { + source_field: "Source field hint", + semantic_project: "Semantic project", + semantic_role: "Semantic role", + semantic_keyman: "Semantic Keyman", +}; + +function chatEvidenceKindLabel(kind: string): string { + return t(CHAT_EVIDENCE_KIND_LABELS[kind] ?? "Evidence"); +} + +function safeSessionStorage(): Storage | null { + if (typeof window === "undefined") return null; + try { + return window.sessionStorage; + } catch { + return null; + } +} + +function readSavedSession(storage: Storage | null): string | undefined { + try { + return storage?.getItem(GLOBAL_ASK_SESSION_STORAGE_KEY) || undefined; + } catch { + return undefined; + } +} + +function saveSession(storage: Storage | null, sessionId: string): void { + try { + storage?.setItem(GLOBAL_ASK_SESSION_STORAGE_KEY, sessionId); + } catch { + // A blocked browser storage policy must not block an evidence-grounded answer. + } +} + +function clearSession(storage: Storage | null): void { + try { + storage?.removeItem(GLOBAL_ASK_SESSION_STORAGE_KEY); + } catch { + // The next request can still proceed without the stale session identifier. + } +} + +function askAgentErrorMessage(error: unknown): string { + if (error instanceof BackendError && error.status === 503) { + return `${t("Ask Agent")} ${t("is temporarily unavailable.")} ${t("Saved evidence is still available.")}`; + } + return String(error); +} + +export type AskAgentRequest = ( + accessToken: string, + question: string, + sessionId?: string, +) => Promise; + +export interface AskAgentWorkspaceViewProps { + question: string; + answer: AskAgentResponse | null; + error: string | null; + asking: boolean; + onQuestionChange: (question: string) => void; + onSubmit: () => void; + onOpenPost: (postId: string) => void; +} + +export function AskAgentWorkspaceView({ + question, + answer, + error, + asking, + onQuestionChange, + onSubmit, + onOpenPost, +}: AskAgentWorkspaceViewProps) { + const answerRef = useRef(null); + + useEffect(() => { + if (answer && !asking) answerRef.current?.focus(); + }, [answer, asking]); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!asking && question.trim()) onSubmit(); + } + + function handleQuestionKeyDown(event: KeyboardEvent) { + if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; + event.preventDefault(); + event.currentTarget.form?.requestSubmit(); + } + + return ( +
+
+
+

{t("Evidence-grounded questions")}

+

{t("Ask Agent")}

+

+ {t("Questions use authorized posts and their evidence.")} +

+
+

{t("Authorized posts in this board.")}

+
+ +
+
+