diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index 926a513e8..af4919878 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -22,6 +22,7 @@ interface MarkdownProps { components?: Components enableRawHtml?: boolean isUser?: boolean + isWrapping?: boolean } function RenderMarkdownComponent({ @@ -30,6 +31,7 @@ function RenderMarkdownComponent({ className, isUser, components, + isWrapping, }: MarkdownProps) { const { codeBlockStyle, showLineNumbers } = useCodeblock() @@ -117,9 +119,13 @@ function RenderMarkdownComponent({ showLineNumbers={showLineNumbers} wrapLines={true} // Temporary comment we try calculate main area width on __root - // lineProps={{ - // style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' }, - // }} + lineProps={ + isWrapping + ? { + style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' }, + } + : {} + } customStyle={{ margin: 0, padding: '8px', diff --git a/web-app/src/containers/StreamingContent.tsx b/web-app/src/containers/StreamingContent.tsx index ce88cbc49..573dc29c9 100644 --- a/web-app/src/containers/StreamingContent.tsx +++ b/web-app/src/containers/StreamingContent.tsx @@ -32,6 +32,12 @@ export const StreamingContent = memo(({ threadId }: Props) => { return extractReasoningSegment(text) }, [streamingContent]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const streamingTools: any = useMemo(() => { + const calls = streamingContent?.metadata?.tool_calls + return calls + }, [streamingContent]) + const lastAssistant = useMemo(() => { return [...messages].reverse().find((m) => m.role === 'assistant') }, [messages]) @@ -52,6 +58,14 @@ export const StreamingContent = memo(({ threadId }: Props) => { // The streaming content is always the last message return ( { const [message, setMessage] = useState(item.content?.[0]?.text?.value || '') @@ -316,8 +318,17 @@ export const ThreadContent = memo( {(item.metadata.tool_calls as ToolCall[]).map((toolCall) => ( @@ -332,7 +343,7 @@ export const ThreadContent = memo( 'flex items-center gap-2', item.isLastMessage && streamingContent && - 'opacity-0 visinility-hidden pointer-events-none' + 'opacity-0 visibility-hidden pointer-events-none' )} > diff --git a/web-app/src/containers/ToolCallBlock.tsx b/web-app/src/containers/ToolCallBlock.tsx index f43b794fa..661406a3b 100644 --- a/web-app/src/containers/ToolCallBlock.tsx +++ b/web-app/src/containers/ToolCallBlock.tsx @@ -9,10 +9,12 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { twMerge } from 'tailwind-merge' interface Props { result: string name: string + args: object id: number loading: boolean } @@ -90,7 +92,7 @@ const ContentItemRenderer = ({ if (item.type === 'image' && item.data && item.mimeType) { const imageUrl = createDataUrl(item.data, item.mimeType) return ( -
+
{`Result { +const ToolCallBlock = ({ id, name, result, loading, args }: Props) => { const { collapseState, setCollapseState } = useToolCallBlockStore() - const isExpanded = collapseState[id] ?? false + const isExpanded = collapseState[id] ?? (loading ? true : false) const [modalImage, setModalImage] = useState<{ url: string alt: string @@ -145,10 +147,9 @@ const ToolCallBlock = ({ id, name, result, loading }: Props) => { } // Parse the MCP response and extract content items - const { parsedResult, contentItems, hasStructuredContent, parseError } = - useMemo(() => { - return parseMCPResponse(result) - }, [result]) + const { parsedResult, contentItems, hasStructuredContent } = useMemo(() => { + return parseMCPResponse(result) + }, [result]) return (
{
{loading && ( - +
+ +
)}
@@ -179,36 +199,43 @@ const ToolCallBlock = ({ id, name, result, loading }: Props) => { isExpanded ? '' : 'max-h-0 overflow-hidden' )} > -
- {hasStructuredContent ? ( - /* Render each content item individually based on its type */ -
- {contentItems.map((item, index) => ( - + {args && Object.keys(args).length > 3 && ( + <> +

Arguments:

+ + + )} + + {result && ( + <> +

Output:

+ {hasStructuredContent ? ( + /* Render each content item individually based on its type */ +
+ {contentItems.map((item, index) => ( + + ))} +
+ ) : ( + /* Fallback: render as JSON for valid JSON but unstructured responses */ + - ))} -
- ) : parseError ? ( - /* Handle JSON parse error - render as plain text */ -
-
- Raw Response: -
-
- {parsedResult as string} -
-
- ) : ( - /* Fallback: render as JSON for valid JSON but unstructured responses */ - + )} + )}
diff --git a/web-app/src/hooks/useAppState.ts b/web-app/src/hooks/useAppState.ts index 6468c6d48..ace57d8d2 100644 --- a/web-app/src/hooks/useAppState.ts +++ b/web-app/src/hooks/useAppState.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { ThreadMessage } from '@janhq/core' import { MCPTool } from '@/types/completion' import { useAssistant } from './useAssistant' +import { ChatCompletionMessageToolCall } from 'openai/resources' type AppState = { streamingContent?: ThreadMessage @@ -10,8 +11,12 @@ type AppState = { serverStatus: 'running' | 'stopped' | 'pending' abortControllers: Record tokenSpeed?: TokenSpeed + currentToolCall?: ChatCompletionMessageToolCall setServerStatus: (value: 'running' | 'stopped' | 'pending') => void updateStreamingContent: (content: ThreadMessage | undefined) => void + updateCurrentToolCall: ( + toolCall: ChatCompletionMessageToolCall | undefined + ) => void updateLoadingModel: (loading: boolean) => void updateTools: (tools: MCPTool[]) => void setAbortController: (threadId: string, controller: AbortController) => void @@ -26,6 +31,7 @@ export const useAppState = create()((set) => ({ serverStatus: 'stopped', abortControllers: {}, tokenSpeed: undefined, + currentToolCall: undefined, updateStreamingContent: (content: ThreadMessage | undefined) => { set(() => ({ streamingContent: content @@ -40,6 +46,11 @@ export const useAppState = create()((set) => ({ : undefined, })) }, + updateCurrentToolCall: (toolCall) => { + set(() => ({ + currentToolCall: toolCall, + })) + }, updateLoadingModel: (loading) => { set({ loadingModel: loading }) }, diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 449b5daa9..3073ececf 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -181,8 +181,21 @@ export const useChat = () => { } else { for await (const part of completion) { const delta = part.choices[0]?.delta?.content || '' + if (part.choices[0]?.delta?.tool_calls) { - extractToolCall(part, currentCall, toolCalls) + const calls = extractToolCall(part, currentCall, toolCalls) + const currentContent = newAssistantThreadContent( + activeThread.id, + accumulatedText, + { + tool_calls: calls.map((e) => ({ + ...e, + state: 'pending', + })), + } + ) + updateStreamingContent(currentContent) + await new Promise((resolve) => setTimeout(resolve, 0)) } if (delta) { accumulatedText += delta @@ -190,7 +203,13 @@ export const useChat = () => { // Use a timeout to prevent React from batching updates too quickly const currentContent = newAssistantThreadContent( activeThread.id, - accumulatedText + accumulatedText, + { + tool_calls: toolCalls.map((e) => ({ + ...e, + state: 'pending', + })), + } ) updateStreamingContent(currentContent) updateTokenSpeed(currentContent) @@ -225,6 +244,7 @@ export const useChat = () => { allowAllMCPPermissions ) addMessage(updatedMessage ?? finalContent) + updateStreamingContent(emptyThreadContent) updateThreadTimestamp(activeThread.id) isCompleted = !toolCalls.length diff --git a/web-app/src/lib/completion.ts b/web-app/src/lib/completion.ts index 2f3d227de..79813192a 100644 --- a/web-app/src/lib/completion.ts +++ b/web-app/src/lib/completion.ts @@ -63,7 +63,8 @@ export const newUserThreadContent = ( */ export const newAssistantThreadContent = ( threadId: string, - content: string + content: string, + metadata: Record = {} ): ThreadMessage => ({ type: 'text', role: ChatCompletionRole.Assistant, @@ -82,6 +83,7 @@ export const newAssistantThreadContent = ( status: MessageStatus.Ready, created_at: 0, completed_at: 0, + metadata, }) /** diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index e6512f447..67e88ed90 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -43,7 +43,7 @@ const AppLayout = () => {
diff --git a/web-app/src/types/message.d.ts b/web-app/src/types/message.d.ts index f4beddef8..fc7128a45 100644 --- a/web-app/src/types/message.d.ts +++ b/web-app/src/types/message.d.ts @@ -3,6 +3,7 @@ type ToolCall = { id?: number function?: { name?: string + arguments?: object } } response?: unknown