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
created_at: datetime
search_space_id: int
thread_id: int | None = None
class PodcastDetail(BaseModel):

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ export const videoPresentationListItem = z.object({
status: videoPresentationStatus.default("ready"),
created_at: z.string(),
search_space_id: z.number(),
thread_id: z.number().nullish(),
});
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",
createdAt: report.created_at,
contentType: isResume ? "typst" : "markdown",
sourceThreadId: report.thread_id,
});
}
@ -51,6 +52,7 @@ async function fetchLibraryArtifacts(searchSpaceId: number): Promise<LibraryArti
status: podcastStatus(podcast.status),
createdAt: podcast.created_at,
contentType: "markdown",
sourceThreadId: podcast.thread_id,
});
}
@ -63,6 +65,7 @@ async function fetchLibraryArtifacts(searchSpaceId: number): Promise<LibraryArti
status: videoStatus(video.status),
createdAt: video.created_at,
contentType: "markdown",
sourceThreadId: video.thread_id,
});
}

View file

@ -18,4 +18,6 @@ export interface LibraryArtifact {
createdAt: string;
/** Report panel content type — "typst" for resumes, "markdown" otherwise. */
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 type { LibraryArtifact } from "../model/artifact";
import { KIND_META } from "./kind-meta";
export function ArtifactCard({
artifact,
searchSpaceId,
onOpen,
}: {
artifact: LibraryArtifact;
searchSpaceId: number;
onOpen: (artifact: LibraryArtifact) => void;
}) {
const meta = KIND_META[artifact.kind];
@ -20,11 +24,16 @@ export function ArtifactCard({
: meta.label;
return (
<button
type="button"
onClick={() => onOpen(artifact)}
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"
>
<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">
{/* Stretched overlay makes the whole card open the viewer; sibling controls sit above it via z-10. */}
<button
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">
<Icon className="size-4" />
</span>
@ -38,6 +47,17 @@ export function ArtifactCard({
<span>{formatRelativeDate(artifact.createdAt)}</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>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{items.map((artifact) => (
<ArtifactCard key={artifact.key} artifact={artifact} onOpen={handleOpen} />
<ArtifactCard
key={artifact.key}
artifact={artifact}
searchSpaceId={searchSpaceId}
onOpen={handleOpen}
/>
))}
</div>
</section>