"use client"; import { useContext, useEffect, useRef, useState } from "react"; import { MessageResponse } from "@/components/ai-elements/message"; import { ChatActionsContext } from "@/components/chat"; import type { ArtifactPartProps, JobRecord, ResearchResult } from "@/src/core"; /** * The research job card (contracts.ts "THE ASYNC PROTOCOL" §4). * * While the part is input-available: an activity card — poll /api/jobs every * 2s, find the job by toolCallId, show a pulsing dot + "Researching…" + the * job's live activity line. When the job flips to done/error, call * syncFromServer() ONCE (fresh messages replace local state; the part * re-renders as the brief card and the sendAutomaticallyWhen continuation * resumes the teacher) and stop polling. * * When output-available: the research brief — markdown body + a small source * link list, in the visual language of answered-card.tsx. */ export function JobCard({ part }: ArtifactPartProps) { if (part.state === "output-available") { return ; } return ; } function ResearchActivityCard({ toolCallId }: { toolCallId: string }) { const actions = useContext(ChatActionsContext); const [job, setJob] = useState(null); const finishedRef = useRef(false); useEffect(() => { if (!actions) return; let cancelled = false; const poll = async () => { if (finishedRef.current) return true; try { const res = await fetch(`/api/jobs?chatId=${encodeURIComponent(actions.chatId)}`); if (!res.ok) return false; const { jobs } = (await res.json()) as { jobs?: JobRecord[] }; const match = jobs?.find((j) => j.toolCallId === toolCallId) ?? null; if (cancelled) return false; setJob(match); if (match && match.status !== "running") { // The part has (or is about to be) fulfilled server-side — pull the // fresh messages once; the continuation resumes the teacher. finishedRef.current = true; void actions.syncFromServer(); return true; } } catch { /* transient fetch failure — keep polling */ } return false; }; const timer = setInterval(() => { if (finishedRef.current) { clearInterval(timer); return; } void poll().then((stop) => { if (stop) clearInterval(timer); }); }, 2000); void poll().then((stop) => { if (stop) clearInterval(timer); }); return () => { cancelled = true; clearInterval(timer); }; }, [actions, toolCallId]); return (
Researching…
{job?.activity && (
{job.activity}
)}
); } function ResearchBriefCard({ output }: { output: unknown }) { const result = output as ResearchResult | undefined; if (!result || typeof result.brief !== "string") { return (
Research completed, but the result could not be displayed.
); } const failed = result.brief.startsWith("Research failed"); return (
Research brief {failed && failed}
{result.brief}
{result.sources.length > 0 && (
Sources
)}
); }