feat: link artifacts to source chat

This commit is contained in:
CREDO23 2026-06-23 15:47:21 +02:00
parent 9c622ae3f3
commit 8b0a2f8964
10 changed files with 44 additions and 7 deletions

View file

@ -84,6 +84,7 @@ class PodcastSummary(BaseModel):
status: PodcastStatus status: PodcastStatus
created_at: datetime created_at: datetime
search_space_id: int search_space_id: int
thread_id: int | None = None
class PodcastDetail(BaseModel): class PodcastDetail(BaseModel):

View file

@ -24,6 +24,7 @@ class ReportRead(BaseModel):
report_metadata: dict[str, Any] | None = None report_metadata: dict[str, Any] | None = None
report_group_id: int | None = None report_group_id: int | None = None
content_type: str = "markdown" content_type: str = "markdown"
thread_id: int | None = None
created_at: datetime created_at: datetime
class Config: class Config:

View file

@ -44,6 +44,7 @@ class VideoPresentationRead(VideoPresentationBase):
status: VideoPresentationStatusEnum = VideoPresentationStatusEnum.READY status: VideoPresentationStatusEnum = VideoPresentationStatusEnum.READY
created_at: datetime created_at: datetime
slide_count: int | None = None slide_count: int | None = None
thread_id: int | None = None
class Config: class Config:
from_attributes = True from_attributes = True
@ -68,6 +69,7 @@ class VideoPresentationRead(VideoPresentationBase):
"status": obj.status, "status": obj.status,
"created_at": obj.created_at, "created_at": obj.created_at,
"slide_count": len(obj.slides) if obj.slides else None, "slide_count": len(obj.slides) if obj.slides else None,
"thread_id": obj.thread_id,
} }
return cls(**data) return cls(**data)

View file

@ -163,6 +163,7 @@ export const podcastSummary = z.object({
status: podcastStatus, status: podcastStatus,
created_at: z.string(), created_at: z.string(),
search_space_id: z.number(), search_space_id: z.number(),
thread_id: z.number().nullish(),
}); });
export type PodcastSummary = z.infer<typeof podcastSummary>; export type PodcastSummary = z.infer<typeof podcastSummary>;

View file

@ -17,6 +17,7 @@ export const reportListItem = z.object({
title: z.string(), title: z.string(),
content_type: z.string().default("markdown"), content_type: z.string().default("markdown"),
report_metadata: reportMetadata, report_metadata: reportMetadata,
thread_id: z.number().nullish(),
created_at: z.string(), created_at: z.string(),
}); });
export type ReportListItem = z.infer<typeof reportListItem>; export type ReportListItem = z.infer<typeof reportListItem>;

View file

@ -13,6 +13,7 @@ export const videoPresentationListItem = z.object({
status: videoPresentationStatus.default("ready"), status: videoPresentationStatus.default("ready"),
created_at: z.string(), created_at: z.string(),
search_space_id: z.number(), search_space_id: z.number(),
thread_id: z.number().nullish(),
}); });
export type VideoPresentationListItem = z.infer<typeof videoPresentationListItem>; export type VideoPresentationListItem = z.infer<typeof videoPresentationListItem>;

View file

@ -39,6 +39,7 @@ async function fetchLibraryArtifacts(searchSpaceId: number): Promise<LibraryArti
status: report.report_metadata?.status === "failed" ? "error" : "ready", status: report.report_metadata?.status === "failed" ? "error" : "ready",
createdAt: report.created_at, createdAt: report.created_at,
contentType: isResume ? "typst" : "markdown", contentType: isResume ? "typst" : "markdown",
sourceThreadId: report.thread_id,
}); });
} }
@ -51,6 +52,7 @@ async function fetchLibraryArtifacts(searchSpaceId: number): Promise<LibraryArti
status: podcastStatus(podcast.status), status: podcastStatus(podcast.status),
createdAt: podcast.created_at, createdAt: podcast.created_at,
contentType: "markdown", contentType: "markdown",
sourceThreadId: podcast.thread_id,
}); });
} }
@ -63,6 +65,7 @@ async function fetchLibraryArtifacts(searchSpaceId: number): Promise<LibraryArti
status: videoStatus(video.status), status: videoStatus(video.status),
createdAt: video.created_at, createdAt: video.created_at,
contentType: "markdown", contentType: "markdown",
sourceThreadId: video.thread_id,
}); });
} }

View file

@ -18,4 +18,6 @@ export interface LibraryArtifact {
createdAt: string; createdAt: string;
/** Report panel content type — "typst" for resumes, "markdown" otherwise. */ /** Report panel content type — "typst" for resumes, "markdown" otherwise. */
contentType: "markdown" | "typst"; contentType: "markdown" | "typst";
/** Chat thread that produced this artifact, when the source recorded one. */
sourceThreadId?: number | null;
} }

View file

@ -1,12 +1,16 @@
import { MessageSquareText } from "lucide-react";
import Link from "next/link";
import { formatRelativeDate } from "@/lib/format-date"; import { formatRelativeDate } from "@/lib/format-date";
import type { LibraryArtifact } from "../model/artifact"; import type { LibraryArtifact } from "../model/artifact";
import { KIND_META } from "./kind-meta"; import { KIND_META } from "./kind-meta";
export function ArtifactCard({ export function ArtifactCard({
artifact, artifact,
searchSpaceId,
onOpen, onOpen,
}: { }: {
artifact: LibraryArtifact; artifact: LibraryArtifact;
searchSpaceId: number;
onOpen: (artifact: LibraryArtifact) => void; onOpen: (artifact: LibraryArtifact) => void;
}) { }) {
const meta = KIND_META[artifact.kind]; const meta = KIND_META[artifact.kind];
@ -20,11 +24,16 @@ export function ArtifactCard({
: meta.label; : meta.label;
return ( return (
<button <div className="group relative flex items-start gap-3 rounded-xl border bg-card p-3 transition-colors hover:border-primary/40 hover:bg-accent/50">
type="button" {/* Stretched overlay makes the whole card open the viewer; sibling controls sit above it via z-10. */}
onClick={() => onOpen(artifact)} <button
className="group flex w-full items-start gap-3 rounded-xl border bg-card p-3 text-left transition-colors hover:border-primary/40 hover:bg-accent/50" type="button"
> onClick={() => onOpen(artifact)}
className="absolute inset-0 rounded-xl"
>
<span className="sr-only">Open {artifact.title}</span>
</button>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground"> <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-4" /> <Icon className="size-4" />
</span> </span>
@ -38,6 +47,17 @@ export function ArtifactCard({
<span>{formatRelativeDate(artifact.createdAt)}</span> <span>{formatRelativeDate(artifact.createdAt)}</span>
</span> </span>
</span> </span>
</button>
{artifact.sourceThreadId ? (
<Link
href={`/dashboard/${searchSpaceId}/new-chat/${artifact.sourceThreadId}`}
title="Open source chat"
className="relative z-10 flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100"
>
<MessageSquareText className="size-4" />
<span className="sr-only">Open source chat</span>
</Link>
) : null}
</div>
); );
} }

View file

@ -121,7 +121,12 @@ export function ArtifactsLibrary({ searchSpaceId }: { searchSpaceId: number }) {
</h2> </h2>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{items.map((artifact) => ( {items.map((artifact) => (
<ArtifactCard key={artifact.key} artifact={artifact} onOpen={handleOpen} /> <ArtifactCard
key={artifact.key}
artifact={artifact}
searchSpaceId={searchSpaceId}
onOpen={handleOpen}
/>
))} ))}
</div> </div>
</section> </section>