Spaces:
Sleeping
Sleeping
harryagasi
[NOTICKET] feat(knowledge,analysis): add deletion confirmation, duplicate-name guard, and unusable-analysis lockout
3baad7e | import { useEffect, useState } from "react"; | |
| import { FileText, Loader2, Maximize2, Minimize2, PanelRightClose } from "lucide-react"; | |
| import { | |
| AgenticApiError, | |
| generateReport, | |
| getReportVersion, | |
| listReportVersions, | |
| type ReportDetail, | |
| type ReportSummary, | |
| } from "@/services/agenticApi"; | |
| import type { Analysis } from "@/services/orchestrationApi"; | |
| import { MarkdownContent } from "./MarkdownContent"; | |
| import { ReportVersionSelector } from "./ReportVersionSelector"; | |
| import { EmptyState, ErrorState } from "./AnalysisStates"; | |
| import { formatDateTime } from "./utils"; | |
| interface ReportSidebarProps { | |
| analysis: Analysis | null; | |
| userId?: string; | |
| onCollapse?: () => void; | |
| fullscreen?: boolean; | |
| onToggleFullscreen?: () => void; | |
| disabled?: boolean; | |
| } | |
| export function ReportSidebar({ analysis, userId, onCollapse, fullscreen = false, onToggleFullscreen, disabled = false }: ReportSidebarProps) { | |
| const [versions, setVersions] = useState<ReportSummary[]>([]); | |
| const [selectedVersion, setSelectedVersion] = useState<number | undefined>(); | |
| const [detail, setDetail] = useState<ReportDetail | null>(null); | |
| const [loadingVersions, setLoadingVersions] = useState(false); | |
| const [loadingDetail, setLoadingDetail] = useState(false); | |
| const [generating, setGenerating] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [precondition, setPrecondition] = useState<string | null>(null); | |
| const loadVersions = async (analysisId: string, options: { silent?: boolean } = {}) => { | |
| setLoadingVersions(true); | |
| setError(null); | |
| try { | |
| const list = await listReportVersions(analysisId); | |
| setVersions(list); | |
| const latest = [...list].sort((a, b) => b.version - a.version)[0]; | |
| setSelectedVersion(latest?.version); | |
| } catch (err) { | |
| setVersions([]); | |
| setSelectedVersion(undefined); | |
| const isMissingReportCollection = err instanceof AgenticApiError && err.status === 404; | |
| if (!options.silent && !isMissingReportCollection) { | |
| setError(err instanceof Error ? err.message : "Failed to load report versions"); | |
| } | |
| } finally { | |
| setLoadingVersions(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| setDetail(null); | |
| setPrecondition(null); | |
| setVersions([]); | |
| setSelectedVersion(undefined); | |
| if (analysis?.id) loadVersions(analysis.id, { silent: true }); | |
| }, [analysis?.id]); | |
| useEffect(() => { | |
| const loadDetail = async () => { | |
| if (!analysis?.id || selectedVersion == null) return; | |
| setLoadingDetail(true); | |
| setError(null); | |
| try { | |
| setDetail(await getReportVersion(analysis.id, selectedVersion)); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : "Failed to load report"); | |
| } finally { | |
| setLoadingDetail(false); | |
| } | |
| }; | |
| loadDetail(); | |
| }, [selectedVersion]); | |
| const handleGenerate = async () => { | |
| if (!analysis?.id || !userId) return; | |
| setGenerating(true); | |
| setError(null); | |
| setPrecondition(null); | |
| try { | |
| const report = await generateReport(analysis.id, userId); | |
| setDetail(report); | |
| await loadVersions(analysis.id, { silent: true }); | |
| setSelectedVersion(report.version); | |
| } catch (err) { | |
| if (err instanceof AgenticApiError && err.status === 409) { | |
| setPrecondition(err.message); | |
| } else { | |
| setError(err instanceof Error ? err.message : "Failed to generate report"); | |
| } | |
| } finally { | |
| setGenerating(false); | |
| } | |
| }; | |
| return ( | |
| <section className="space-y-4"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <div className="flex min-w-0 items-center gap-2"> | |
| <FileText className="h-4 w-4 flex-shrink-0 text-slate-600" /> | |
| <div className="min-w-0"> | |
| <h3 className="text-sm font-semibold text-slate-900">Report</h3> | |
| <p className="truncate text-xs text-slate-500">Generate and preview report versions.</p> | |
| </div> | |
| </div> | |
| <div className="flex flex-shrink-0 items-center gap-1.5"> | |
| {onToggleFullscreen && ( | |
| <button | |
| type="button" | |
| title={fullscreen ? "Exit fullscreen" : "Expand report to fullscreen"} | |
| aria-label={fullscreen ? "Exit fullscreen" : "Expand report to fullscreen"} | |
| onClick={onToggleFullscreen} | |
| className="rounded-md p-2 text-slate-500 hover:bg-slate-100 hover:text-slate-900" | |
| > | |
| {fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />} | |
| </button> | |
| )} | |
| {onCollapse && !fullscreen && ( | |
| <button | |
| type="button" | |
| title="Collapse report sidebar" | |
| aria-label="Collapse report sidebar" | |
| onClick={onCollapse} | |
| className="rounded-md p-2 text-slate-500 hover:bg-slate-100 hover:text-slate-900" | |
| > | |
| <PanelRightClose className="h-4 w-4" /> | |
| </button> | |
| )} | |
| <button | |
| type="button" | |
| aria-label="Generate report" | |
| title="Generate a new report version with the Python report skill" | |
| onClick={handleGenerate} | |
| disabled={!analysis || !userId || generating || disabled} | |
| className="inline-flex items-center gap-2 rounded-md bg-slate-900 px-3 py-2 text-xs font-medium text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50" | |
| > | |
| {generating && <Loader2 className="h-3.5 w-3.5 animate-spin" />} | |
| Generate | |
| </button> | |
| </div> | |
| </div> | |
| {!analysis ? ( | |
| <EmptyState title="Select an analysis" description="Reports are generated per analysis." /> | |
| ) : ( | |
| <> | |
| <ReportVersionSelector versions={versions} selectedVersion={selectedVersion} disabled={loadingVersions || loadingDetail} onSelect={setSelectedVersion} /> | |
| {precondition && <p className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-800">{precondition}</p>} | |
| {error && <ErrorState title="Report error" message={error} />} | |
| {loadingDetail ? ( | |
| <div className="flex items-center justify-center gap-2 rounded-lg border border-slate-200 bg-white py-8 text-sm text-slate-500"> | |
| <Loader2 className="h-4 w-4 animate-spin" /> Loading report | |
| </div> | |
| ) : detail ? ( | |
| <article className="rounded-lg border border-slate-200 bg-white p-4"> | |
| <div className="mb-3 text-xs text-slate-500">v{detail.version} generated {formatDateTime(detail.generated_at)}</div> | |
| {detail.rendered_markdown ? ( | |
| <MarkdownContent content={detail.rendered_markdown} /> | |
| ) : ( | |
| <div className="space-y-4 text-sm leading-6 text-slate-700"> | |
| {detail.executive_summary && <p>{detail.executive_summary}</p>} | |
| {detail.findings && detail.findings.length > 0 && ( | |
| <div> | |
| <p className="font-medium text-slate-900">Findings</p> | |
| <ul className="mt-1 list-disc space-y-1 pl-5"> | |
| {detail.findings.map((item, index) => <li key={index}>{item.text}</li>)} | |
| </ul> | |
| </div> | |
| )} | |
| {detail.caveats && detail.caveats.length > 0 && ( | |
| <div> | |
| <p className="font-medium text-slate-900">Caveats</p> | |
| <ul className="mt-1 list-disc space-y-1 pl-5"> | |
| {detail.caveats.map((item, index) => <li key={index}>{item.text}</li>)} | |
| </ul> | |
| </div> | |
| )} | |
| {detail.open_questions && detail.open_questions.length > 0 && ( | |
| <div> | |
| <p className="font-medium text-slate-900">Open questions</p> | |
| <ul className="mt-1 list-disc space-y-1 pl-5"> | |
| {detail.open_questions.map((item, index) => <li key={index}>{item.text}</li>)} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </article> | |
| ) : ( | |
| <EmptyState title="No report selected" description="Generate a report or select an existing version." /> | |
| )} | |
| </> | |
| )} | |
| </section> | |
| ); | |
| } | |