/** The shared chat, grouped for reading rather than event delivery. */ import { useEffect, useId, useRef, useState, useSyncExternalStore } from "react"; import { ChevronIcon, CloseIcon, LoaderIcon, SignInIcon } from "@chopin/icons"; import { AgentFace, Face, MotionDisclosure, MotionDisclosureIcon, useCardMeta, } from "@chopin/editor"; import { MessageMarkdown } from "./markdown"; import { capitalize, displayText, duration, group, summarize, toolCopy } from "./model"; import { motionContract } from "../motion-contract"; import { motionImmediately } from "../motion-input"; import type { Chat, ConversationPlan } from "@chopin/protocol"; import type { Group, Message } from "./model"; import type { CardMetaStore, QuestionnaireStore } from "@chopin/editor"; import type { Transport } from "@chopin/question/react"; import { ActivityLine, DecisionPrompt } from "./decision-entry"; import { ScopedChoicePrompt } from "./scoped-choice-entry"; export type TranscriptDecisions = { questions: QuestionnaireStore; meta: CardMetaStore; wire?: Transport; connected: boolean; canEdit: boolean; onOpenCard: (questionnaireId: string) => void; }; function when(ts: number): string { return new Date(ts * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function ToolRun({ tools }: { tools: Chat.Activity[] }) { let [open, setOpen] = useState(false); let contentId = useId(); let summary = summarize(tools); let motion = motionContract("collapse"); if (summary.state === "running") { return (
); } return (
); } function SystemEntry({ item }: { item: Extract }) { return (

{displayText(item.text)}

); } function DecisionSystemEntry( { conversationPlan, decisions, item, latest }: { conversationPlan?: ConversationPlan.State; decisions: TranscriptDecisions; item: Extract & { decision: NonNullable }; latest: boolean; }, ) { let id = item.decision.questionnaireId; let values = useSyncExternalStore( decisions.questions.subscribe, decisions.questions.snapshot, decisions.questions.snapshot, ); let value = values.find(entry => entry.id === id)?.value; let meta = useCardMeta(decisions.meta, id); let entry: Chat.Entry & { decision: NonNullable } = { id: item.id, author: { kind: "system" }, text: item.text, ts: item.ts!, decision: item.decision, }; let props = { entry, latest, value, meta, wire: decisions.wire, connected: decisions.connected, canEdit: decisions.canEdit, onOpenCard: decisions.onOpenCard, }; if (entry.decision.kind === "scoped-choice") { return ( ); } return entry.decision.kind === "prompt" ? : ; } function MessageBody( { handle, message, onWithdraw }: { handle: string; message: Message; onWithdraw: (id: string) => void; }, ) { let text = displayText(message.text) ? message.text : message.author.kind === "member" ? "Ask Planner" : ""; return (
{text && (
{message.streaming && ▍}
{message.queued && message.author.kind === "member" && message.author.handle === handle && ( )}
)} {message.tools && message.tools.length > 0 && }
); } function MessageGroup( { group: item, handle, onWithdraw }: { group: Extract; handle: string; onWithdraw: (id: string) => void; }, ) { let first = item.messages[0]!; let name = item.author.kind === "agent" ? "Planner" : capitalize(item.author.handle); return (
{item.author.kind === "agent" ? : }
{name} {item.queued ? "queued" : when(first.ts!)}
{item.messages.map(message => ( ))}
); } export function Transcript( { active, conversationPlan, decisions, entries, handle, onWithdraw, queued, working, }: { active: boolean; canEdit?: boolean; conversationPlan?: ConversationPlan.State; decisions?: TranscriptDecisions; entries: Chat.Entry[]; handle: string; onWithdraw: (id: string) => void; queued: Chat.Waiting[]; working?: Pick; }, ) { let bottom = useRef(null); let pinned = useRef(true); let groups = group(entries, queued, working); let latestPrompt = new Map(); let latestScoped = new Map(); for (let entry of entries) { if (entry.decision?.kind === "prompt") { latestPrompt.set(entry.decision.questionnaireId, entry.id); } if (entry.decision?.kind === "scoped-choice") { latestScoped.set(entry.decision.proposalId, entry.id); } } useEffect(() => { if (active && pinned.current) bottom.current?.scrollIntoView({ block: "end" }); }, [active, entries, queued]); return (
{ let element = event.currentTarget; let distance = element.scrollHeight - element.scrollTop - element.clientHeight; pinned.current = distance < 40; }} >
{groups.map(item => item.kind === "system" ? decisions && item.decision && item.ts !== undefined ? ( & { decision: NonNullable; }} key={item.id} latest={item.decision.kind === "prompt" ? latestPrompt.get(item.decision.questionnaireId) === item.id : item.decision.kind === "scoped-choice" ? latestScoped.get(item.decision.proposalId) === item.id : true} /> ) : : ( ) )}
); }