summaryrefslogtreecommitdiffhomepage
path: root/app/packages
diff options
context:
space:
mode:
Diffstat (limited to 'app/packages')
-rw-r--r--app/packages/web/package.json1
-rw-r--r--app/packages/web/src/app.tsx592
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>