From 67f81d10a0aaa662e4bdaf39e45aee184b334277 Mon Sep 17 00:00:00 2001 From: Adam Cohen Hillel Date: Fri, 18 Apr 2025 18:30:09 +0100 Subject: [PATCH] ask attachements --- .../app/dashboard/agents/[threadId]/page.tsx | 64 +++++++++++++++++-- .../components/thread/file-viewer-modal.tsx | 49 +++++++++++++- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/dashboard/agents/[threadId]/page.tsx b/frontend/src/app/dashboard/agents/[threadId]/page.tsx index 011ca773..68d81aeb 100644 --- a/frontend/src/app/dashboard/agents/[threadId]/page.tsx +++ b/frontend/src/app/dashboard/agents/[threadId]/page.tsx @@ -5,7 +5,7 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { - ArrowDown, CheckCircle, CircleDashed, AlertTriangle, Info + ArrowDown, CheckCircle, CircleDashed, AlertTriangle, Info, File } from 'lucide-react'; import { addUserMessage, getMessages, startAgent, stopAgent, getAgentRuns, getProject, getThread, updateProject, Project, Message as BaseApiMessageType } from '@/lib/api'; import { toast } from 'sonner'; @@ -94,6 +94,7 @@ export default function ThreadPage({ params }: { params: Promise } const [sandboxId, setSandboxId] = useState(null); const [fileViewerOpen, setFileViewerOpen] = useState(false); const [projectName, setProjectName] = useState('Project'); + const [fileToView, setFileToView] = useState(null); const initialLoadCompleted = useRef(false); const messagesLoadedRef = useRef(false); @@ -454,7 +455,14 @@ export default function ThreadPage({ params }: { params: Promise } console.log(`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`); }, [agentStatus, streamHookStatus, agentRunId, currentHookRunId]); - const handleOpenFileViewer = useCallback(() => setFileViewerOpen(true), []); + const handleOpenFileViewer = useCallback((filePath?: string) => { + if (filePath) { + setFileToView(filePath); + } else { + setFileToView(null); + } + setFileViewerOpen(true); + }, []); const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => { if (!clickedAssistantMessageId) { @@ -796,11 +804,54 @@ export default function ThreadPage({ params }: { params: Promise } const toolResult = potentialResults.find(r => !renderedToolResultIds.has(r.message_id!)); // Find first available result if (toolName === 'ask') { - // Render tag content as plain text + // Extract attachments from the XML attributes + const attachmentsMatch = rawXml.match(/attachments=["']([^"']*)["']/i); + const attachments = attachmentsMatch + ? attachmentsMatch[1].split(',').map(a => a.trim()) + : []; + + // Extract content from the ask tag + const contentMatch = rawXml.match(/]*>([\s\S]*?)<\/ask>/i); + const content = contentMatch ? contentMatch[1] : rawXml; + + // Render tag content with attachment UI contentParts.push( - - {rawXml.match(/([\s\S]*?)<\/ask>/i)?.[1] || rawXml} - +
+ + {content} + + + {attachments.length > 0 && ( +
+
Attachments:
+
+ {attachments.map((attachment, idx) => { + // Determine file type & icon based on extension + const extension = attachment.split('.').pop()?.toLowerCase(); + const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension || ''); + const isPdf = extension === 'pdf'; + const isMd = extension === 'md'; + + let icon = ; + if (isImage) icon = ; + if (isPdf) icon = ; + if (isMd) icon = ; + + return ( + + ); + })} +
+
+ )} +
); } else { // Render tool button AND its result icon inline @@ -1004,6 +1055,7 @@ export default function ThreadPage({ params }: { params: Promise } open={fileViewerOpen} onOpenChange={setFileViewerOpen} sandboxId={sandboxId} + initialFilePath={fileToView} /> )} diff --git a/frontend/src/components/thread/file-viewer-modal.tsx b/frontend/src/components/thread/file-viewer-modal.tsx index 1d722333..0275637f 100644 --- a/frontend/src/components/thread/file-viewer-modal.tsx +++ b/frontend/src/components/thread/file-viewer-modal.tsx @@ -29,12 +29,14 @@ interface FileViewerModalProps { open: boolean; onOpenChange: (open: boolean) => void; sandboxId: string; + initialFilePath?: string | null; } export function FileViewerModal({ open, onOpenChange, - sandboxId + sandboxId, + initialFilePath }: FileViewerModalProps) { const [workspaceFiles, setWorkspaceFiles] = useState([]); const [isLoadingFiles, setIsLoadingFiles] = useState(false); @@ -57,6 +59,51 @@ export function FileViewerModal({ } }, [open, sandboxId, currentPath]); + // Handle initial file path when provided + useEffect(() => { + if (open && sandboxId && initialFilePath) { + // Extract the directory path from the file path + const filePath = initialFilePath.startsWith('/workspace/') + ? initialFilePath + : `/workspace/${initialFilePath}`; + + const lastSlashIndex = filePath.lastIndexOf('/'); + const directoryPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '/workspace'; + const fileName = lastSlashIndex > 0 ? filePath.substring(lastSlashIndex + 1) : filePath; + + // First navigate to the directory + if (directoryPath !== currentPath) { + setCurrentPath(directoryPath); + setPathHistory(['/workspace', directoryPath]); + setHistoryIndex(1); + + // After directory is loaded, find and click the file + const findAndClickFile = async () => { + try { + const files = await listSandboxFiles(sandboxId, directoryPath); + const targetFile = files.find(f => f.path === filePath || f.name === fileName); + if (targetFile) { + // Wait a moment for the UI to update with the files + setTimeout(() => { + handleFileClick(targetFile); + }, 100); + } + } catch (error) { + console.error('Failed to load directory for initial file', error); + } + }; + + findAndClickFile(); + } else { + // If already in the right directory, just find and click the file + const targetFile = workspaceFiles.find(f => f.path === filePath || f.name === fileName); + if (targetFile) { + handleFileClick(targetFile); + } + } + } + }, [open, sandboxId, initialFilePath]); + // Function to load files from a specific path const loadFilesAtPath = async (path: string) => { if (!sandboxId) return;