From beb5034c4cfcc5ec7a110c06260b39d0b599c8f3 Mon Sep 17 00:00:00 2001 From: john Date: Tue, 18 Mar 2025 11:54:05 +0800 Subject: [PATCH 1/3] feat: inline tool_calls and tool_result collapse --- package-lock.json | 133 ++++++++++++++++++++++++++++++++ package.json | 1 + src/views/Chat/ChatMessages.tsx | 5 -- src/views/Chat/Message.tsx | 68 ++++++++-------- src/views/Chat/ToolPanel.tsx | 6 +- src/views/Chat/index.tsx | 21 ++--- 6 files changed, 179 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6553749..fb01fb13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "react-router-dom": "^6.28.1", "react-syntax-highlighter": "^15.6.1", "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "semver": "^7.7.1", @@ -12372,6 +12373,46 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.3.tgz", @@ -12444,6 +12485,64 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-to-text": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", @@ -12588,6 +12687,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -18548,6 +18657,30 @@ "@types/unist": "*" } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", diff --git a/package.json b/package.json index 1d1d276f..9130a45f 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "react-router-dom": "^6.28.1", "react-syntax-highlighter": "^15.6.1", "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "semver": "^7.7.1", diff --git a/src/views/Chat/ChatMessages.tsx b/src/views/Chat/ChatMessages.tsx index b7cf4fe1..3f039e38 100644 --- a/src/views/Chat/ChatMessages.tsx +++ b/src/views/Chat/ChatMessages.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef } from "react" import Message from "./Message" -import { ToolCall, ToolResult } from "./ToolPanel" import { isChatStreamingAtom } from "../../atoms/chatState" import { useAtomValue } from "jotai" @@ -11,8 +10,6 @@ export interface Message { timestamp: number files?: File[] isError?: boolean - toolCalls?: ToolCall[] - toolResults?: ToolResult[] } interface Props { @@ -69,8 +66,6 @@ const ChatMessages = ({ messages, isLoading, onRetry, onEdit }: Props) => { files={message.files} isError={message.isError} isLoading={!message.isSent && index === messages.length - 1 && isLoading} - toolCalls={message.toolCalls} - toolResults={message.toolResults} messageId={message.id} onRetry={() => onRetry(message.id)} onEdit={(newText: string) => onEdit(message.id, newText)} diff --git a/src/views/Chat/Message.tsx b/src/views/Chat/Message.tsx index c7e758a3..36078b03 100644 --- a/src/views/Chat/Message.tsx +++ b/src/views/Chat/Message.tsx @@ -5,17 +5,32 @@ import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import remarkMath from "remark-math" import rehypeKatex from "rehype-katex" +import rehypeRaw from "rehype-raw" import { PrismAsyncLight as SyntaxHighlighter } from "react-syntax-highlighter"; import { tomorrow, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { codeStreamingAtom } from '../../atoms/codeStreaming' -import ToolPanel, { ToolCall, ToolResult } from './ToolPanel' +import ToolPanel from './ToolPanel' import FilePreview from './FilePreview' import { useTranslation } from 'react-i18next' import { themeAtom } from "../../atoms/themeState"; import Textarea from "../../components/WrappedTextarea" import { isChatStreamingAtom } from "../../atoms/chatState" +declare global { + namespace JSX { + interface IntrinsicElements { + "tool-call": { + children: any + }; + "tool-result": { + children: any, + name: string, + }; + } + } +} + interface MessageProps { messageId: string text: string @@ -24,13 +39,11 @@ interface MessageProps { files?: (File | string)[] isError?: boolean isLoading?: boolean - toolCalls?: ToolCall[] - toolResults?: ToolResult[] onRetry: () => void onEdit: (editedText: string) => void } -const Message = ({ messageId, text, isSent, files, isError, isLoading, toolCalls, toolResults, onRetry, onEdit }: MessageProps) => { +const Message = ({ messageId, text, isSent, files, isError, isLoading, onRetry, onEdit }: MessageProps) => { const { t } = useTranslation() const [theme] = useAtom(themeAtom) const updateStreamingCode = useSetAtom(codeStreamingAtom) @@ -127,8 +140,25 @@ const Message = ({ messageId, text, isSent, files, isError, isLoading, toolCalls singleDollarTextMath: false, inlineMathDouble: false }], remarkGfm]} - rehypePlugins={[rehypeKatex]} + rehypePlugins={[rehypeKatex, rehypeRaw]} components={{ + "tool-call"({children}) { + return ( + + ) + }, + "tool-result"({children, name}) { + return ( + + ) + }, a(props) { return ( @@ -234,19 +264,6 @@ const Message = ({ messageId, text, isSent, files, isError, isLoading, toolCalls return (
- {toolCalls && ( - - )} - {toolResults && ( - - )} {formattedText} {files && files.length > 0 && } {isLoading && ( @@ -259,21 +276,6 @@ const Message = ({ messageId, text, isSent, files, isError, isLoading, toolCalls
{!isLoading && !isChatStreaming && (
- {/* {messageId.includes("-") && ( -
- -
- 1 - / - 2 -
- -
- )} */}
diff --git a/src/views/Chat/index.tsx b/src/views/Chat/index.tsx index 6ca637fb..c4f9fd06 100644 --- a/src/views/Chat/index.tsx +++ b/src/views/Chat/index.tsx @@ -5,13 +5,23 @@ import ChatInput from "./ChatInput" import CodeModal from './CodeModal' import { useAtom, useSetAtom } from 'jotai' import { codeStreamingAtom } from '../../atoms/codeStreaming' -import { ToolCall, ToolResult } from "./ToolPanel" import useHotkeyEvent from "../../hooks/useHotkeyEvent" import { showToastAtom } from "../../atoms/toastState" import { useTranslation } from "react-i18next" import { currentChatIdAtom, isChatStreamingAtom, lastMessageAtom } from "../../atoms/chatState" import { safeBase64Encode } from "../../util" +interface ToolCall { + name: string + arguments: any +} + +interface ToolResult { + name: string + result: any +} + + const ChatWindow = () => { const { chatId } = useParams() const location = useLocation() @@ -28,6 +38,8 @@ const ChatWindow = () => { const setCurrentChatId = useSetAtom(currentChatIdAtom) const [isChatStreaming, setIsChatStreaming] = useAtom(isChatStreamingAtom) const toolCallResults = useRef("") + const toolResultCount = useRef(0) + const toolResultTotal = useRef(0) const loadChat = useCallback(async (id: string) => { try { @@ -263,7 +275,13 @@ const ChatWindow = () => { case "tool_calls": const toolCalls = data.content as ToolCall[] - const toolName = data.content?.length > 0 ? data.content[0].name || "%name%" : "%name%" + + const tools = data.content?.map((call: {name: string}) => call.name) || [] + toolResultTotal.current = tools.length + + const uniqTools = new Set(tools) + const toolName = uniqTools.size === 0 ? "%name%" : Array.from(uniqTools).join(", ") + toolCallResults.current += `\n##Tool Calls:${safeBase64Encode(JSON.stringify(toolCalls))}` setMessages(prev => { const newMessages = [...prev] @@ -274,16 +292,24 @@ const ChatWindow = () => { case "tool_result": const result = data.content as ToolResult + + toolCallResults.current = toolCallResults.current.replace(`\n`, "") toolCallResults.current += `##Tool Result:${safeBase64Encode(JSON.stringify(result.result))}\n` - currentText += toolCallResults.current.replace("%name%", result.name) + setMessages(prev => { const newMessages = [...prev] - newMessages[newMessages.length - 1].text = currentText + newMessages[newMessages.length - 1].text = currentText + toolCallResults.current.replace("%name%", result.name) return newMessages }) - toolCallResults.current = "" - scrollToBottom() + toolResultCount.current++ + if (toolResultTotal.current === toolResultCount.current) { + currentText += toolCallResults.current.replace("%name%", result.name) + toolCallResults.current = "" + toolResultTotal.current = 0 + toolResultCount.current = 0 + } + break case "chat_info":