From c872e2427d323538baa803a2786b0250c20927a1 Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Tue, 26 Nov 2024 10:03:16 -0500 Subject: [PATCH] [app] Layout improvments for messages and tool calls/results --- ui/desktop/src/ChatWindow.tsx | 4 +- ui/desktop/src/components/GooseMessage.tsx | 25 +-- ui/desktop/src/components/ToolInvocation.tsx | 165 ------------------ ui/desktop/src/components/ToolInvocations.tsx | 158 +++++++++++++++++ ui/desktop/src/components/UserMessage.tsx | 6 +- .../src/fixtures/tool-calls-and-results.json | 83 +++++++++ ui/desktop/tsconfig.json | 1 + 7 files changed, 257 insertions(+), 185 deletions(-) delete mode 100644 ui/desktop/src/components/ToolInvocation.tsx create mode 100644 ui/desktop/src/components/ToolInvocations.tsx create mode 100644 ui/desktop/src/fixtures/tool-calls-and-results.json diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index 3ed08ac34..735d0b24f 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -12,7 +12,7 @@ import Tabs from './components/Tabs'; import MoreMenu from './components/MoreMenu'; import { Bird } from './components/ui/icons'; import LoadingGoose from './components/LoadingGoose'; - +// import fakeToolInvocations from './fixtures/tool-calls-and-results.json'; export interface Chat { id: number; @@ -115,6 +115,8 @@ function ChatContent({ }, }); + // const messages = fakeToolInvocations; + // Update chat messages when they change useEffect(() => { const updatedChats = chats.map((c) => diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index f2e48509f..aa73a65bb 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import ToolInvocation from './ToolInvocation'; import ReactMarkdown from 'react-markdown'; +import ToolInvocations from './ToolInvocations'; import LinkPreview from './LinkPreview'; import GooseResponseForm from './GooseResponseForm'; import { extractUrls } from '../utils/urlUtils'; @@ -22,32 +22,23 @@ export default function GooseMessage({ message, metadata, messages, append }: Go const previousUrls = previousMessage ? extractUrls(previousMessage.content) : []; const urls = !message.toolInvocations ? extractUrls(message.content, previousUrls) : []; - console.log("message", message.content) - return ( -
-
+
+
{message.toolInvocations && ( -
-
- {message.toolInvocations.map((toolInvocation) => ( - - ))} -
+
+
)} {message.content && ( -
+
{message.content}
)} {urls.length > 0 && ( -
+
{urls.map((url, index) => ( ))} @@ -55,11 +46,13 @@ export default function GooseMessage({ message, metadata, messages, append }: Go )} {metadata && ( +
+
)}
diff --git a/ui/desktop/src/components/ToolInvocation.tsx b/ui/desktop/src/components/ToolInvocation.tsx deleted file mode 100644 index 9839b84ee..000000000 --- a/ui/desktop/src/components/ToolInvocation.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react'; -import { Card } from './ui/card'; -import { BoxIcon } from './ui/icons' -import ReactMarkdown from 'react-markdown' - -export default function ToolInvocation({ toolInvocation }) { - return ( -
- - {toolInvocation.state === 'result' && } -
- ) -} - - - - -interface ToolCallProps { - call: { - state: 'call' | 'result' - toolCallId: string - toolName: string - args: Record - } -} - -function ToolCall({ call }: ToolCallProps) { - const argsMarkdownContent = convertArgsToMarkdown(call.args); - - return ( - -
- - Tool Called: {call.toolName.substring(call.toolName.lastIndexOf("__") + 2)} -
- - {call.args && ( - - {argsMarkdownContent} - - )} -
- ) -} - - - - -interface ResultItem { - text?: string - type: 'text' | 'image' - mimeType?: string - data?: string // Base64 encoded image data -} - -interface ToolResultProps { - result: { - message?: string - result?: ResultItem[] | string - state?: string - toolCallId?: string - toolName?: string - args?: any - input_todo?: any - } -} - -function ToolResult({ result }: ToolResultProps) { - if (!result || !result.result) return null - - return ( - -
-
- - Tool Result: {result.toolName.substring(result.toolName.lastIndexOf("__") + 2)} -
- {Array.isArray(result.result) ? ( -
- {result.result.map((item: ResultItem, index: number) => ( -
- {item.type === 'text' && item.text && ( - - {item.text} - - )} - {item.type === 'image' && item.data && item.mimeType && ( - Tool result { - console.error('Failed to load image: Invalid MIME-type encoded image data'); - e.currentTarget.style.display = 'none'; - }} - /> - )} -
- ))} -
- ) : ( - - {typeof children === 'string' ? children : "Unrenderable tool result - check logs"} - - ) - }, - pre({ children }) { - return
- {typeof children === 'string' ? children : "Unrenderable tool result - check logs"} -
- }, - p({ children }) { - return
- {typeof children === 'string' ? children : "Unrenderable tool result - check logs"} -
- } - }} - > - {result.result} -
- )} -
-
- ) -} - -// Utils - -const convertArgsToMarkdown = (args: Record): string => { - const lines: string[] = []; - - Object.entries(args).forEach(([key, value]) => { - // Add the parameter name as a heading - lines.push(`### ${key}`); - lines.push(''); - - // Handle different value types - if (typeof value === 'string') { - lines.push('```'); - lines.push(value); - lines.push('```'); - } else if (Array.isArray(value)) { - value.forEach((item, index) => { - lines.push(`${index + 1}. ${JSON.stringify(item)}`); - }); - } else if (typeof value === 'object' && value !== null) { - lines.push('```json'); - lines.push(JSON.stringify(value, null, 2)); - lines.push('```'); - } else { - lines.push('```'); - lines.push(String(value)); - lines.push('```'); - } - lines.push(''); - }); - - return lines.join('\n'); -}; \ No newline at end of file diff --git a/ui/desktop/src/components/ToolInvocations.tsx b/ui/desktop/src/components/ToolInvocations.tsx new file mode 100644 index 000000000..3a63e5063 --- /dev/null +++ b/ui/desktop/src/components/ToolInvocations.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { Card } from './ui/card'; +import { BoxIcon } from './ui/icons' +import ReactMarkdown from 'react-markdown' + + +export default function ToolInvocations({ toolInvocations }) { + return ( +
+ {toolInvocations.map((toolInvocation) => ( + + ))} +
+ ) +} + + + + +function ToolInvocation({ toolInvocation }) { + return ( +
+ + {toolInvocation.state === 'result' && } +
+ ) +} + + + + +interface ToolCallProps { + call: { + state: 'call' | 'result' + toolCallId: string + toolName: string + args: Record + } +} + +function ToolCall({ call }: ToolCallProps) { + const argsMarkdownContent = convertArgsToMarkdown(call.args); + + return ( + +
+ + Tool Called: {call.toolName.substring(call.toolName.lastIndexOf("__") + 2)} +
+ + {call.args && ( + + {argsMarkdownContent} + + )} +
+ ) +} + + + + +interface ResultItem { + text?: string + type: 'text' | 'image' + mimeType?: string + data?: string // Base64 encoded image data +} + +interface ToolResultProps { + result: { + message?: string + result?: ResultItem[] | string + state?: string + toolCallId?: string + toolName?: string + args?: any + input_todo?: any + } +} + +function ToolResult({ result }: ToolResultProps) { + if (!result || !result.result) return null + + return ( + +
+ + Tool Result: {result.toolName.substring(result.toolName.lastIndexOf("__") + 2)} +
+ {Array.isArray(result.result) ? ( +
+ {result.result.map((item: ResultItem, index: number) => ( +
+ {item.type === 'text' && item.text && ( + + {item.text} + + )} + {item.type === 'image' && item.data && item.mimeType && ( + Tool result { + console.error('Failed to load image: Invalid MIME-type encoded image data'); + e.currentTarget.style.display = 'none'; + }} + /> + )} +
+ ))} +
+ ) : ( + + {result.result} + + )} +
+ ) +} + +// Utils + +const convertArgsToMarkdown = (args: Record): string => { + const lines: string[] = []; + + Object.entries(args).forEach(([key, value]) => { + // Add the parameter name as a heading + lines.push(`### ${key}`); + lines.push(''); + + // Handle different value types + if (typeof value === 'string') { + lines.push('```'); + lines.push(value); + lines.push('```'); + } else if (Array.isArray(value)) { + value.forEach((item, index) => { + lines.push(`${index + 1}. ${JSON.stringify(item)}`); + }); + } else if (typeof value === 'object' && value !== null) { + lines.push('```json'); + lines.push(JSON.stringify(value, null, 2)); + lines.push('```'); + } else { + lines.push('```'); + lines.push(String(value)); + lines.push('```'); + } + lines.push(''); + }); + + return lines.join('\n'); +}; \ No newline at end of file diff --git a/ui/desktop/src/components/UserMessage.tsx b/ui/desktop/src/components/UserMessage.tsx index 226ca325a..b569ed890 100644 --- a/ui/desktop/src/components/UserMessage.tsx +++ b/ui/desktop/src/components/UserMessage.tsx @@ -9,12 +9,12 @@ export default function UserMessage({ message }) { return (
-
-
+
+
{message.content}
{urls.length > 0 && ( -
+
{urls.map((url, index) => ( ))} diff --git a/ui/desktop/src/fixtures/tool-calls-and-results.json b/ui/desktop/src/fixtures/tool-calls-and-results.json new file mode 100644 index 000000000..78d310ab0 --- /dev/null +++ b/ui/desktop/src/fixtures/tool-calls-and-results.json @@ -0,0 +1,83 @@ +[ + { + "id": "uQyUSas", + "createdAt": "2024-11-26T14:42:49.609Z", + "role": "user", + "content": "ping apple.com ten times" + }, + { + "id": "adQWt8l", + "role": "assistant", + "content": "The ping test to `apple.com` was completed. Here are the results:\n\n- **Packets transmitted**: 10\n- **Packets received**: 9\n- **Packet loss**: 10.0%\n- **Round-trip time**: \n - Minimum: 19.553 ms\n - Average: 22.795 ms\n - Maximum: 30.950 ms\n - Standard deviation: 3.245 ms\n\nOne of the packets timed out, which caused the 10% packet loss. \n", + "createdAt": "2024-11-26T14:42:49.635Z", + "toolInvocations": [ + { + "state": "result", + "args": { + "command": "ping -c 10 apple.com" + }, + "toolCallId": "call_V0eRNYxfzRDqCVwJJbpT1MJq", + "toolName": "DeveloperSystem__bash", + "result": [ + { + "audience": [ + "assistant" + ], + "text": "Finished with Status Code: exit status: 0\nOutput:\nPING apple.com (17.253.144.10): 56 data bytes\n64 bytes from 17.253.144.10: icmp_seq=0 ttl=51 time=21.033 ms\n64 bytes from 17.253.144.10: icmp_seq=1 ttl=51 time=19.828 ms\nRequest timeout for icmp_seq 2\n64 bytes from 17.253.144.10: icmp_seq=3 ttl=51 time=21.733 ms\n64 bytes from 17.253.144.10: icmp_seq=4 ttl=51 time=19.553 ms\n64 bytes from 17.253.144.10: icmp_seq=5 ttl=51 time=22.079 ms\n64 bytes from 17.253.144.10: icmp_seq=6 ttl=51 time=23.344 ms\n64 bytes from 17.253.144.10: icmp_seq=7 ttl=51 time=30.950 ms\n64 bytes from 17.253.144.10: icmp_seq=8 ttl=51 time=24.615 ms\n64 bytes from 17.253.144.10: icmp_seq=9 ttl=51 time=22.022 ms\n\n--- apple.com ping statistics ---\n10 packets transmitted, 9 packets received, 10.0% packet loss\nround-trip min/avg/max/stddev = 19.553/22.795/30.950/3.245 ms\n", + "type": "text" + } + ] + } + ], + "internalUpdateId": "dqOjjej" + }, + { + "id": "Mnqg419", + "createdAt": "2024-11-26T14:43:15.551Z", + "role": "user", + "content": "Ping apple.com and google.com ten times in parallel" + }, + { + "id": "A2MivMv", + "role": "assistant", + "content": "Here are the results of the parallel ping tests:\n\n### Apple.com\n- **Packets transmitted**: 10\n- **Packets received**: 10\n- **Packet loss**: 0.0%\n- **Round-trip time**:\n - Minimum: 18.081 ms\n - Average: 21.225 ms\n - Maximum: 26.032 ms\n - Standard deviation: 2.600 ms\n\n### Google.com\n- **Packets transmitted**: 10\n- **Packets received**: 10\n- **Packet loss**: 0.0%\n- **Round-trip time**:\n - Minimum: 19.365 ms\n - Average: 23.897 ms\n - Maximum: 29.509 ms\n - Standard deviation: 3.191 ms\n\nBoth sites showed no packet loss with low latency values.\n", + "createdAt": "2024-11-26T14:43:15.569Z", + "toolInvocations": [ + { + "state": "result", + "args": { + "command": "ping -c 10 apple.com" + }, + "toolCallId": "call_GEhhPCvqh46MFrVASCXCs3Od", + "toolName": "DeveloperSystem__bash", + "result": [ + { + "audience": [ + "assistant" + ], + "text": "Finished with Status Code: exit status: 0\nOutput:\nPING apple.com (17.253.144.10): 56 data bytes\n64 bytes from 17.253.144.10: icmp_seq=0 ttl=51 time=26.032 ms\n64 bytes from 17.253.144.10: icmp_seq=1 ttl=51 time=20.094 ms\n64 bytes from 17.253.144.10: icmp_seq=2 ttl=51 time=19.911 ms\n64 bytes from 17.253.144.10: icmp_seq=3 ttl=51 time=18.858 ms\n64 bytes from 17.253.144.10: icmp_seq=4 ttl=51 time=19.156 ms\n64 bytes from 17.253.144.10: icmp_seq=5 ttl=51 time=18.081 ms\n64 bytes from 17.253.144.10: icmp_seq=6 ttl=51 time=19.812 ms\n64 bytes from 17.253.144.10: icmp_seq=7 ttl=51 time=24.239 ms\n64 bytes from 17.253.144.10: icmp_seq=8 ttl=51 time=24.469 ms\n64 bytes from 17.253.144.10: icmp_seq=9 ttl=51 time=21.594 ms\n\n--- apple.com ping statistics ---\n10 packets transmitted, 10 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 18.081/21.225/26.032/2.600 ms\n", + "type": "text" + } + ] + }, + { + "state": "result", + "args": { + "command": "ping -c 10 google.com" + }, + "toolCallId": "call_L6IdCFMS8OYnQOj2Ybd3Oq7U", + "toolName": "DeveloperSystem__bash", + "result": [ + { + "audience": [ + "assistant" + ], + "text": "Finished with Status Code: exit status: 0\nOutput:\nPING google.com (64.233.180.102): 56 data bytes\n64 bytes from 64.233.180.102: icmp_seq=0 ttl=99 time=23.647 ms\n64 bytes from 64.233.180.102: icmp_seq=1 ttl=99 time=24.998 ms\n64 bytes from 64.233.180.102: icmp_seq=2 ttl=99 time=22.560 ms\n64 bytes from 64.233.180.102: icmp_seq=3 ttl=99 time=24.673 ms\n64 bytes from 64.233.180.102: icmp_seq=4 ttl=99 time=22.524 ms\n64 bytes from 64.233.180.102: icmp_seq=5 ttl=99 time=29.037 ms\n64 bytes from 64.233.180.102: icmp_seq=6 ttl=99 time=22.847 ms\n64 bytes from 64.233.180.102: icmp_seq=7 ttl=99 time=29.509 ms\n64 bytes from 64.233.180.102: icmp_seq=8 ttl=99 time=19.365 ms\n64 bytes from 64.233.180.102: icmp_seq=9 ttl=99 time=19.809 ms\n\n--- google.com ping statistics ---\n10 packets transmitted, 10 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 19.365/23.897/29.509/3.191 ms\n", + "type": "text" + } + ] + } + ], + "internalUpdateId": "j4KQqx1" + } +] diff --git a/ui/desktop/tsconfig.json b/ui/desktop/tsconfig.json index f3de875a0..31e53012b 100644 --- a/ui/desktop/tsconfig.json +++ b/ui/desktop/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "esModuleInterop": true, + "resolveJsonModule": true, "jsx": "react", "paths": { "@/*": ["./*"]