SurfSense/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx

258 lines
8.5 KiB
TypeScript
Raw Normal View History

"use client";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { PanelRight } from "lucide-react";
2026-03-31 14:45:46 -07:00
import dynamic from "next/dynamic";
import { startTransition, useEffect } from "react";
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
2026-04-28 23:25:26 -07:00
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { DocumentsSidebar } from "../sidebar";
const EditorPanelContent = dynamic(
2026-03-31 14:45:46 -07:00
() =>
import("@/components/editor-panel/editor-panel").then((m) => ({
default: m.EditorPanelContent,
})),
{ ssr: false, loading: () => null }
);
2026-04-28 23:25:26 -07:00
const CitationPanelContent = dynamic(
() =>
import("@/components/citation-panel/citation-panel").then((m) => ({
default: m.CitationPanelContent,
})),
{ ssr: false, loading: () => null }
);
const HitlEditPanelContent = dynamic(
2026-03-31 14:45:46 -07:00
() =>
import("@/components/hitl-edit-panel/hitl-edit-panel").then((m) => ({
default: m.HitlEditPanelContent,
})),
{ ssr: false, loading: () => null }
);
const ReportPanelContent = dynamic(
2026-03-31 14:45:46 -07:00
() =>
import("@/components/report-panel/report-panel").then((m) => ({
default: m.ReportPanelContent,
})),
{ ssr: false, loading: () => null }
);
interface RightPanelProps {
documentsPanel?: {
open: boolean;
onOpenChange: (open: boolean) => void;
};
}
function CollapseButton({ onClick }: { onClick: () => void }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={onClick} className="h-8 w-8 shrink-0">
<PanelRight className="h-4 w-4" />
<span className="sr-only">Collapse panel</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Collapse panel</TooltipContent>
</Tooltip>
);
}
/**
* Absolutely positioned expand button renders at top-right of the main
* container so it occupies the same screen position as the collapse button
* inside the Documents header.
*/
export function RightPanelExpandButton() {
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
const reportState = useAtomValue(reportPanelAtom);
const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom);
2026-04-28 23:25:26 -07:00
const citationState = useAtomValue(citationPanelAtom);
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
editorState.isOpen &&
2026-04-27 14:04:50 -07:00
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
2026-04-28 23:25:26 -07:00
const citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
if (!collapsed || !hasContent) return null;
return (
<div className="flex shrink-0 items-center px-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
2026-03-11 02:18:59 +05:30
onClick={() => startTransition(() => setCollapsed(false))}
className="h-8 w-8 shrink-0 -m-0.5"
>
<PanelRight className="h-4 w-4" />
<span className="sr-only">Expand panel</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Expand panel</TooltipContent>
</Tooltip>
</div>
);
}
2026-04-28 23:25:26 -07:00
const PANEL_WIDTHS = {
sources: 420,
report: 640,
editor: 640,
"hitl-edit": 640,
citation: 560,
} as const;
export function RightPanel({ documentsPanel }: RightPanelProps) {
const [activeTab] = useAtom(rightPanelTabAtom);
const reportState = useAtomValue(reportPanelAtom);
const closeReport = useSetAtom(closeReportPanelAtom);
const editorState = useAtomValue(editorPanelAtom);
const closeEditor = useSetAtom(closeEditorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom);
const closeHitlEdit = useSetAtom(closeHitlEditPanelAtom);
2026-04-28 23:25:26 -07:00
const citationState = useAtomValue(citationPanelAtom);
const closeCitation = useSetAtom(closeCitationPanelAtom);
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = documentsPanel?.open ?? false;
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
editorState.isOpen &&
2026-04-27 14:04:50 -07:00
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
2026-04-28 23:25:26 -07:00
const citationOpen = citationState.isOpen && citationState.chunkId != null;
useEffect(() => {
2026-04-28 23:25:26 -07:00
if (!reportOpen && !editorOpen && !hitlEditOpen && !citationOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (hitlEditOpen) closeHitlEdit();
2026-04-28 23:25:26 -07:00
else if (citationOpen) closeCitation();
else if (editorOpen) closeEditor();
else if (reportOpen) closeReport();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
2026-04-28 23:25:26 -07:00
}, [
reportOpen,
editorOpen,
hitlEditOpen,
citationOpen,
closeReport,
closeEditor,
closeHitlEdit,
closeCitation,
]);
2026-04-28 23:25:26 -07:00
const isVisible =
(documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen) && !collapsed;
let effectiveTab = activeTab;
if (effectiveTab === "hitl-edit" && !hitlEditOpen) {
2026-04-28 23:25:26 -07:00
effectiveTab = citationOpen
? "citation"
: editorOpen
? "editor"
: reportOpen
? "report"
: "sources";
} else if (effectiveTab === "citation" && !citationOpen) {
effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources";
} else if (effectiveTab === "editor" && !editorOpen) {
2026-04-28 23:25:26 -07:00
effectiveTab = citationOpen ? "citation" : reportOpen ? "report" : "sources";
} else if (effectiveTab === "report" && !reportOpen) {
2026-04-28 23:25:26 -07:00
effectiveTab = citationOpen ? "citation" : editorOpen ? "editor" : "sources";
} else if (effectiveTab === "sources" && !documentsOpen) {
2026-03-21 13:20:13 +05:30
effectiveTab = hitlEditOpen
? "hitl-edit"
2026-04-28 23:25:26 -07:00
: citationOpen
? "citation"
: editorOpen
? "editor"
: reportOpen
? "report"
: "sources";
}
const targetWidth = PANEL_WIDTHS[effectiveTab];
const collapseButton = <CollapseButton onClick={() => setCollapsed(true)} />;
if (!isVisible) return null;
return (
<aside
style={{ width: targetWidth }}
className="flex h-full shrink-0 flex-col rounded-xl border bg-sidebar text-sidebar-foreground overflow-hidden transition-[width] duration-200 ease-out"
>
<div className="relative flex-1 min-h-0 overflow-hidden">
{effectiveTab === "sources" && documentsOpen && documentsPanel && (
<div className="h-full">
<DocumentsSidebar
open={documentsPanel.open}
onOpenChange={documentsPanel.onOpenChange}
embedded
headerAction={collapseButton}
/>
</div>
)}
{effectiveTab === "report" && reportOpen && (
<div className="h-full flex flex-col">
<ReportPanelContent
reportId={reportState.reportId as number}
title={reportState.title || "Report"}
onClose={closeReport}
shareToken={reportState.shareToken}
/>
</div>
)}
{effectiveTab === "editor" && editorOpen && (
<div className="h-full flex flex-col">
<EditorPanelContent
kind={editorState.kind}
documentId={editorState.documentId ?? undefined}
localFilePath={editorState.localFilePath ?? undefined}
searchSpaceId={editorState.searchSpaceId ?? undefined}
title={editorState.title}
onClose={closeEditor}
/>
</div>
)}
{effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && (
<div className="h-full flex flex-col">
2026-03-21 13:20:13 +05:30
<HitlEditPanelContent
title={hitlEditState.title}
content={hitlEditState.content}
toolName={hitlEditState.toolName}
contentFormat={hitlEditState.contentFormat}
2026-03-21 13:20:13 +05:30
extraFields={hitlEditState.extraFields}
onSave={hitlEditState.onSave}
onClose={closeHitlEdit}
/>
</div>
)}
2026-04-28 23:25:26 -07:00
{effectiveTab === "citation" && citationOpen && citationState.chunkId != null && (
<div className="h-full flex flex-col">
<CitationPanelContent chunkId={citationState.chunkId} onClose={closeCitation} />
</div>
)}
</div>
</aside>
);
}