mirror of https://github.com/kortix-ai/suna.git
ask attachements
This commit is contained in:
parent
6570ce2b62
commit
67f81d10a0
|
@ -5,7 +5,7 @@ import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
ArrowDown, CheckCircle, CircleDashed, AlertTriangle, Info
|
ArrowDown, CheckCircle, CircleDashed, AlertTriangle, Info, File
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { addUserMessage, getMessages, startAgent, stopAgent, getAgentRuns, getProject, getThread, updateProject, Project, Message as BaseApiMessageType } from '@/lib/api';
|
import { addUserMessage, getMessages, startAgent, stopAgent, getAgentRuns, getProject, getThread, updateProject, Project, Message as BaseApiMessageType } from '@/lib/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
@ -94,6 +94,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
const [sandboxId, setSandboxId] = useState<string | null>(null);
|
const [sandboxId, setSandboxId] = useState<string | null>(null);
|
||||||
const [fileViewerOpen, setFileViewerOpen] = useState(false);
|
const [fileViewerOpen, setFileViewerOpen] = useState(false);
|
||||||
const [projectName, setProjectName] = useState<string>('Project');
|
const [projectName, setProjectName] = useState<string>('Project');
|
||||||
|
const [fileToView, setFileToView] = useState<string | null>(null);
|
||||||
|
|
||||||
const initialLoadCompleted = useRef<boolean>(false);
|
const initialLoadCompleted = useRef<boolean>(false);
|
||||||
const messagesLoadedRef = useRef(false);
|
const messagesLoadedRef = useRef(false);
|
||||||
|
@ -454,7 +455,14 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
console.log(`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`);
|
console.log(`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`);
|
||||||
}, [agentStatus, streamHookStatus, agentRunId, currentHookRunId]);
|
}, [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) => {
|
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
||||||
if (!clickedAssistantMessageId) {
|
if (!clickedAssistantMessageId) {
|
||||||
|
@ -796,11 +804,54 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
const toolResult = potentialResults.find(r => !renderedToolResultIds.has(r.message_id!)); // Find first available result
|
const toolResult = potentialResults.find(r => !renderedToolResultIds.has(r.message_id!)); // Find first available result
|
||||||
|
|
||||||
if (toolName === 'ask') {
|
if (toolName === 'ask') {
|
||||||
// Render <ask> 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(/<ask[^>]*>([\s\S]*?)<\/ask>/i);
|
||||||
|
const content = contentMatch ? contentMatch[1] : rawXml;
|
||||||
|
|
||||||
|
// Render <ask> tag content with attachment UI
|
||||||
contentParts.push(
|
contentParts.push(
|
||||||
<span key={`ask-${match.index}`} className="whitespace-pre-wrap break-words">
|
<div key={`ask-${match.index}`} className="space-y-3">
|
||||||
{rawXml.match(/<ask>([\s\S]*?)<\/ask>/i)?.[1] || rawXml}
|
<span className="whitespace-pre-wrap break-words">
|
||||||
</span>
|
{content}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">Attachments:</div>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
|
{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 = <File className="h-4 w-4 text-gray-500" />;
|
||||||
|
if (isImage) icon = <File className="h-4 w-4 text-purple-500" />;
|
||||||
|
if (isPdf) icon = <File className="h-4 w-4 text-red-500" />;
|
||||||
|
if (isMd) icon = <File className="h-4 w-4 text-blue-500" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`attachment-${idx}`}
|
||||||
|
onClick={() => handleOpenFileViewer(attachment)}
|
||||||
|
className="flex items-center gap-1.5 py-1.5 px-2.5 text-xs text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors cursor-pointer border border-gray-200"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<span className="font-mono text-xs text-gray-700 truncate">{attachment}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Render tool button AND its result icon inline
|
// Render tool button AND its result icon inline
|
||||||
|
@ -1004,6 +1055,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
open={fileViewerOpen}
|
open={fileViewerOpen}
|
||||||
onOpenChange={setFileViewerOpen}
|
onOpenChange={setFileViewerOpen}
|
||||||
sandboxId={sandboxId}
|
sandboxId={sandboxId}
|
||||||
|
initialFilePath={fileToView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,12 +29,14 @@ interface FileViewerModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
sandboxId: string;
|
sandboxId: string;
|
||||||
|
initialFilePath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileViewerModal({
|
export function FileViewerModal({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
sandboxId
|
sandboxId,
|
||||||
|
initialFilePath
|
||||||
}: FileViewerModalProps) {
|
}: FileViewerModalProps) {
|
||||||
const [workspaceFiles, setWorkspaceFiles] = useState<FileInfo[]>([]);
|
const [workspaceFiles, setWorkspaceFiles] = useState<FileInfo[]>([]);
|
||||||
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
|
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
|
||||||
|
@ -57,6 +59,51 @@ export function FileViewerModal({
|
||||||
}
|
}
|
||||||
}, [open, sandboxId, currentPath]);
|
}, [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
|
// Function to load files from a specific path
|
||||||
const loadFilesAtPath = async (path: string) => {
|
const loadFilesAtPath = async (path: string) => {
|
||||||
if (!sandboxId) return;
|
if (!sandboxId) return;
|
||||||
|
|
Loading…
Reference in New Issue