Spaces:
Sleeping
Sleeping
File size: 8,477 Bytes
30cd0c9 15d8afd 30cd0c9 15d8afd 3baad7e 30cd0c9 3baad7e 30cd0c9 f18b03d 30cd0c9 f18b03d 30cd0c9 e2da2f6 f18b03d 30cd0c9 587748b 30cd0c9 f18b03d 30cd0c9 15d8afd 30cd0c9 3baad7e 30cd0c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | 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>
);
}
|