From a5be3fbcf8165c170f3f7e99cf7db70978addedf Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 23 Jun 2026 15:18:08 +0200 Subject: [PATCH] feat: add artifacts library page --- .../[search_space_id]/artifacts/page.tsx | 11 ++ .../features/artifacts-library/index.ts | 1 + .../ui/artifacts-library.tsx | 137 ++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 surfsense_web/app/dashboard/[search_space_id]/artifacts/page.tsx create mode 100644 surfsense_web/features/artifacts-library/index.ts create mode 100644 surfsense_web/features/artifacts-library/ui/artifacts-library.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/artifacts/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/artifacts/page.tsx new file mode 100644 index 000000000..8f8109156 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/artifacts/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { ArtifactsLibrary } from "@/features/artifacts-library"; + +export default function ArtifactsPage() { + const params = useParams(); + const searchSpaceId = Number(params.search_space_id); + + return ; +} diff --git a/surfsense_web/features/artifacts-library/index.ts b/surfsense_web/features/artifacts-library/index.ts new file mode 100644 index 000000000..f086f50ae --- /dev/null +++ b/surfsense_web/features/artifacts-library/index.ts @@ -0,0 +1 @@ +export { ArtifactsLibrary } from "./ui/artifacts-library"; diff --git a/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx b/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx new file mode 100644 index 000000000..0c354c331 --- /dev/null +++ b/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useSetAtom } from "jotai"; +import { Boxes, RefreshCw, TriangleAlert } from "lucide-react"; +import { useMemo, useState } from "react"; +import { openReportPanelAtom } from "@/atoms/chat/report-panel.atom"; +import { MobileReportPanel } from "@/components/report-panel/report-panel"; +import { Button } from "@/components/ui/button"; +import { useLibraryArtifacts } from "../hooks/use-library-artifacts"; +import type { LibraryArtifact, LibraryArtifactKind } from "../model/artifact"; +import { ArtifactCard } from "./artifact-card"; +import { KIND_META, KIND_ORDER } from "./kind-meta"; +import { MediaViewerDialog } from "./media-viewer-dialog"; + +const SKELETON_KEYS = ["s1", "s2", "s3", "s4", "s5", "s6"]; + +function LoadingState() { + return ( +
+ {SKELETON_KEYS.map((key) => ( +
+ ))} +
+ ); +} + +function ErrorState({ onRetry }: { onRetry: () => void }) { + return ( +
+ + + +
+

Couldn't load artifacts

+

+ Something went wrong fetching this search space's deliverables. +

+
+ +
+ ); +} + +function EmptyState() { + return ( +
+ + + +
+

No artifacts yet

+

+ Reports, resumes, podcasts, presentations, and images you generate appear here. +

+
+
+ ); +} + +export function ArtifactsLibrary({ searchSpaceId }: { searchSpaceId: number }) { + const { artifacts, loading, error, refresh } = useLibraryArtifacts(searchSpaceId); + const openReportPanel = useSetAtom(openReportPanelAtom); + const [selectedMedia, setSelectedMedia] = useState(null); + + const grouped = useMemo(() => { + const map = new Map(); + for (const artifact of artifacts) { + const bucket = map.get(artifact.kind); + if (bucket) bucket.push(artifact); + else map.set(artifact.kind, [artifact]); + } + return map; + }, [artifacts]); + + const handleOpen = (artifact: LibraryArtifact) => { + // Reports/resumes reuse the shared report panel; the rest open in the dialog. + if (artifact.kind === "report" || artifact.kind === "resume") { + openReportPanel({ + reportId: artifact.entityId, + title: artifact.title, + contentType: artifact.contentType, + }); + return; + } + setSelectedMedia(artifact); + }; + + return ( +
+
+
+

Artifacts

+

+ Every deliverable created across this search space. +

+
+ {!loading && artifacts.length > 0 ? ( + {artifacts.length} total + ) : null} +
+ + {loading ? ( + + ) : error ? ( + refresh()} /> + ) : artifacts.length === 0 ? ( + + ) : ( +
+ {KIND_ORDER.map((kind) => { + const items = grouped.get(kind); + if (!items || items.length === 0) return null; + return ( +
+

+ {KIND_META[kind].group} + {items.length} +

+
+ {items.map((artifact) => ( + + ))} +
+
+ ); + })} +
+ )} + + setSelectedMedia(null)} /> + +
+ ); +}