feat: render artifacts in right panel

This commit is contained in:
CREDO23 2026-06-22 22:38:15 +02:00
parent 695ad19620
commit 050d6bf998

View file

@ -8,9 +8,14 @@ import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom"; import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms"; import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom"; import {
type RightPanelTab,
rightPanelCollapsedAtom,
rightPanelTabAtom,
} from "@/atoms/layout/right-panel.atom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { artifactsPanelOpenAtom, closeArtifactsPanelAtom } from "@/features/chat-artifacts";
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl"; import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl";
import { useMediaQuery } from "@/hooks/use-media-query"; import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -48,6 +53,14 @@ const ReportPanelContent = dynamic(
{ ssr: false, loading: () => null } { ssr: false, loading: () => null }
); );
const ArtifactsPanelContent = dynamic(
() =>
import("@/features/chat-artifacts").then((m) => ({
default: m.ArtifactsPanelContent,
})),
{ ssr: false, loading: () => null }
);
interface RightPanelProps { interface RightPanelProps {
documentsPanel?: { documentsPanel?: {
open: boolean; open: boolean;
@ -101,6 +114,7 @@ export function RightPanelToggleButton({
const editorState = useAtomValue(editorPanelAtom); const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom); const hitlEditState = useAtomValue(hitlEditPanelAtom);
const citationState = useAtomValue(citationPanelAtom); const citationState = useAtomValue(citationPanelAtom);
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
const reportOpen = reportState.isOpen && !!reportState.reportId; const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen = const editorOpen =
editorState.isOpen && editorState.isOpen &&
@ -111,7 +125,8 @@ export function RightPanelToggleButton({
: !!editorState.localFilePath); : !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const citationOpen = citationState.isOpen && citationState.chunkId != null; const citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen; const hasContent =
documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
const label = collapsed ? "Expand panel" : "Collapse panel"; const label = collapsed ? "Expand panel" : "Collapse panel";
if (!hasContent) return null; if (!hasContent) return null;
@ -153,6 +168,7 @@ export function RightPanelExpandButton() {
const editorState = useAtomValue(editorPanelAtom); const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom); const hitlEditState = useAtomValue(hitlEditPanelAtom);
const citationState = useAtomValue(citationPanelAtom); const citationState = useAtomValue(citationPanelAtom);
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
const reportOpen = reportState.isOpen && !!reportState.reportId; const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen = const editorOpen =
editorState.isOpen && editorState.isOpen &&
@ -163,7 +179,8 @@ export function RightPanelExpandButton() {
: !!editorState.localFilePath); : !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const citationOpen = citationState.isOpen && citationState.chunkId != null; const citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen; const hasContent =
documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
if (!collapsed || !hasContent) return null; if (!collapsed || !hasContent) return null;
@ -180,8 +197,31 @@ const PANEL_WIDTHS = {
editor: 640, editor: 640,
"hitl-edit": 640, "hitl-edit": 640,
citation: 560, citation: 560,
artifacts: 420,
} as const; } as const;
/**
* Priority order used to fall back to another open surface when the active
* tab's content closes. Artifacts sit just above the always-available sources
* tab.
*/
const TAB_FALLBACK_ORDER: RightPanelTab[] = [
"hitl-edit",
"citation",
"editor",
"report",
"artifacts",
"sources",
];
function resolveEffectiveTab(
activeTab: RightPanelTab,
openByTab: Record<RightPanelTab, boolean>
): RightPanelTab {
if (openByTab[activeTab]) return activeTab;
return TAB_FALLBACK_ORDER.find((tab) => openByTab[tab]) ?? "sources";
}
export function RightPanel({ export function RightPanel({
documentsPanel, documentsPanel,
showCollapseButton = true, showCollapseButton = true,
@ -196,6 +236,8 @@ export function RightPanel({
const closeHitlEdit = useSetAtom(closeHitlEditPanelAtom); const closeHitlEdit = useSetAtom(closeHitlEditPanelAtom);
const citationState = useAtomValue(citationPanelAtom); const citationState = useAtomValue(citationPanelAtom);
const closeCitation = useSetAtom(closeCitationPanelAtom); const closeCitation = useSetAtom(closeCitationPanelAtom);
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
const closeArtifacts = useSetAtom(closeArtifactsPanelAtom);
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom); const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
// Desktop-only surface; mobile uses the dedicated Mobile* drawers. Without // Desktop-only surface; mobile uses the dedicated Mobile* drawers. Without
// this guard both render together and two editors fight over one model. // this guard both render together and two editors fight over one model.
@ -214,13 +256,14 @@ export function RightPanel({
const citationOpen = citationState.isOpen && citationState.chunkId != null; const citationOpen = citationState.isOpen && citationState.chunkId != null;
useEffect(() => { useEffect(() => {
if (!reportOpen && !editorOpen && !hitlEditOpen && !citationOpen) return; if (!reportOpen && !editorOpen && !hitlEditOpen && !citationOpen && !artifactsOpen) return;
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") { if (e.key === "Escape") {
if (hitlEditOpen) closeHitlEdit(); if (hitlEditOpen) closeHitlEdit();
else if (citationOpen) closeCitation(); else if (citationOpen) closeCitation();
else if (editorOpen) closeEditor(); else if (editorOpen) closeEditor();
else if (reportOpen) closeReport(); else if (reportOpen) closeReport();
else if (artifactsOpen) closeArtifacts();
} }
}; };
document.addEventListener("keydown", handleKeyDown); document.addEventListener("keydown", handleKeyDown);
@ -230,41 +273,26 @@ export function RightPanel({
editorOpen, editorOpen,
hitlEditOpen, hitlEditOpen,
citationOpen, citationOpen,
artifactsOpen,
closeReport, closeReport,
closeEditor, closeEditor,
closeHitlEdit, closeHitlEdit,
closeCitation, closeCitation,
closeArtifacts,
]); ]);
const isVisible = const isVisible =
(documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen) && !collapsed; (documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen) &&
!collapsed;
let effectiveTab = activeTab; const effectiveTab = resolveEffectiveTab(activeTab, {
if (effectiveTab === "hitl-edit" && !hitlEditOpen) { sources: documentsOpen,
effectiveTab = citationOpen report: reportOpen,
? "citation" editor: editorOpen,
: editorOpen "hitl-edit": hitlEditOpen,
? "editor" citation: citationOpen,
: reportOpen artifacts: artifactsOpen,
? "report" });
: "sources";
} else if (effectiveTab === "citation" && !citationOpen) {
effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources";
} else if (effectiveTab === "editor" && !editorOpen) {
effectiveTab = citationOpen ? "citation" : reportOpen ? "report" : "sources";
} else if (effectiveTab === "report" && !reportOpen) {
effectiveTab = citationOpen ? "citation" : editorOpen ? "editor" : "sources";
} else if (effectiveTab === "sources" && !documentsOpen) {
effectiveTab = hitlEditOpen
? "hitl-edit"
: citationOpen
? "citation"
: editorOpen
? "editor"
: reportOpen
? "report"
: "sources";
}
const targetWidth = PANEL_WIDTHS[effectiveTab]; const targetWidth = PANEL_WIDTHS[effectiveTab];
const collapseButton = showCollapseButton ? ( const collapseButton = showCollapseButton ? (
@ -335,6 +363,11 @@ export function RightPanel({
<CitationPanelContent chunkId={citationState.chunkId} onClose={closeCitation} /> <CitationPanelContent chunkId={citationState.chunkId} onClose={closeCitation} />
</div> </div>
)} )}
{effectiveTab === "artifacts" && artifactsOpen && (
<div className="h-full flex flex-col">
<ArtifactsPanelContent onClose={closeArtifacts} />
</div>
)}
</div> </div>
</aside> </aside>
); );