TypeScript · 3274 bytes Raw Blame History
1 import type { ContentBlock, StreamStatus } from "@/lib/ipc/types";
2 import { shortModel } from "@/lib/format";
3
4 import { StreamingIndicator } from "./StreamingIndicator";
5 import { TextBlock } from "./content/TextBlock";
6 import { ThinkingBlock } from "./content/ThinkingBlock";
7 import { ToolUseBlock } from "./content/ToolUseBlock";
8
9 interface AssistantCardProps {
10 at: string;
11 model: string | null;
12 blocks: ContentBlock[];
13 stopReason?: string | null;
14 usage?: {
15 input_tokens: number;
16 output_tokens: number;
17 cache_creation_input_tokens: number;
18 cache_read_input_tokens: number;
19 } | null;
20 /** v1 chat status flag. Optional so v0 disk-read paths remain
21 * unchanged. `"streaming"` shows a pulsing indicator next to the
22 * model badge; `"error"` tints the border red. */
23 status?: StreamStatus | null;
24 }
25
26 export function AssistantCard({
27 at,
28 model,
29 blocks,
30 stopReason,
31 usage,
32 status,
33 }: AssistantCardProps) {
34 const borderClass =
35 status === "error"
36 ? "border-red-900/60"
37 : "border-border";
38 return (
39 <div className={`rounded border ${borderClass} bg-bg-1 px-4 py-3`}>
40 <div className="mb-2 flex flex-wrap items-center gap-2 text-[10px] uppercase tracking-wide">
41 <span className="text-fg-0">claude</span>
42 {model && (
43 <span className="rounded bg-bg-3 px-1.5 py-0.5 font-mono normal-case text-fg-2">
44 {shortModel(model)}
45 </span>
46 )}
47 {status === "streaming" && <StreamingIndicator />}
48 <span className="text-fg-3"></span>
49 <span className="text-fg-3">{formatAt(at)}</span>
50 {stopReason && stopReason !== "end_turn" && (
51 <>
52 <span className="text-fg-3"></span>
53 <span className="text-fg-3">stop: {stopReason}</span>
54 </>
55 )}
56 {usage && (
57 <>
58 <span className="text-fg-3"></span>
59 <span className="text-fg-3 normal-case">
60 in {usage.input_tokens.toLocaleString()} · out{" "}
61 {usage.output_tokens.toLocaleString()}
62 </span>
63 </>
64 )}
65 </div>
66 <div>
67 {blocks.map((block, i) => (
68 <BlockRenderer key={i} block={block} />
69 ))}
70 </div>
71 </div>
72 );
73 }
74
75 function BlockRenderer({ block }: { block: ContentBlock }) {
76 switch (block.type) {
77 case "text":
78 return <TextBlock text={block.text} />;
79 case "thinking":
80 return <ThinkingBlock text={block.text} />;
81 case "tool_use":
82 return <ToolUseBlock name={block.name} input={block.input} />;
83 case "tool_result":
84 return (
85 <div
86 className={`my-2 rounded border px-3 py-2 font-mono text-[11px] ${
87 block.isError
88 ? "border-red-900/60 bg-red-950/20 text-red-300"
89 : "border-border bg-bg-2 text-fg-2"
90 }`}
91 >
92 <div className="mb-1 text-[10px] uppercase tracking-wide text-fg-3">
93 {block.isError ? "tool error" : "tool result"}
94 </div>
95 <pre className="whitespace-pre-wrap break-all">{block.content}</pre>
96 </div>
97 );
98 }
99 }
100
101 function formatAt(iso: string): string {
102 try {
103 return new Date(iso).toLocaleString();
104 } catch {
105 return iso;
106 }
107 }