harryagasi
fix: resizable report sidebar, stable chat layout, guided empty state
15d8afd
Raw
History Blame Contribute Delete
8.43 kB
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;
}
export function ReportSidebar({ analysis, userId, onCollapse, fullscreen = false, onToggleFullscreen }: 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}
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>
);
}