import { type JSX } from "solid-js" import { For, Show, Match, Switch, onMount, onCleanup, splitProps, createMemo, createEffect, createSignal, } from "solid-js" import { DateTime } from "luxon" import { IconOpenAI, IconGemini, IconAnthropic } from "./icons/custom" import { IconCpuChip, IconSparkles, IconUserCircle, IconChevronDown, IconCommandLine, IconChevronRight, IconPencilSquare, IconRectangleStack, IconMagnifyingGlass, IconWrenchScrewdriver, IconDocumentArrowDown, IconDocumentMagnifyingGlass, } from "./icons" import DiffView from "./DiffView" import CodeBlock from "./CodeBlock" import styles from "./share.module.css" import { type UIMessage } from "ai" import { createStore, reconcile } from "solid-js/store" const MIN_DURATION = 2 type Status = | "disconnected" | "connecting" | "connected" | "error" | "reconnecting" type SessionMessage = UIMessage<{ time: { created: number completed?: number } assistant?: { modelID: string providerID: string cost: number tokens: { input: number output: number reasoning: number } } sessionID: string tool: Record< string, { [key: string]: any time: { start: number end: number } } > }> type SessionInfo = { title: string cost?: number } function getFileType(path: string) { return path.split(".").pop() } function formatDuration(ms: number): string { const ONE_SECOND = 1000 const ONE_MINUTE = 60 * ONE_SECOND if (ms >= ONE_MINUTE) { const minutes = Math.floor(ms / ONE_MINUTE) return minutes === 1 ? `1min` : `${minutes}mins` } if (ms >= ONE_SECOND) { const seconds = Math.floor(ms / ONE_SECOND) return `${seconds}s` } return `${ms}ms` } // Converts nested objects/arrays into [path, value] pairs. // E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]] function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { const entries: Array<[string, any]> = [] for (const [key, value] of Object.entries(obj)) { const path = prefix ? `${prefix}.${key}` : key if (value !== null && typeof value === "object") { if (Array.isArray(value)) { value.forEach((item, index) => { const arrayPath = `${path}[${index}]` if (item !== null && typeof item === "object") { entries.push(...flattenToolArgs(item, arrayPath)) } else { entries.push([arrayPath, item]) } }) } else { entries.push(...flattenToolArgs(value, path)) } } else { entries.push([path, value]) } } return entries } function getStatusText(status: [Status, string?]): string { switch (status[0]) { case "connected": return "Connected" case "connecting": return "Connecting..." case "disconnected": return "Disconnected" case "reconnecting": return "Reconnecting..." case "error": return status[1] || "Error" default: return "Unknown" } } function ProviderIcon(props: { provider: string; size?: number }) { const size = props.size || 16 return ( }> ) } interface ResultsButtonProps extends JSX.HTMLAttributes { showCopy?: string hideCopy?: string results: boolean } function ResultsButton(props: ResultsButtonProps) { const [local, rest] = splitProps(props, ["results", "showCopy", "hideCopy"]) return ( ) } interface TextPartProps extends JSX.HTMLAttributes { text: string expand?: boolean highlight?: boolean } function TextPart(props: TextPartProps) { const [local, rest] = splitProps(props, ["text", "expand", "highlight"]) const [expanded, setExpanded] = createSignal(false) const [overflowed, setOverflowed] = createSignal(false) let preEl: HTMLPreElement | undefined function checkOverflow() { if (preEl && !local.expand) { setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1) } } onMount(() => { checkOverflow() window.addEventListener("resize", checkOverflow) }) createEffect(() => { local.text setTimeout(checkOverflow, 0) }) onCleanup(() => { window.removeEventListener("resize", checkOverflow) }) return (
 (preEl = el)}>{local.text}
{((!local.expand && overflowed()) || expanded()) && ( )}
) } interface TerminalPartProps extends JSX.HTMLAttributes { text: string desc?: string expand?: boolean } function TerminalPart(props: TerminalPartProps) { const [local, rest] = splitProps(props, ["text", "desc", "expand"]) const [expanded, setExpanded] = createSignal(false) const [overflowed, setOverflowed] = createSignal(false) let preEl: HTMLElement | undefined function checkOverflow() { if (!preEl) return const code = preEl.getElementsByTagName("code")[0] if (code && !local.expand) { console.log(preEl.clientHeight, code.offsetHeight) setOverflowed(preEl.clientHeight < code.offsetHeight) } } onMount(() => { window.addEventListener("resize", checkOverflow) }) onCleanup(() => { window.removeEventListener("resize", checkOverflow) }) return (
{local.desc}
(preEl = el)} code={`\x1b[90m>\x1b[0m ${local.text}`} />
{((!local.expand && overflowed()) || expanded()) && ( )}
) } function ToolFooter(props: { time: number }) { return ( props.time > MIN_DURATION ? {formatDuration(props.time)} :
) } export default function Share(props: { api: string }) { let params = new URLSearchParams(document.location.search) const id = params.get("id") const [store, setStore] = createStore<{ info?: SessionInfo messages: Record }>({ messages: {}, }) const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)), ) const [connectionStatus, setConnectionStatus] = createSignal< [Status, string?] >(["disconnected", "Disconnected"]) onMount(() => { const apiUrl = props.api if (!id) { setConnectionStatus(["error", "id not found"]) return } if (!apiUrl) { console.error("API URL not found in environment variables") setConnectionStatus(["error", "API URL not found"]) return } let reconnectTimer: number | undefined let socket: WebSocket | null = null // Function to create and set up WebSocket with auto-reconnect const setupWebSocket = () => { // Close any existing connection if (socket) { socket.close() } setConnectionStatus(["connecting"]) // Always use secure WebSocket protocol (wss) const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://") const wsUrl = `${wsBaseUrl}/share_poll?id=${id}` console.log("Connecting to WebSocket URL:", wsUrl) // Create WebSocket connection socket = new WebSocket(wsUrl) // Handle connection opening socket.onopen = () => { setConnectionStatus(["connected"]) console.log("WebSocket connection established") } // Handle incoming messages socket.onmessage = (event) => { console.log("WebSocket message received") try { const data = JSON.parse(event.data) const [root, type, ...splits] = data.key.split("/") if (root !== "session") return if (type === "info") { setStore("info", reconcile(data.content)) return } if (type === "message") { const [, messageID] = splits setStore("messages", messageID, reconcile(data.content)) } } catch (error) { console.error("Error parsing WebSocket message:", error) } } // Handle errors socket.onerror = (error) => { console.error("WebSocket error:", error) setConnectionStatus(["error", "Connection failed"]) } // Handle connection close and reconnection socket.onclose = (event) => { console.log(`WebSocket closed: ${event.code} ${event.reason}`) setConnectionStatus(["reconnecting"]) // Try to reconnect after 2 seconds clearTimeout(reconnectTimer) reconnectTimer = window.setTimeout( setupWebSocket, 2000, ) as unknown as number } } // Initial connection setupWebSocket() // Clean up on component unmount onCleanup(() => { console.log("Cleaning up WebSocket connection") if (socket) { socket.close() } clearTimeout(reconnectTimer) }) }) const data = createMemo(() => { const result = { created: undefined as number | undefined, system: [] as string[], messages: [] as SessionMessage[], models: [] as string[][], cost: 0, tokens: { input: 0, output: 0, reasoning: 0, }, } for (let i = 0; i < messages().length; i++) { const msg = messages()[i] const system = i === 0 && msg.role === "system" const assistant = msg.metadata?.assistant if (system) { for (const part of msg.parts) { if (part.type === "text") { result.system.push(part.text) } } result.created = msg.metadata?.time.created continue } result.messages.push(msg) if (assistant) { result.cost += assistant.cost result.tokens.input += assistant.tokens.input result.tokens.output += assistant.tokens.output result.tokens.reasoning += assistant.tokens.reasoning result.models.push([ assistant.providerID, assistant.modelID, ]) } } return result }) const [showingSystemPrompt, showSystemPrompt] = createSignal(false) return (

{store.info?.title}

{data().created ? ( {DateTime.fromMillis( data().created || 0, ).toLocaleString(DateTime.DATE_MED)} ) : ( Started at — )}

{getStatusText(connectionStatus())}

  • Cost {data().cost !== undefined ? ( ${data().cost.toFixed(2)} ) : ( )}
  • Input Tokens {data().tokens.input ? ( {data().tokens.input} ) : ( )}
  • Output Tokens {data().tokens.output ? ( {data().tokens.output} ) : ( )}
  • Reasoning Tokens {data().tokens.reasoning ? ( {data().tokens.reasoning} ) : ( )}
    {data().models.length > 0 ? ( {([provider, model]) => (
  • {model}
  • )}
    ) : (
  • Models
  • )}
0} fallback={

Waiting for messages...

} >
{(msg, msgIndex) => ( {(part, partIndex) => { if ( part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant) ) return null const [results, showResults] = createSignal(false) const isLastPart = createMemo( () => data().messages.length === msgIndex() + 1 && msg.parts.length === partIndex() + 1, ) return ( {/* User text */} {(part) => (
)}
{/* AI text */} {(part) => (
)}
{/* AI model */} {(assistant) => (
{assistant().providerID} {assistant().modelID}
)}
{/* System text */} {(part) => (
System
)}
{/* Grep tool */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId] ) const args = part().toolInvocation.args const result = part().toolInvocation.state === "result" && part().toolInvocation.result const matches = metadata()?.matches const { pattern, ...rest } = args const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
Grep “{pattern}” 0}>
{([name, value]) => ( <>
{name}
{value}
)}
0}>
showResults((e) => !e)} />
) }}
{/* Glob tool */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId] ) const args = part().toolInvocation.args const result = part().toolInvocation.state === "result" && part().toolInvocation.result const count = metadata()?.count const pattern = args.pattern const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
Glob “{pattern}” 0}>
showResults((e) => !e)} />
) }}
{/* LS tool */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId] ) const args = part().toolInvocation.args const path = args.path const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
LS {path}
showResults((e) => !e)} />
) }}
{/* Read tool */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId]) const args = part().toolInvocation.args const filePath = args.filePath const hasError = metadata()?.error const preview = metadata()?.preview const result = part().toolInvocation.state === "result" && part().toolInvocation.result const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
Read {filePath}
showResults((e) => !e)} />
showResults((e) => !e)} />
) }}
{/* Edit tool */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId]) const args = part().toolInvocation.args const filePath = args.filePath const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
Edit {filePath}
) }}
{/* Bash tool */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId]) const command = part().toolInvocation.args.command const desc = part().toolInvocation.args.description const stdout = metadata()?.stdout const result = stdout || (part().toolInvocation.state === "result" && part().toolInvocation.result) const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
) }}
{/* Tool call */} {(part) => { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId]) const duration = createMemo(() => DateTime.fromMillis(metadata()?.time.end || 0).diff( DateTime.fromMillis(metadata()?.time.start || 0), ).toMillis(), ) return (
{part().toolInvocation.toolName}
{([name, value]) => ( <>
{name}
{value}
)}
showResults((e) => !e)} />
) }}
{/* Fallback */}
} >
{part.type}
) }}
)}
0} fallback={

Waiting for messages...

} >
    {(msg) => (
  • Key: {msg.id}
    {JSON.stringify(msg, null, 2)}
  • )}
) }