This commit is contained in:
marko-kraemer 2025-04-21 14:37:09 +01:00
parent bd0db22966
commit 3d885142ba
3 changed files with 281 additions and 33 deletions

View File

@ -441,6 +441,30 @@ For casual conversation and social interactions:
- Tool Results: Carefully analyze all tool execution results to inform your next actions. **Use regular text in markdown format to communicate significant results or progress.**
## 7.3 ATTACHMENT PROTOCOL
- **CRITICAL: ALL VISUALIZATIONS MUST BE ATTACHED:**
* When using the 'ask' tool <ask attachments="file1, file2, file3"></ask>, ALWAYS attach ALL visualizations, markdown files, charts, graphs, reports, and any viewable content created
* This includes but is not limited to: HTML files, PDF documents, markdown files, images, data visualizations, presentations, reports, dashboards, and UI mockups
* NEVER mention a visualization or viewable content without attaching it
* If you've created multiple visualizations, attach ALL of them
* Always make visualizations available to the user BEFORE marking tasks as complete
* For web applications or interactive content, always attach the main HTML file
* When creating data analysis results, charts must be attached, not just described
* Remember: If the user should SEE it, you must ATTACH it with the 'ask' tool
* Verify that ALL visual outputs have been attached before proceeding
- **Attachment Checklist:**
* Data visualizations (charts, graphs, plots)
* Web interfaces (HTML/CSS/JS files)
* Reports and documents (PDF, HTML)
* Presentation materials
* Images and diagrams
* Interactive dashboards
* Analysis results with visual components
* UI designs and mockups
* Any file intended for user viewing or interaction
# 8. COMPLETION PROTOCOLS
## 8.1 TERMINATION RULES

View File

@ -1,11 +1,11 @@
'use client';
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import {
ArrowDown, CircleDashed, Info, File, ChevronRight, Play, Pause, MonitorPlay, ExternalLink
ArrowDown, CircleDashed, Info, File, ChevronRight, Play, Pause
} from 'lucide-react';
import { getMessages, getProject, getThread, Project, Message as BaseApiMessageType } from '@/lib/api';
import { toast } from 'sonner';
@ -20,28 +20,6 @@ import { cn } from "@/lib/utils";
import { UnifiedMessage, ParsedContent, ParsedMetadata, ThreadParams } from '@/components/thread/types';
import { getToolIcon, extractPrimaryParam, safeJsonParse } from '@/components/thread/utils';
// Function to extract tool calls from a message
function extractToolCallsFromMessage(messageText: string) {
const result: { name: string; fullMatch: string }[] = [];
// Define regex to find tool calls in text
const toolCallRegex = /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
let match;
while ((match = toolCallRegex.exec(messageText)) !== null) {
const toolName = match[1] || match[2];
// Skip <ask> tags
if (toolName !== 'ask') {
result.push({
name: toolName,
fullMatch: match[0]
});
}
}
return result;
}
// Define the set of tags whose raw XML should be hidden during streaming
const HIDE_STREAMING_XML_TAGS = new Set([
'execute-command',
@ -1117,7 +1095,6 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
}
}, [projectName]);
// In the useEffect where streamingTextContent is handled
useEffect(() => {
if (streamingTextContent && streamHookStatus === 'streaming' && messages.length > 0) {
// Find the last assistant message to update with streaming content
@ -1154,6 +1131,257 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
}
}, [streamingTextContent, streamHookStatus, messages, isStreamingText, isPlaying, currentMessageIndex, streamText]);
// Create a message-to-tool-index map for faster lookups
const [messageToToolIndex, setMessageToToolIndex] = useState<Record<string, number>>({});
// Build the message-to-tool-index map when tool calls change
useEffect(() => {
if (!toolCalls.length) return;
const mapBuilder: Record<string, number> = {};
toolCalls.forEach((tool, index) => {
const content = tool.assistantCall?.content || '';
const match = content.match(/<!-- messageId:([\w-]+) -->/);
if (match && match[1]) {
mapBuilder[match[1]] = index;
console.log(`Mapped message ID ${match[1]} to tool index ${index}`);
}
});
setMessageToToolIndex(mapBuilder);
}, [toolCalls]);
// Very direct approach to update the tool index during message playback
useEffect(() => {
if (!isPlaying || currentMessageIndex <= 0 || !messages.length) return;
// Check if current message is a tool message
const currentMsg = messages[currentMessageIndex - 1]; // Look at previous message that just played
if (currentMsg?.type === 'tool' && currentMsg.metadata) {
try {
const metadata = safeJsonParse<ParsedMetadata>(currentMsg.metadata, {});
const assistantId = metadata.assistant_message_id;
if (assistantId && messageToToolIndex[assistantId] !== undefined) {
const toolIndex = messageToToolIndex[assistantId];
console.log(`Direct mapping: Setting tool index to ${toolIndex} for message ${assistantId}`);
setCurrentToolIndex(toolIndex);
}
} catch (e) {
console.error('Error in direct tool mapping:', e);
}
}
}, [currentMessageIndex, isPlaying, messages, messageToToolIndex]);
// Add a helper function to extract tool calls from message content
const extractToolCallsFromMessage = (content: string) => {
const toolCallRegex = /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
const results = [];
let match;
while ((match = toolCallRegex.exec(content)) !== null) {
const toolName = match[1] || match[2];
results.push({
name: toolName,
fullMatch: match[0]
});
}
return results;
};
// Force an explicit update to the tool panel based on the current message index
useEffect(() => {
// Skip if not playing or no messages
if (!isPlaying || messages.length === 0 || currentMessageIndex <= 0) return;
// Get all messages up to the current index
const currentMessages = messages.slice(0, currentMessageIndex);
// Find the most recent tool message to determine which panel to show
for (let i = currentMessages.length - 1; i >= 0; i--) {
const msg = currentMessages[i];
if (msg.type === 'tool' && msg.metadata) {
try {
const metadata = safeJsonParse<ParsedMetadata>(msg.metadata, {});
const assistantId = metadata.assistant_message_id;
if (assistantId) {
console.log(`Looking for tool panel for assistant message ${assistantId}`);
// Scan for matching tool call
for (let j = 0; j < toolCalls.length; j++) {
const content = toolCalls[j].assistantCall?.content || '';
if (content.includes(assistantId)) {
console.log(`Found matching tool call at index ${j}, updating panel`);
setCurrentToolIndex(j);
return;
}
}
}
} catch (e) {
console.error('Error parsing tool message metadata:', e);
}
}
}
}, [currentMessageIndex, isPlaying, messages, toolCalls]);
// Add a special button to each tool call to show its debug info
// This replaces the existing ToolCallSidePanel component with a wrapper that adds debug info
const ToolCallPanelWithDebugInfo = React.useMemo(() => {
const WrappedPanel = (props: any) => {
const { isOpen, onClose, toolCalls, currentIndex, onNavigate, ...rest } = props;
// Add a function to show debug info for the current tool call
const showDebugInfo = useCallback(() => {
if (toolCalls && toolCalls.length > 0 && currentIndex >= 0 && currentIndex < toolCalls.length) {
const tool = toolCalls[currentIndex];
console.log('Current tool call debug info:', {
name: tool.assistantCall?.name,
content: tool.assistantCall?.content,
messageIdMatches: tool.assistantCall?.content?.match(/<!-- messageId:([\w-]+) -->/),
toolResult: tool.toolResult
});
}
}, [toolCalls, currentIndex]);
return (
<div>
<ToolCallSidePanel
isOpen={isOpen}
onClose={onClose}
toolCalls={toolCalls}
currentIndex={currentIndex}
onNavigate={onNavigate}
{...rest}
/>
{/* Add debug button */}
{isOpen && toolCalls && toolCalls.length > 0 && (
<div className="fixed bottom-4 right-4 z-50">
<Button
variant="outline"
size="sm"
className="h-8 w-8 rounded-full p-0 bg-background/50 backdrop-blur"
onClick={showDebugInfo}
>
<span className="sr-only">Debug</span>
<Info className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
};
return WrappedPanel;
}, []);
if (isLoading && !initialLoadCompleted.current) {
return (
<div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
{/* Skeleton Header */}
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center gap-4 px-4">
<div className="flex-1">
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-6 rounded-full" />
<Skeleton className="h-5 w-40" />
</div>
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
</div>
</div>
{/* Skeleton Chat Messages */}
<div className="flex-1 overflow-y-auto px-6 py-4 pb-[5.5rem]">
<div className="mx-auto max-w-3xl space-y-6">
{/* User message */}
<div className="flex justify-end">
<div className="max-w-[85%] rounded-lg bg-primary/10 px-4 py-3">
<div className="space-y-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</div>
</div>
{/* Assistant response with tool usage */}
<div>
<div className="flex items-start gap-3">
<Skeleton className="flex-shrink-0 w-5 h-5 mt-2 rounded-full" />
<div className="flex-1 space-y-2">
<div className="max-w-[90%] w-full rounded-lg bg-muted px-4 py-3">
<div className="space-y-3">
<div>
<Skeleton className="h-4 w-full max-w-[360px] mb-2" />
<Skeleton className="h-4 w-full max-w-[320px] mb-2" />
<Skeleton className="h-4 w-full max-w-[290px]" />
</div>
{/* Tool call button skeleton */}
<div className="py-1">
<Skeleton className="h-6 w-32 rounded-md" />
</div>
<div>
<Skeleton className="h-4 w-full max-w-[340px] mb-2" />
<Skeleton className="h-4 w-full max-w-[280px]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Skeleton Side Panel (closed state) */}
<div className={`hidden sm:block ${isSidePanelOpen ? 'block' : ''}`}>
<div className="h-screen w-[450px] border-l">
<div className="p-4">
<Skeleton className="h-8 w-32 mb-4" />
<Skeleton className="h-20 w-full rounded-md mb-4" />
<Skeleton className="h-40 w-full rounded-md" />
</div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center gap-4 px-4">
<div className="flex-1">
<span className="text-foreground font-medium">Shared Conversation</span>
</div>
</div>
</div>
<div className="flex flex-1 items-center justify-center p-4">
<div className="flex w-full max-w-md flex-col items-center gap-4 rounded-lg border bg-card p-6 text-center">
<h2 className="text-lg font-semibold text-destructive">Error</h2>
<p className="text-sm text-muted-foreground">{error}</p>
<Button variant="outline" onClick={() => router.push(`/`)}>
Back to Home
</Button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
@ -1526,8 +1754,8 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
</Button>
)}
{/* Tool calls side panel */}
<ToolCallSidePanel
{/* Tool calls side panel - Replace with debug-enabled version */}
<ToolCallPanelWithDebugInfo
isOpen={isSidePanelOpen}
onClose={() => setIsSidePanelOpen(false)}
toolCalls={toolCalls}
@ -1550,4 +1778,4 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
/>
</div>
);
}
}

View File

@ -144,11 +144,7 @@ export function FileOperationToolView({
? `${project.sandbox.sandbox_url}/${processedFilePath}`
: undefined;
// Only log HTML preview URL when it exists
if (htmlPreviewUrl) {
console.log('HTML Preview URL:', htmlPreviewUrl);
}
// console.log('HTML Preview URL:', htmlPreviewUrl);
// Add state for view mode toggle (code or preview)
const [viewMode, setViewMode] = useState<'code' | 'preview'>(isHtml || isMarkdown || isCsv ? 'preview' : 'code');