diff --git a/surfsense_web/atoms/citation/citation-panel.atom.ts b/surfsense_web/atoms/citation/citation-panel.atom.ts index ca7312857..f92fd6f90 100644 --- a/surfsense_web/atoms/citation/citation-panel.atom.ts +++ b/surfsense_web/atoms/citation/citation-panel.atom.ts @@ -1,14 +1,19 @@ -import { atom } from "jotai"; +import { atom, type Getter, type Setter } from "jotai"; import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom"; +/** The source the citation panel is showing: a KB chunk or a scraper run. */ +export type CitationTarget = + | { kind: "chunk"; chunkId: number } + | { kind: "run"; runId: string }; + interface CitationPanelState { isOpen: boolean; - chunkId: number | null; + target: CitationTarget | null; } const initialState: CitationPanelState = { isOpen: false, - chunkId: null, + target: null, }; export const citationPanelAtom = atom(initialState); @@ -17,16 +22,21 @@ export const citationPanelOpenAtom = atom((get) => get(citationPanelAtom).isOpen const preCitationCollapsedAtom = atom(null); -export const openCitationPanelAtom = atom(null, (get, set, payload: { chunkId: number }) => { +function openWithTarget(get: Getter, set: Setter, target: CitationTarget) { if (!get(citationPanelAtom).isOpen) { set(preCitationCollapsedAtom, get(rightPanelCollapsedAtom)); } - set(citationPanelAtom, { - isOpen: true, - chunkId: payload.chunkId, - }); + set(citationPanelAtom, { isOpen: true, target }); set(rightPanelTabAtom, "citation"); set(rightPanelCollapsedAtom, false); +} + +export const openCitationPanelAtom = atom(null, (get, set, payload: { chunkId: number }) => { + openWithTarget(get, set, { kind: "chunk", chunkId: payload.chunkId }); +}); + +export const openRunCitationPanelAtom = atom(null, (get, set, payload: { runId: string }) => { + openWithTarget(get, set, { kind: "run", runId: payload.runId }); }); export const closeCitationPanelAtom = atom(null, (get, set) => { diff --git a/surfsense_web/components/citations/run-citation-panel.tsx b/surfsense_web/components/citations/run-citation-panel.tsx new file mode 100644 index 000000000..3b7af7ca1 --- /dev/null +++ b/surfsense_web/components/citations/run-citation-panel.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { XIcon } from "lucide-react"; +import { useParams } from "next/navigation"; +import type { FC } from "react"; +import { RunDetail } from "@/app/dashboard/[workspace_id]/playground/components/run-detail"; +import { Button } from "@/components/ui/button"; + +/** Right-panel viewer for a cited scraper run. `runId` is the `run_` handle. */ +export const RunCitationPanelContent: FC<{ runId: string; onClose?: () => void }> = ({ + runId, + onClose, +}) => { + const params = useParams(); + const rawWorkspaceId = Array.isArray(params?.workspace_id) + ? params.workspace_id[0] + : params?.workspace_id; + const workspaceId = Number(rawWorkspaceId); + const scraperRunId = runId.replace(/^run_/, ""); + + return ( + <> +
+

Scraper run

+ {onClose && ( + + )} +
+ +
+ {Number.isFinite(workspaceId) ? ( + + ) : ( +

Open a workspace to view this run.

+ )} +
+ + ); +}; diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx index 15025f4eb..362e676d8 100644 --- a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx +++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx @@ -35,6 +35,14 @@ const CitationPanelContent = dynamic( { ssr: false, loading: () => null } ); +const RunCitationPanelContent = dynamic( + () => + import("@/components/citations/run-citation-panel").then((m) => ({ + default: m.RunCitationPanelContent, + })), + { ssr: false, loading: () => null } +); + const HitlEditPanelContent = dynamic( () => import("@/features/chat-messages/hitl").then((m) => ({ @@ -89,7 +97,7 @@ export function RightPanelToggleButton({ ? !!editorState.memoryScope : !!editorState.localFilePath); const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; - const citationOpen = citationState.isOpen && citationState.chunkId != null; + const citationOpen = citationState.isOpen && citationState.target != null; const hasContent = reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen; const label = collapsed ? "Expand panel" : "Collapse panel"; @@ -141,7 +149,7 @@ export function RightPanelExpandButton() { ? !!editorState.memoryScope : !!editorState.localFilePath); const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; - const citationOpen = citationState.isOpen && citationState.chunkId != null; + const citationOpen = citationState.isOpen && citationState.target != null; const hasContent = reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen; if (!collapsed || !hasContent) return null; @@ -212,7 +220,7 @@ export function RightPanel({ showTopBorder = false }: RightPanelProps) { ? !!editorState.memoryScope : !!editorState.localFilePath); const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; - const citationOpen = citationState.isOpen && citationState.chunkId != null; + const citationOpen = citationState.isOpen && citationState.target != null; useEffect(() => { if (!reportOpen && !editorOpen && !hitlEditOpen && !citationOpen && !artifactsOpen) return; @@ -306,9 +314,19 @@ export function RightPanel({ showTopBorder = false }: RightPanelProps) { /> )} - {effectiveTab === "citation" && citationOpen && citationState.chunkId != null && ( + {effectiveTab === "citation" && citationOpen && citationState.target && (
- + {citationState.target.kind === "run" ? ( + + ) : ( + + )}
)} {effectiveTab === "artifacts" && artifactsOpen && (