diff options
Diffstat (limited to 'app/packages')
| -rw-r--r-- | app/packages/web/package.json | 1 | ||||
| -rw-r--r-- | app/packages/web/src/app.tsx | 592 |
2 files changed, 466 insertions, 127 deletions
diff --git a/app/packages/web/package.json b/app/packages/web/package.json index fde040111..7d3a1674d 100644 --- a/app/packages/web/package.json +++ b/app/packages/web/package.json @@ -11,6 +11,7 @@ }, "license": "MIT", "devDependencies": { + "ai": "^5.0.0-alpha.2", "typescript": "5.8.2", "vite": "6.2.2", "vite-plugin-solid": "2.11.6" diff --git a/app/packages/web/src/app.tsx b/app/packages/web/src/app.tsx index 015d44c44..369bcfa41 100644 --- a/app/packages/web/src/app.tsx +++ b/app/packages/web/src/app.tsx @@ -1,22 +1,6 @@ -import { createSignal, onCleanup, onMount, Show, For, createMemo } from "solid-js" +import { createSignal, onCleanup, onMount, Show, For } from "solid-js" import { useParams } from "@solidjs/router" - -type MessagePart = { - type: string - text?: string - [key: string]: any -} - -type MessageContent = { - role?: string - parts?: MessagePart[] - metadata?: { - time?: { - created?: number - } - } - [key: string]: any -} +import { type UIMessage } from "ai" type Message = { key: string @@ -33,9 +17,11 @@ type SessionInfo = { export default function App() { const params = useParams<{ id: string }>() - const [messages, setMessages] = createSignal<Message[]>([]) const [connectionStatus, setConnectionStatus] = createSignal("Disconnected") const [sessionInfo, setSessionInfo] = createSignal<SessionInfo | null>(null) + const [systemMessage, setSystemMessage] = createSignal<Message | null>(null) + const [messages, setMessages] = createSignal<Message[]>([]) + const [expandedSystemMessage, setExpandedSystemMessage] = createSignal(false) onMount(() => { // Get the API URL from environment @@ -56,7 +42,7 @@ export default function App() { setConnectionStatus("Error: API URL not found") return } - + let reconnectTimer: number | undefined let socket: WebSocket | null = null @@ -88,33 +74,37 @@ export default function App() { console.log("WebSocket message received") try { const data = JSON.parse(event.data) as Message - + // Check if this is a session info message if (data.key.startsWith("session/info/")) { - try { - const infoContent = JSON.parse(data.content) as SessionInfo; - setSessionInfo(infoContent); - console.log("Session info updated:", infoContent); - } catch (e) { - console.error("Error parsing session info:", e); - } - } else { - // For all other messages - setMessages((prev) => { - // Check if message with this key already exists - const existingIndex = prev.findIndex(msg => msg.key === data.key) - - if (existingIndex >= 0) { - // Update existing message - const updated = [...prev] - updated[existingIndex] = data - return updated - } else { - // Add new message - return [...prev, data] - } - }) + const infoContent = JSON.parse(data.content) as SessionInfo + setSessionInfo(infoContent) + console.log("Session info updated:", infoContent) + return } + + // Check if it's a system message + const msgContent = JSON.parse(data.content) as UIMessage + if (msgContent.role === "system") { + setSystemMessage(data) + console.log("System message updated:", data) + return + } + + // Non-system messages + setMessages((prev) => { + // Check if message with this key already exists + const existingIndex = prev.findIndex((msg) => msg.key === data.key) + if (existingIndex >= 0) { + // Update existing message + const updated = [...prev] + updated[existingIndex] = data + return updated + } else { + // Add new message + return [...prev, data] + } + }) } catch (error) { console.error("Error parsing WebSocket message:", error) } @@ -164,37 +154,126 @@ export default function App() { </p> <h3>Live Updates</h3> - + <Show when={sessionInfo()}> - <div + <div style={{ - backgroundColor: "#f8f9fa", padding: "1rem", - borderRadius: "0.5rem", marginBottom: "1rem", - border: "1px solid #dee2e6" + border: "1px solid #dee2e6", }} > <h4 style={{ margin: "0 0 0.75rem 0" }}>Session Information</h4> <div style={{ display: "flex", gap: "1.5rem" }}> <div> - <strong>Input Tokens:</strong> {sessionInfo()?.tokens?.input || 0} + <strong>Input Tokens:</strong>{" "} + {sessionInfo()?.tokens?.input || 0} </div> <div> - <strong>Output Tokens:</strong> {sessionInfo()?.tokens?.output || 0} + <strong>Output Tokens:</strong>{" "} + {sessionInfo()?.tokens?.output || 0} </div> <div> - <strong>Reasoning Tokens:</strong> {sessionInfo()?.tokens?.reasoning || 0} + <strong>Reasoning Tokens:</strong>{" "} + {sessionInfo()?.tokens?.reasoning || 0} </div> </div> </div> </Show> - + + {/* Display system message as context in the Session Information block */} + <Show when={systemMessage()}> + <div + style={{ + padding: "1rem", + marginBottom: "1rem", + border: "1px solid #dee2e6", + }} + > + <h4 style={{ margin: "0 0 0.75rem 0" }}>Context</h4> + {(() => { + try { + const parsed = JSON.parse( + systemMessage()?.content || "", + ) as UIMessage + if ( + parsed.parts && + parsed.parts.length > 0 && + parsed.parts[0].type === "text" + ) { + const text = parsed.parts[0].text || "" + const lines = text.split("\n") + const visibleLines = expandedSystemMessage() + ? lines + : lines.slice(0, 5) + const hasMoreLines = lines.length > 5 + + return ( + <> + <div + style={{ + padding: "0.75rem", + border: "1px solid #dee2e6", + }} + > + {/* Create a modified version of the text part for the system message */} + {(() => { + // Create a modified part with truncated text + const modifiedPart = { + ...parsed.parts[0], + text: visibleLines.join("\n"), + } + + return ( + <> + <pre>{modifiedPart.text}</pre> + {hasMoreLines && !expandedSystemMessage() && ( + <div + style={{ + color: "#6c757d", + fontStyle: "italic", + marginTop: "0.5rem", + }} + > + {lines.length - 5} more lines... + </div> + )} + </> + ) + })()} + </div> + {hasMoreLines && ( + <button + onClick={() => + setExpandedSystemMessage(!expandedSystemMessage()) + } + style={{ + marginTop: "0.5rem", + padding: "0.25rem 0.75rem", + border: "1px solid #ced4da", + cursor: "pointer", + fontSize: "0.875rem", + }} + > + {expandedSystemMessage() ? "Show Less" : "Show More"} + </button> + )} + </> + ) + } + } catch (e) { + return <div>Error parsing system message</div> + } + + return null + })()} + </div> + </Show> + <div style={{ border: "1px solid #ccc", padding: "1rem", - borderRadius: "0.5rem", maxHeight: "500px", overflowY: "auto", }} @@ -210,31 +289,29 @@ export default function App() { style={{ padding: "0.75rem", margin: "0.75rem 0", - backgroundColor: "#f5f5f5", - borderRadius: "0.5rem", boxShadow: "0 1px 3px rgba(0,0,0,0.1)", }} > <div> <strong>Key:</strong> {msg.key} </div> - + {(() => { try { - const parsed = JSON.parse(msg.content) as MessageContent; - const createdTime = parsed.metadata?.time?.created - ? new Date(parsed.metadata.time.created).toLocaleString() - : 'Unknown time'; - + const parsed = JSON.parse(msg.content) as UIMessage + const createdTime = parsed.metadata?.time?.created + ? new Date( + parsed.metadata.time.created, + ).toLocaleString() + : "Unknown time" + return ( <> <div style={{ marginTop: "0.5rem" }}> <strong>Full Content:</strong> <pre style={{ - backgroundColor: "#f0f0f0", padding: "0.5rem", - borderRadius: "0.25rem", overflow: "auto", maxHeight: "150px", whiteSpace: "pre-wrap", @@ -245,91 +322,352 @@ export default function App() { {JSON.stringify(parsed, null, 2)} </pre> </div> - + {parsed.parts && parsed.parts.length > 0 && ( <div style={{ marginTop: "0.75rem" }}> - <div style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - padding: "0.25rem 0.5rem", - backgroundColor: "#e9ecef", - borderRadius: "0.25rem", - marginBottom: "0.5rem" - }}> - <strong>Role: {parsed.role || 'Unknown'}</strong> - <span style={{ fontSize: "0.8rem", color: "#6c757d" }}> + <div + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "0.25rem 0.5rem", + marginBottom: "0.5rem", + }} + > + <strong> + Role: {parsed.role || "Unknown"} + </strong> + <span + style={{ + fontSize: "0.8rem", + color: "#6c757d", + }} + > {createdTime} </span> </div> - - <div style={{ - backgroundColor: "#fff", - padding: "0.75rem", - borderRadius: "0.25rem", - border: "1px solid #dee2e6" - }}> - <For each={parsed.parts}> - {(part, index) => ( - <div style={{ marginBottom: index() < parsed.parts!.length - 1 ? "0.75rem" : "0" }}> - {part.type === "text" ? ( - <pre style={{ - whiteSpace: "pre-wrap", - wordBreak: "break-word", - fontFamily: "inherit", - margin: 0, - padding: 0, - backgroundColor: "transparent", - border: "none", - fontSize: "inherit", - overflow: "visible" - }}> - {part.text} + + <div + style={{ + padding: "0.75rem", + border: "1px solid #dee2e6", + }} + > + <For + each={parsed.parts.filter( + (part) => part.type !== "step-start", + )} + > + {(part) => { + if (part.type === "text") { + //{ + // "type": "text", + // "text": "Hello! How can I help you today?" + //} + return ( + <pre> + [{part.type}] {part.text}{" "} </pre> - ) : ( + ) + } + if (part.type === "reasoning") { + //{ + // "type": "reasoning", + // "text": "The user asked for a weather forecast. I should call the 'getWeather' tool with the location 'San Francisco'.", + // "providerMetadata": { "step_id": "reason_step_1" } + //} + return ( + <pre> + [{part.type}] {part.text} + </pre> + ) + } + if (part.type === "tool-invocation") { + return ( <div> - <div style={{ - fontSize: "0.85rem", - fontWeight: "bold", - marginBottom: "0.25rem", - color: "#495057" - }}> - Part type: {part.type} - </div> - <pre + <div style={{ - backgroundColor: "#f8f9fa", - padding: "0.5rem", - borderRadius: "0.25rem", - overflow: "auto", - maxHeight: "200px", - whiteSpace: "pre-wrap", - wordBreak: "break-word", - fontSize: "0.85rem", - margin: 0 + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "0.3rem", }} > - {JSON.stringify(part, null, 2)} - </pre> + <span> + <pre + style={{ + margin: 0, + display: "inline", + }} + > + [{part.type}] + </pre>{" "} + Tool:{" "} + <strong> + {part.toolInvocation.toolName} + </strong> + </span> + {parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ]?.time?.start && + parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ]?.time?.end && ( + <span + style={{ + color: "#6c757d", + fontSize: "0.8rem", + }} + > + {( + (new Date( + parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ].time.end, + ) - + new Date( + parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ].time.start, + )) / + 1000 + ).toFixed(2)} + s + </span> + )} + </div> + {(() => { + if ( + part.toolInvocation.state === + "partial-call" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "partial-call", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "argsTextDelta": "{\"query\":\"latest AI news" + // } + //} + return ( + <> + <pre> + { + part.toolInvocation + .argsTextDelta + } + </pre> + <span>...</span> + </> + ) + } + if ( + part.toolInvocation.state === + "call" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "call", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "args": { "query": "latest AI news", "count": 3 } + // } + //} + return ( + <pre> + {JSON.stringify( + part.toolInvocation.args, + null, + 2, + )} + </pre> + ) + } + if ( + part.toolInvocation.state === + "result" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "result", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "args": { "query": "latest AI news", "count": 3 }, + // "result": [ + // { "title": "AI SDK v5 Announced", "url": "..." }, + // { "title": "New LLM Achieves SOTA", "url": "..." } + // ] + // } + //} + return ( + <> + <pre> + {JSON.stringify( + part.toolInvocation + .args, + null, + 2, + )} + </pre> + <pre> + {JSON.stringify( + part.toolInvocation + .result, + null, + 2, + )} + </pre> + </> + ) + } + if ( + part.toolInvocation.state === + "error" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "error", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "args": { "query": "latest AI news", "count": 3 }, + // "errorMessage": "API limit exceeded for searchWeb tool." + // } + //} + return ( + <> + <pre> + {JSON.stringify( + part.toolInvocation + .args, + null, + 2, + )} + </pre> + <pre> + { + part.toolInvocation + .errorMessage + } + </pre> + </> + ) + } + })()} </div> - )} - </div> - )} + ) + } + if (part.type === "source") { + //{ + // "type": "source", + // "source": { + // "sourceType": "url", + // "id": "doc_xyz789", + // "url": "https://example.com/research-paper.pdf", + // "title": "Groundbreaking AI Research Paper" + // } + //} + return ( + <div> + <div> + <span> + <pre>[{part.type}]</pre> + </span> + <span> + Source:{" "} + {part.source.title || + part.source.id} + </span> + </div> + {part.source.url && ( + <div> + <a + href={part.source.url} + target="_blank" + rel="noopener noreferrer" + style={{ color: "#0c5460" }} + > + {part.source.url} + </a> + </div> + )} + {part.source.sourceType && ( + <div> + Type: {part.source.sourceType} + </div> + )} + </div> + ) + } + if (part.type === "file") { + //{ + // "type": "file", + // "mediaType": "image/jpeg", + // "filename": "cat_photo.jpg", + // "url": "https://example-files.com/cats/cat_photo.jpg" + //} + const isImage = + part.mediaType?.startsWith("image/") + + return ( + <div> + <div> + <span> + <pre>[{part.type}]</pre> + </span> + <span>File: {part.filename}</span> + <span>{part.mediaType}</span> + </div> + + {isImage && part.url ? ( + <div> + <img + src={part.url} + alt={ + part.filename || + "Attached image" + } + /> + </div> + ) : ( + <div> + {part.url ? ( + <a + href={part.url} + target="_blank" + rel="noopener noreferrer" + > + Download: {part.filename} + </a> + ) : ( + <div> + File attachment (no URL + available) + </div> + )} + </div> + )} + </div> + ) + } + return null + }} </For> </div> </div> )} </> - ); + ) } catch (e) { return ( <div> <strong>Content:</strong> <pre style={{ - backgroundColor: "#f0f0f0", padding: "0.5rem", - borderRadius: "0.25rem", overflow: "auto", maxHeight: "200px", whiteSpace: "pre-wrap", @@ -339,7 +677,7 @@ export default function App() { {msg.content} </pre> </div> - ); + ) } })()} </li> |
