diff --git a/frontend/src/lib/components/chat/MessageContent.svelte b/frontend/src/lib/components/chat/MessageContent.svelte index a3b4217..3d1143c 100644 --- a/frontend/src/lib/components/chat/MessageContent.svelte +++ b/frontend/src/lib/components/chat/MessageContent.svelte @@ -21,12 +21,16 @@ // Pattern to find fenced code blocks const CODE_BLOCK_PATTERN = /```(\w+)?\n([\s\S]*?)```/g; - // Pattern to detect tool execution results - const TOOL_RESULT_PATTERN = /Tool execution results:\s*\n(Tool (?:result|error):[\s\S]*?)(?=\n\nBased on these results|$)/; + // Pattern to detect tool results in various formats + const TOOL_RESULT_PATTERN = /Tool result:\s*(\{[\s\S]*?\}|\S[\s\S]*?)(?=\n\n|$)/; + const TOOL_ERROR_PATTERN = /Tool error:\s*(.+?)(?=\n\n|$)/; // Pattern for "Called tool:" text (redundant with ToolCallDisplay) const CALLED_TOOL_PATTERN = /Called tool:\s*\w+\([^)]*\)\s*\n*/g; + // Pattern for "Tool execution results:" header + const TOOL_EXEC_HEADER_PATTERN = /Tool execution results:\s*\n?/g; + // Languages that should show a preview const PREVIEW_LANGUAGES = ['html', 'htm']; @@ -46,10 +50,14 @@ let modalImage = $state(null); /** - * Clean redundant "Called tool:" text (shown via ToolCallDisplay) + * Clean redundant tool text (shown via ToolCallDisplay) */ - function cleanCalledToolText(text: string): string { - return text.replace(CALLED_TOOL_PATTERN, '').trim(); + function cleanToolText(text: string): string { + return text + .replace(CALLED_TOOL_PATTERN, '') + .replace(TOOL_EXEC_HEADER_PATTERN, '') + .replace(/^Based on these results.*$/gm, '') + .trim(); } /** @@ -209,7 +217,7 @@ } // Clean and parse content into parts - const cleanedContent = $derived(cleanCalledToolText(content)); + const cleanedContent = $derived(cleanToolText(content)); const contentParts = $derived(parseContent(cleanedContent)); diff --git a/frontend/src/lib/components/chat/MessageItem.svelte b/frontend/src/lib/components/chat/MessageItem.svelte index 5587428..1c09c54 100644 --- a/frontend/src/lib/components/chat/MessageItem.svelte +++ b/frontend/src/lib/components/chat/MessageItem.svelte @@ -40,6 +40,15 @@ const hasContent = $derived(node.message.content.length > 0); const hasToolCalls = $derived(node.message.toolCalls && node.message.toolCalls.length > 0); + // Detect tool result messages (sent as user role but should be hidden or styled differently) + const isToolResultMessage = $derived( + isUser && ( + node.message.content.startsWith('Tool execution results:') || + node.message.content.startsWith('Tool result:') || + node.message.content.startsWith('Tool error:') + ) + ); + /** * Start editing a message */ @@ -80,6 +89,10 @@ } + +{#if isToolResultMessage} + +{:else}
{/if}
+{/if} diff --git a/frontend/src/lib/components/chat/ToolResultDisplay.svelte b/frontend/src/lib/components/chat/ToolResultDisplay.svelte index 924f78b..ccbaf4c 100644 --- a/frontend/src/lib/components/chat/ToolResultDisplay.svelte +++ b/frontend/src/lib/components/chat/ToolResultDisplay.svelte @@ -11,7 +11,7 @@ let { content }: Props = $props(); interface ParsedResult { - type: 'location' | 'search' | 'error' | 'text' | 'json'; + type: 'location' | 'search' | 'error' | 'text' | 'json' | 'fetch'; data: unknown; } @@ -39,36 +39,62 @@ results?: SearchResult[]; } + /** + * Try to extract JSON from text + */ + function extractJSON(text: string): unknown | null { + // Try to find JSON object in text + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + try { + return JSON.parse(jsonMatch[0]); + } catch { + return null; + } + } + return null; + } + /** * Parse the tool result content */ function parseResult(text: string): ParsedResult { - // Try to extract JSON from "Tool result: {...}" format - const jsonMatch = text.match(/Tool result:\s*(\{[\s\S]*\})/); - if (!jsonMatch) { - // Check for error - if (text.includes('Tool error:')) { - const errorMatch = text.match(/Tool error:\s*(.+)/); - return { type: 'error', data: errorMatch?.[1] || text }; - } - return { type: 'text', data: text }; + // Check for error first + if (text.includes('Tool error:') || text.includes('HTTP 403') || text.includes('HTTP 4') || text.includes('HTTP 5')) { + const errorMatch = text.match(/(?:Tool error:\s*)?(.+)/); + return { type: 'error', data: errorMatch?.[1]?.trim() || text }; } - try { - const data = JSON.parse(jsonMatch[1]); + // Try to extract JSON + const json = extractJSON(text); + if (json && typeof json === 'object') { + const data = json as Record; // Detect result type - if (data.location && (data.location.city || data.location.latitude)) { + if (data.location && typeof data.location === 'object') { return { type: 'location', data }; } if (data.results && Array.isArray(data.results) && data.query) { return { type: 'search', data }; } + if (data.title || data.text || data.url) { + return { type: 'fetch', data }; + } return { type: 'json', data }; - } catch { - return { type: 'text', data: text }; } + + // Plain text result (might be scraped content) + const cleanText = text.replace(/^Tool result:\s*/i, '').trim(); + if (cleanText.length > 0) { + // Check if it looks like HTML garbage + if (cleanText.includes(' +{:else if parsed.type === 'fetch'} + {@const data = parsed.data as Record} +
+
+
+ 🌐 + {#if data.title} + {data.title} + {:else if data.url} + + {data.url} + + {:else} + Fetched content + {/if} +
+ {#if data.text && typeof data.text === 'string'} +

{data.text.substring(0, 300)}{data.text.length > 300 ? '...' : ''}

+ {/if} +
+
+ {:else if parsed.type === 'json'} {@const data = parsed.data as Record}
@@ -146,6 +194,10 @@
{:else} - -

{parsed.data}

+ + {#if typeof parsed.data === 'string' && parsed.data.trim().length > 0} +
+

{parsed.data}

+
+ {/if} {/if}