Share.tsx 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. import { type JSX } from "solid-js"
  2. import {
  3. For,
  4. Show,
  5. Match,
  6. Switch,
  7. onMount,
  8. onCleanup,
  9. splitProps,
  10. createMemo,
  11. createEffect,
  12. createSignal,
  13. } from "solid-js"
  14. import { DateTime } from "luxon"
  15. import { IconOpenAI, IconGemini, IconAnthropic } from "./icons/custom"
  16. import {
  17. IconCpuChip,
  18. IconSparkles,
  19. IconUserCircle,
  20. IconChevronDown,
  21. IconCommandLine,
  22. IconChevronRight,
  23. IconPencilSquare,
  24. IconWrenchScrewdriver,
  25. } from "./icons"
  26. import DiffView from "./DiffView"
  27. import CodeBlock from "./CodeBlock"
  28. import styles from "./share.module.css"
  29. import { type UIMessage } from "ai"
  30. import { createStore, reconcile } from "solid-js/store"
  31. type Status =
  32. | "disconnected"
  33. | "connecting"
  34. | "connected"
  35. | "error"
  36. | "reconnecting"
  37. type SessionMessage = UIMessage<{
  38. time: {
  39. created: number
  40. completed?: number
  41. }
  42. assistant?: {
  43. modelID: string
  44. providerID: string
  45. cost: number
  46. tokens: {
  47. input: number
  48. output: number
  49. reasoning: number
  50. }
  51. }
  52. sessionID: string
  53. tool: Record<
  54. string,
  55. {
  56. [key: string]: any
  57. time: {
  58. start: number
  59. end: number
  60. }
  61. }
  62. >
  63. }>
  64. type SessionInfo = {
  65. title: string
  66. cost?: number
  67. }
  68. function getFileType(path: string) {
  69. return path.split(".").pop()
  70. }
  71. // Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]`
  72. function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
  73. const entries: Array<[string, any]> = []
  74. for (const [key, value] of Object.entries(obj)) {
  75. const path = prefix ? `${prefix}.${key}` : key
  76. if (value !== null && typeof value === "object" && !Array.isArray(value)) {
  77. entries.push(...flattenToolArgs(value, path))
  78. } else {
  79. entries.push([path, value])
  80. }
  81. }
  82. return entries
  83. }
  84. function getStatusText(status: [Status, string?]): string {
  85. switch (status[0]) {
  86. case "connected":
  87. return "Connected"
  88. case "connecting":
  89. return "Connecting..."
  90. case "disconnected":
  91. return "Disconnected"
  92. case "reconnecting":
  93. return "Reconnecting..."
  94. case "error":
  95. return status[1] || "Error"
  96. default:
  97. return "Unknown"
  98. }
  99. }
  100. function ProviderIcon(props: { provider: string; size?: number }) {
  101. const size = props.size || 16
  102. return (
  103. <Switch fallback={<IconSparkles width={size} height={size} />}>
  104. <Match when={props.provider === "openai"}>
  105. <IconOpenAI width={size} height={size} />
  106. </Match>
  107. <Match when={props.provider === "anthropic"}>
  108. <IconAnthropic width={size} height={size} />
  109. </Match>
  110. <Match when={props.provider === "gemini"}>
  111. <IconGemini width={size} height={size} />
  112. </Match>
  113. </Switch>
  114. )
  115. }
  116. interface ResultsButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
  117. results: boolean
  118. }
  119. function ResultsButton(props: ResultsButtonProps) {
  120. const [local, rest] = splitProps(props, ["results"])
  121. return (
  122. <button
  123. type="button"
  124. data-element-button-text
  125. data-element-button-more
  126. {...rest}
  127. >
  128. <span>{local.results ? "Hide results" : "Show results"}</span>
  129. <span data-button-icon>
  130. <Show
  131. when={local.results}
  132. fallback={<IconChevronRight width={10} height={10} />}
  133. >
  134. <IconChevronDown width={10} height={10} />
  135. </Show>
  136. </span>
  137. </button>
  138. )
  139. }
  140. interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
  141. text: string
  142. expand?: boolean
  143. highlight?: boolean
  144. }
  145. function TextPart(props: TextPartProps) {
  146. const [local, rest] = splitProps(props, ["text", "expand", "highlight"])
  147. const [expanded, setExpanded] = createSignal(false)
  148. const [overflowed, setOverflowed] = createSignal(false)
  149. let preEl: HTMLPreElement | undefined
  150. function checkOverflow() {
  151. if (preEl && !local.expand) {
  152. setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
  153. }
  154. }
  155. onMount(() => {
  156. checkOverflow()
  157. window.addEventListener("resize", checkOverflow)
  158. })
  159. createEffect(() => {
  160. local.text
  161. setTimeout(checkOverflow, 0)
  162. })
  163. onCleanup(() => {
  164. window.removeEventListener("resize", checkOverflow)
  165. })
  166. return (
  167. <div
  168. data-element-message-text
  169. data-highlight={local.highlight}
  170. data-expanded={expanded() || local.expand === true}
  171. {...rest}
  172. >
  173. <pre ref={(el) => (preEl = el)}>{local.text}</pre>
  174. {((!local.expand && overflowed()) || expanded()) && (
  175. <button
  176. type="button"
  177. data-element-button-text
  178. onClick={() => setExpanded((e) => !e)}
  179. >
  180. {expanded() ? "Show less" : "Show more"}
  181. </button>
  182. )}
  183. </div>
  184. )
  185. }
  186. interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
  187. text: string
  188. expand?: boolean
  189. }
  190. function TerminalPart(props: TerminalPartProps) {
  191. const [local, rest] = splitProps(props, ["text", "expand"])
  192. const [expanded, setExpanded] = createSignal(false)
  193. const [overflowed, setOverflowed] = createSignal(false)
  194. let preEl: HTMLElement | undefined
  195. function checkOverflow() {
  196. if (!preEl) return
  197. const code = preEl.getElementsByTagName("code")[0]
  198. if (code && !local.expand) {
  199. console.log(preEl.clientHeight, code.offsetHeight)
  200. setOverflowed(preEl.clientHeight < code.offsetHeight)
  201. }
  202. }
  203. onMount(() => {
  204. window.addEventListener("resize", checkOverflow)
  205. })
  206. onCleanup(() => {
  207. window.removeEventListener("resize", checkOverflow)
  208. })
  209. return (
  210. <div
  211. data-element-message-terminal
  212. data-expanded={expanded() || local.expand === true}
  213. {...rest}
  214. >
  215. <div data-section="body">
  216. <div data-section="header"></div>
  217. <div data-section="content">
  218. <CodeBlock
  219. lang="ansi"
  220. onRendered={checkOverflow}
  221. ref={(el) => (preEl = el)}
  222. code={`\x1b[90m>\x1b[0m ${local.text}`}
  223. />
  224. </div>
  225. </div>
  226. {((!local.expand && overflowed()) || expanded()) && (
  227. <button
  228. type="button"
  229. data-element-button-text
  230. onClick={() => setExpanded((e) => !e)}
  231. >
  232. {expanded() ? "Show less" : "Show more"}
  233. </button>
  234. )}
  235. </div>
  236. )
  237. }
  238. function PartFooter(props: { time: number }) {
  239. return (
  240. <span
  241. data-part-footer
  242. title={DateTime.fromMillis(props.time).toLocaleString(
  243. DateTime.DATETIME_FULL_WITH_SECONDS,
  244. )}
  245. >
  246. {DateTime.fromMillis(props.time).toLocaleString(
  247. DateTime.TIME_WITH_SECONDS,
  248. )}
  249. </span>
  250. )
  251. }
  252. export default function Share(props: { api: string }) {
  253. let params = new URLSearchParams(document.location.search)
  254. const id = params.get("id")
  255. const [store, setStore] = createStore<{
  256. info?: SessionInfo
  257. messages: Record<string, SessionMessage>
  258. }>({
  259. messages: {},
  260. })
  261. const messages = createMemo(() =>
  262. Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)),
  263. )
  264. const [connectionStatus, setConnectionStatus] = createSignal<
  265. [Status, string?]
  266. >(["disconnected", "Disconnected"])
  267. onMount(() => {
  268. const apiUrl = props.api
  269. if (!id) {
  270. setConnectionStatus(["error", "id not found"])
  271. return
  272. }
  273. if (!apiUrl) {
  274. console.error("API URL not found in environment variables")
  275. setConnectionStatus(["error", "API URL not found"])
  276. return
  277. }
  278. let reconnectTimer: number | undefined
  279. let socket: WebSocket | null = null
  280. // Function to create and set up WebSocket with auto-reconnect
  281. const setupWebSocket = () => {
  282. // Close any existing connection
  283. if (socket) {
  284. socket.close()
  285. }
  286. setConnectionStatus(["connecting"])
  287. // Always use secure WebSocket protocol (wss)
  288. const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://")
  289. const wsUrl = `${wsBaseUrl}/share_poll?id=${id}`
  290. console.log("Connecting to WebSocket URL:", wsUrl)
  291. // Create WebSocket connection
  292. socket = new WebSocket(wsUrl)
  293. // Handle connection opening
  294. socket.onopen = () => {
  295. setConnectionStatus(["connected"])
  296. console.log("WebSocket connection established")
  297. }
  298. // Handle incoming messages
  299. socket.onmessage = (event) => {
  300. console.log("WebSocket message received")
  301. try {
  302. const data = JSON.parse(event.data)
  303. const [root, type, ...splits] = data.key.split("/")
  304. if (root !== "session") return
  305. if (type === "info") {
  306. setStore("info", reconcile(data.content))
  307. return
  308. }
  309. if (type === "message") {
  310. const [, messageID] = splits
  311. setStore("messages", messageID, reconcile(data.content))
  312. }
  313. } catch (error) {
  314. console.error("Error parsing WebSocket message:", error)
  315. }
  316. }
  317. // Handle errors
  318. socket.onerror = (error) => {
  319. console.error("WebSocket error:", error)
  320. setConnectionStatus(["error", "Connection failed"])
  321. }
  322. // Handle connection close and reconnection
  323. socket.onclose = (event) => {
  324. console.log(`WebSocket closed: ${event.code} ${event.reason}`)
  325. setConnectionStatus(["reconnecting"])
  326. // Try to reconnect after 2 seconds
  327. clearTimeout(reconnectTimer)
  328. reconnectTimer = window.setTimeout(
  329. setupWebSocket,
  330. 2000,
  331. ) as unknown as number
  332. }
  333. }
  334. // Initial connection
  335. setupWebSocket()
  336. // Clean up on component unmount
  337. onCleanup(() => {
  338. console.log("Cleaning up WebSocket connection")
  339. if (socket) {
  340. socket.close()
  341. }
  342. clearTimeout(reconnectTimer)
  343. })
  344. })
  345. const models = createMemo(() => {
  346. const result: string[][] = []
  347. for (const msg of messages()) {
  348. if (msg.role === "assistant" && msg.metadata?.assistant) {
  349. result.push([
  350. msg.metadata.assistant.providerID,
  351. msg.metadata.assistant.modelID,
  352. ])
  353. }
  354. }
  355. return result
  356. })
  357. const metrics = createMemo(() => {
  358. const result = {
  359. cost: 0,
  360. tokens: {
  361. input: 0,
  362. output: 0,
  363. reasoning: 0,
  364. },
  365. }
  366. for (const msg of messages()) {
  367. const assistant = msg.metadata?.assistant
  368. if (!assistant) continue
  369. result.cost += assistant.cost
  370. result.tokens.input += assistant.tokens.input
  371. result.tokens.output += assistant.tokens.output
  372. result.tokens.reasoning += assistant.tokens.reasoning
  373. }
  374. return result
  375. })
  376. return (
  377. <main class={`${styles.root} not-content`}>
  378. <div class={styles.header}>
  379. <div data-section="title">
  380. <h1>{store.info?.title}</h1>
  381. <p>
  382. <span data-status={connectionStatus()[0]}>&#9679;</span>
  383. <span data-element-label>{getStatusText(connectionStatus())}</span>
  384. </p>
  385. </div>
  386. <div data-section="row">
  387. <ul data-section="stats">
  388. <li>
  389. <span data-element-label>Cost</span>
  390. {metrics().cost !== undefined ? (
  391. <span>${metrics().cost.toFixed(2)}</span>
  392. ) : (
  393. <span data-placeholder>&mdash;</span>
  394. )}
  395. </li>
  396. <li>
  397. <span data-element-label>Input Tokens</span>
  398. {metrics().tokens.input ? (
  399. <span>{metrics().tokens.input}</span>
  400. ) : (
  401. <span data-placeholder>&mdash;</span>
  402. )}
  403. </li>
  404. <li>
  405. <span data-element-label>Output Tokens</span>
  406. {metrics().tokens.output ? (
  407. <span>{metrics().tokens.output}</span>
  408. ) : (
  409. <span data-placeholder>&mdash;</span>
  410. )}
  411. </li>
  412. <li>
  413. <span data-element-label>Reasoning Tokens</span>
  414. {metrics().tokens.reasoning ? (
  415. <span>{metrics().tokens.reasoning}</span>
  416. ) : (
  417. <span data-placeholder>&mdash;</span>
  418. )}
  419. </li>
  420. </ul>
  421. <ul data-section="stats" data-section-models>
  422. {models().length > 0 ? (
  423. <For each={Array.from(models())}>
  424. {([provider, model]) => (
  425. <li>
  426. <div data-stat-model-icon title={provider}>
  427. <ProviderIcon provider={provider} />
  428. </div>
  429. <span data-stat-model>{model}</span>
  430. </li>
  431. )}
  432. </For>
  433. ) : (
  434. <li>
  435. <span data-element-label>Models</span>
  436. <span data-placeholder>&mdash;</span>
  437. </li>
  438. )}
  439. </ul>
  440. <div data-section="date">
  441. {messages().length > 0 && messages()[0].metadata?.time.created ? (
  442. <span
  443. title={DateTime.fromMillis(
  444. messages()[0].metadata?.time.created || 0,
  445. ).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
  446. >
  447. {DateTime.fromMillis(
  448. messages()[0].metadata?.time.created || 0,
  449. ).toLocaleString(DateTime.DATE_MED)}
  450. </span>
  451. ) : (
  452. <span data-element-label data-placeholder>
  453. Started at &mdash;
  454. </span>
  455. )}
  456. </div>
  457. </div>
  458. </div>
  459. <div>
  460. <Show
  461. when={messages().length > 0}
  462. fallback={<p>Waiting for messages...</p>}
  463. >
  464. <div class={styles.parts}>
  465. <For each={messages()}>
  466. {(msg, msgIndex) => (
  467. <For each={msg.parts}>
  468. {(part, partIndex) => {
  469. if (
  470. part.type === "step-start" &&
  471. (partIndex() > 0 || !msg.metadata?.assistant)
  472. )
  473. return null
  474. const [results, showResults] = createSignal(false)
  475. const isLastPart = createMemo(
  476. () =>
  477. messages().length === msgIndex() + 1 &&
  478. msg.parts.length === partIndex() + 1,
  479. )
  480. const time =
  481. msg.metadata?.time.completed ||
  482. msg.metadata?.time.created ||
  483. 0
  484. return (
  485. <Switch>
  486. {/* User text */}
  487. <Match
  488. when={
  489. msg.role === "user" && part.type === "text" && part
  490. }
  491. >
  492. {(part) => (
  493. <div data-section="part" data-part-type="user-text">
  494. <div data-section="decoration">
  495. <div title="Message">
  496. <IconUserCircle width={18} height={18} />
  497. </div>
  498. <div></div>
  499. </div>
  500. <div data-section="content">
  501. <TextPart
  502. highlight
  503. text={part().text}
  504. expand={isLastPart()}
  505. />
  506. <PartFooter time={time} />
  507. </div>
  508. </div>
  509. )}
  510. </Match>
  511. {/* AI text */}
  512. <Match
  513. when={
  514. msg.role === "assistant" &&
  515. part.type === "text" &&
  516. part
  517. }
  518. >
  519. {(part) => (
  520. <div data-section="part" data-part-type="ai-text">
  521. <div data-section="decoration">
  522. <div title="AI response">
  523. <IconSparkles width={18} height={18} />
  524. </div>
  525. <div></div>
  526. </div>
  527. <div data-section="content">
  528. <TextPart
  529. text={part().text}
  530. expand={isLastPart()}
  531. />
  532. <PartFooter time={time} />
  533. </div>
  534. </div>
  535. )}
  536. </Match>
  537. {/* AI model */}
  538. <Match
  539. when={
  540. msg.role === "assistant" &&
  541. part.type === "step-start" &&
  542. msg.metadata?.assistant
  543. }
  544. >
  545. {(assistant) => (
  546. <div data-section="part" data-part-type="ai-model">
  547. <div data-section="decoration">
  548. <div>
  549. <ProviderIcon
  550. size={18}
  551. provider={assistant().providerID}
  552. />
  553. </div>
  554. <div></div>
  555. </div>
  556. <div data-section="content">
  557. <div data-part-tool-body>
  558. <span
  559. data-size="md"
  560. data-part-title
  561. data-element-label
  562. >
  563. {assistant().providerID}
  564. </span>
  565. <span data-part-model>
  566. {assistant().modelID}
  567. </span>
  568. </div>
  569. </div>
  570. </div>
  571. )}
  572. </Match>
  573. {/* System text */}
  574. <Match
  575. when={
  576. msg.role === "system" &&
  577. part.type === "text" &&
  578. part
  579. }
  580. >
  581. {(part) => (
  582. <div
  583. data-section="part"
  584. data-part-type="system-text"
  585. >
  586. <div data-section="decoration">
  587. <div title="System message">
  588. <IconCpuChip width={18} height={18} />
  589. </div>
  590. <div></div>
  591. </div>
  592. <div data-section="content">
  593. <div data-part-tool-body>
  594. <span data-element-label data-part-title>
  595. System
  596. </span>
  597. <TextPart
  598. data-size="sm"
  599. text={part().text}
  600. data-color="dimmed"
  601. />
  602. </div>
  603. <PartFooter time={time} />
  604. </div>
  605. </div>
  606. )}
  607. </Match>
  608. {/* Edit tool */}
  609. <Match
  610. when={
  611. msg.role === "assistant" &&
  612. part.type === "tool-invocation" &&
  613. part.toolInvocation.toolName === "opencode_edit" &&
  614. part
  615. }
  616. >
  617. {(part) => {
  618. const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
  619. const args = part().toolInvocation.args
  620. const filePath = args.filePath
  621. return (
  622. <div
  623. data-section="part"
  624. data-part-type="tool-edit"
  625. >
  626. <div data-section="decoration">
  627. <div title="Edit file">
  628. <IconPencilSquare width={18} height={18} />
  629. </div>
  630. <div></div>
  631. </div>
  632. <div data-section="content">
  633. <div data-part-tool-body>
  634. <span data-part-title data-size="md">
  635. <span data-element-label>Edit</span>
  636. <b>{filePath}</b>
  637. </span>
  638. <div data-part-tool-edit>
  639. <DiffView
  640. class={styles["diff-code-block"]}
  641. changes={metadata()?.changes || []}
  642. lang={getFileType(filePath)}
  643. />
  644. </div>
  645. </div>
  646. <PartFooter time={time} />
  647. </div>
  648. </div>
  649. )
  650. }}
  651. </Match>
  652. {/* Bash tool */}
  653. <Match
  654. when={
  655. msg.role === "assistant" &&
  656. part.type === "tool-invocation" &&
  657. part.toolInvocation.toolName === "opencode_bash" &&
  658. part
  659. }
  660. >
  661. {(part) => {
  662. const id = part().toolInvocation.toolCallId
  663. const command = part().toolInvocation.args.command
  664. const stdout = msg.metadata?.tool[id]?.stdout
  665. const result = stdout || (part().toolInvocation.state === "result" && part().toolInvocation.result)
  666. return (
  667. <div
  668. data-section="part"
  669. data-part-type="tool-edit"
  670. >
  671. <div data-section="decoration">
  672. <div title="Bash command">
  673. <IconCommandLine width={18} height={18} />
  674. </div>
  675. <div></div>
  676. </div>
  677. <div data-section="content">
  678. <div data-part-tool-body>
  679. <TerminalPart
  680. data-size="sm"
  681. text={command + (result ? `\n${result}` : "")}
  682. />
  683. </div>
  684. <PartFooter time={time} />
  685. </div>
  686. </div>
  687. )
  688. }}
  689. </Match>
  690. {/* Tool call */}
  691. <Match
  692. when={
  693. msg.role === "assistant" &&
  694. part.type === "tool-invocation" &&
  695. part
  696. }
  697. >
  698. {(part) => (
  699. <div
  700. data-section="part"
  701. data-part-type="tool-fallback"
  702. >
  703. <div data-section="decoration">
  704. <div title="Tool call">
  705. <IconWrenchScrewdriver
  706. width={18}
  707. height={18}
  708. />
  709. </div>
  710. <div></div>
  711. </div>
  712. <div data-section="content">
  713. <div data-part-tool-body>
  714. <span data-part-title data-size="md">
  715. {part().toolInvocation.toolName}
  716. </span>
  717. <div data-part-tool-args>
  718. <For
  719. each={flattenToolArgs(
  720. part().toolInvocation.args,
  721. )}
  722. >
  723. {([name, value]) => (
  724. <>
  725. <div></div>
  726. <div>{name}</div>
  727. <div>{value}</div>
  728. </>
  729. )}
  730. </For>
  731. </div>
  732. <Switch>
  733. <Match
  734. when={
  735. part().toolInvocation.state ===
  736. "result" &&
  737. part().toolInvocation.result
  738. }
  739. >
  740. <div data-part-tool-result>
  741. <ResultsButton
  742. results={results()}
  743. onClick={() => showResults((e) => !e)}
  744. />
  745. <Show when={results()}>
  746. <TextPart
  747. expand
  748. data-size="sm"
  749. data-color="dimmed"
  750. text={part().toolInvocation.result}
  751. />
  752. </Show>
  753. </div>
  754. </Match>
  755. <Match
  756. when={
  757. part().toolInvocation.state === "call"
  758. }
  759. >
  760. <TextPart
  761. data-size="sm"
  762. data-color="dimmed"
  763. text="Calling..."
  764. />
  765. </Match>
  766. </Switch>
  767. </div>
  768. <PartFooter time={time} />
  769. </div>
  770. </div>
  771. )}
  772. </Match>
  773. {/* Fallback */}
  774. <Match when={true}>
  775. <div data-section="part" data-part-type="fallback">
  776. <div data-section="decoration">
  777. <div>
  778. <Switch
  779. fallback={
  780. <IconWrenchScrewdriver
  781. width={16}
  782. height={16}
  783. />
  784. }
  785. >
  786. <Match
  787. when={
  788. msg.role === "assistant" &&
  789. part.type !== "tool-invocation"
  790. }
  791. >
  792. <IconSparkles width={18} height={18} />
  793. </Match>
  794. <Match when={msg.role === "system"}>
  795. <IconCpuChip width={18} height={18} />
  796. </Match>
  797. <Match when={msg.role === "user"}>
  798. <IconUserCircle width={18} height={18} />
  799. </Match>
  800. </Switch>
  801. </div>
  802. <div></div>
  803. </div>
  804. <div data-section="content">
  805. <div data-part-tool-body>
  806. <span data-element-label data-part-title>
  807. {part.type}
  808. </span>
  809. <TextPart
  810. text={JSON.stringify(part, null, 2)}
  811. />
  812. </div>
  813. <PartFooter time={time} />
  814. </div>
  815. </div>
  816. </Match>
  817. </Switch>
  818. )
  819. }}
  820. </For>
  821. )}
  822. </For>
  823. </div>
  824. </Show>
  825. </div>
  826. <div style={{ margin: "2rem 0" }}>
  827. <div
  828. style={{
  829. border: "1px solid #ccc",
  830. padding: "1rem",
  831. "overflow-y": "auto",
  832. }}
  833. >
  834. <Show
  835. when={messages().length > 0}
  836. fallback={<p>Waiting for messages...</p>}
  837. >
  838. <ul style={{ "list-style-type": "none", padding: 0 }}>
  839. <For each={messages()}>
  840. {(msg) => (
  841. <li
  842. style={{
  843. padding: "0.75rem",
  844. margin: "0.75rem 0",
  845. "box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
  846. }}
  847. >
  848. <div>
  849. <strong>Key:</strong> {msg.id}
  850. </div>
  851. <pre>{JSON.stringify(msg, null, 2)}</pre>
  852. </li>
  853. )}
  854. </For>
  855. </ul>
  856. </Show>
  857. </div>
  858. </div>
  859. </main>
  860. )
  861. }