tools components

This commit is contained in:
Adam Cohen Hillel 2025-04-18 18:21:48 +01:00
parent 2b3556facc
commit 6570ce2b62
12 changed files with 1501 additions and 88 deletions

View File

@ -1,10 +1,21 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { X, SkipBack, SkipForward } from "lucide-react"; import { X } from "lucide-react";
import { Project } from "@/lib/api"; import { Project } from "@/lib/api";
import { getToolIcon } from "@/components/thread/utils"; import { getToolIcon } from "@/components/thread/utils";
import React from "react"; import React from "react";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
// Import tool view components from the tool-views directory
import { CommandToolView } from "./tool-views/CommandToolView";
import { StrReplaceToolView } from "./tool-views/StrReplaceToolView";
import { GenericToolView } from "./tool-views/GenericToolView";
import { CreateFileToolView } from "./tool-views/CreateFileToolView";
import { FileRewriteToolView } from "./tool-views/FileRewriteToolView";
import { DeleteFileToolView } from "./tool-views/DeleteFileToolView";
import { BrowserToolView } from "./tool-views/BrowserToolView";
import { WebSearchToolView } from "./tool-views/WebSearchToolView";
import { WebCrawlToolView } from "./tool-views/WebCrawlToolView";
// Simple input interface // Simple input interface
export interface ToolCallInput { export interface ToolCallInput {
assistantCall: { assistantCall: {
@ -19,85 +30,119 @@ export interface ToolCallInput {
}; };
} }
// Helper function to format timestamp // Get the specialized tool view component based on the tool name
function formatTimestamp(isoString?: string): string { function getToolView(
if (!isoString) return ''; toolName: string | undefined,
try { assistantContent: string | undefined,
const date = new Date(isoString); toolContent: string | undefined,
return isNaN(date.getTime()) ? 'Invalid date' : date.toLocaleString(); assistantTimestamp: string | undefined,
} catch (e) { toolTimestamp: string | undefined,
return 'Invalid date'; isSuccess: boolean = true,
} project?: Project
} ) {
if (!toolName) return null;
// Simplified generic tool view
function GenericToolView({
name,
assistantContent,
toolContent,
isSuccess = true,
assistantTimestamp,
toolTimestamp
}: {
name?: string;
assistantContent?: string;
toolContent?: string;
isSuccess?: boolean;
assistantTimestamp?: string;
toolTimestamp?: string;
}) {
const toolName = name || 'Unknown Tool';
return ( const normalizedToolName = toolName.toLowerCase();
<div className="space-y-4 p-4">
<div className="flex items-center justify-between"> switch (normalizedToolName) {
<div className="flex items-center gap-2"> case 'execute-command':
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center"> return (
{React.createElement(getToolIcon(toolName), { className: "h-4 w-4" })} <CommandToolView
</div> assistantContent={assistantContent}
<div> toolContent={toolContent}
<h4 className="text-sm font-medium">{toolName}</h4> assistantTimestamp={assistantTimestamp}
</div> toolTimestamp={toolTimestamp}
</div> isSuccess={isSuccess}
/>
{toolContent && ( );
<div className={`px-2 py-1 rounded-full text-xs ${ case 'str-replace':
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700' return (
}`}> <StrReplaceToolView
{isSuccess ? 'Success' : 'Failed'} assistantContent={assistantContent}
</div> toolContent={toolContent}
)} assistantTimestamp={assistantTimestamp}
</div> toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
case 'create-file':
return (
<CreateFileToolView
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
case 'full-file-rewrite':
return (
<FileRewriteToolView
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
case 'delete-file':
return (
<DeleteFileToolView
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
case 'web-search':
return (
<WebSearchToolView
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
case 'crawl-webpage':
return (
<WebCrawlToolView
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
default:
// Check if it's a browser operation
if (normalizedToolName.startsWith('browser-')) {
return (
<BrowserToolView
name={toolName}
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
project={project}
/>
);
}
{/* Assistant Message */} // Fallback to generic view
<div className="space-y-1"> return (
<div className="flex justify-between items-center"> <GenericToolView
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div> name={toolName}
{assistantTimestamp && ( assistantContent={assistantContent}
<div className="text-xs text-muted-foreground">{formatTimestamp(assistantTimestamp)}</div> toolContent={toolContent}
)} assistantTimestamp={assistantTimestamp}
</div> toolTimestamp={toolTimestamp}
<div className="rounded-md border bg-muted/50 p-3"> isSuccess={isSuccess}
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{assistantContent}</pre> />
</div> );
</div> }
{/* Tool Result */}
{toolContent && (
<div className="space-y-1">
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-muted-foreground">Tool Result</div>
{toolTimestamp && (
<div className="text-xs text-muted-foreground">{formatTimestamp(toolTimestamp)}</div>
)}
</div>
<div className={`rounded-md border p-3 ${isSuccess ? 'bg-muted/50' : 'bg-red-50'}`}>
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{toolContent}</pre>
</div>
</div>
)}
</div>
);
} }
interface ToolCallSidePanelProps { interface ToolCallSidePanelProps {
@ -133,22 +178,22 @@ export function ToolCallSidePanel({
); );
} }
return ( // Get the specific tool view based on the tool name
<GenericToolView return getToolView(
name={currentToolCall.assistantCall.name} currentToolCall.assistantCall.name,
assistantContent={currentToolCall.assistantCall.content} currentToolCall.assistantCall.content,
assistantTimestamp={currentToolCall.assistantCall.timestamp} currentToolCall.toolResult?.content,
toolContent={currentToolCall.toolResult?.content} currentToolCall.assistantCall.timestamp,
isSuccess={currentToolCall.toolResult?.isSuccess ?? true} currentToolCall.toolResult?.timestamp,
toolTimestamp={currentToolCall.toolResult?.timestamp} currentToolCall.toolResult?.isSuccess ?? true,
/> project
); );
}; };
return ( return (
<div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[600px] bg-background border-l flex flex-col z-10"> <div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[600px] bg-background border-l flex flex-col z-10">
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<h3 className="text-sm font-semibold">Tool Details</h3> <h3 className="text-sm font-semibold">Suna&apos;s Computer</h3>
<Button variant="ghost" size="icon" onClick={onClose} className="text-muted-foreground hover:text-foreground"> <Button variant="ghost" size="icon" onClick={onClose} className="text-muted-foreground hover:text-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>

View File

@ -0,0 +1,110 @@
import React from "react";
import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle } from "lucide-react";
import { BrowserToolViewProps } from "./types";
import { extractBrowserUrl, extractBrowserOperation, formatTimestamp } from "./utils";
export function BrowserToolView({
name,
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true,
project
}: BrowserToolViewProps) {
const url = extractBrowserUrl(assistantContent);
const operation = extractBrowserOperation(name);
// Check if we have a VNC preview URL from the project
const vncPreviewUrl = project?.sandbox?.vnc_preview ?
`${project.sandbox.vnc_preview}/vnc_lite.html?password=${project?.sandbox?.pass}` :
undefined;
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<Globe className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">{operation}</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden">
<div className="bg-muted p-2 flex items-center justify-between border-b">
<div className="flex items-center">
<MonitorPlay className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-sm font-medium">Browser Window</span>
</div>
{url && (
<div className="text-xs font-mono text-muted-foreground truncate max-w-[200px]">
{url}
</div>
)}
</div>
{vncPreviewUrl ? (
<div className="aspect-video relative bg-black">
<iframe
src={vncPreviewUrl}
title="Browser preview"
className="w-full h-full"
style={{ minHeight: "400px" }}
frameBorder="0"
allowFullScreen
/>
</div>
) : (
<div className="p-8 flex flex-col items-center justify-center bg-muted/10 text-muted-foreground">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
<p className="text-sm font-medium">Browser preview not available</p>
{url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="mt-3 flex items-center text-blue-600 hover:underline"
>
Visit URL <ExternalLink className="h-3 w-3 ml-1" />
</a>
)}
</div>
)}
{isSuccess && (
<div className="px-3 py-2 border-t bg-green-50 text-green-700 text-xs flex items-center">
<CheckCircle className="h-3 w-3 mr-2" />
{operation} completed successfully
</div>
)}
{!isSuccess && (
<div className="px-3 py-2 border-t bg-red-50 text-red-700 text-xs flex items-center">
<AlertTriangle className="h-3 w-3 mr-2" />
{operation} failed
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,144 @@
import React from "react";
import { Terminal, CheckCircle, AlertTriangle } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractCommand, extractCommandOutput, extractExitCode, formatTimestamp } from "./utils";
export function CommandToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
// Clean the command by removing any leading/trailing whitespace and newlines
const rawCommand = extractCommand(assistantContent);
// First remove the prompt prefix, then remove any newlines and extra spaces
const command = rawCommand
?.replace(/^user@machine:~\$\s*/g, '') // Remove prompt prefix
?.replace(/\\n/g, '') // Remove escaped newlines
?.replace(/\n/g, '') // Remove actual newlines
?.trim(); // Clean up any remaining whitespace
// Extract and clean the output
const rawOutput = extractCommandOutput(toolContent);
let output = rawOutput;
// Try to parse JSON if the output contains JSON structure
try {
if (rawOutput && rawOutput.includes('"output"')) {
const jsonMatch = rawOutput.match(/"output":\s*"([\s\S]*?)"/);
if (jsonMatch && jsonMatch[1]) {
// Replace escaped newlines with actual newlines
output = jsonMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
}
}
} catch (e) {
// If parsing fails, use the original output
console.error("Error parsing command output:", e);
}
const exitCode = extractExitCode(toolContent);
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<Terminal className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">Execute Command</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden">
<div className="flex items-center p-2 bg-zinc-800 justify-between">
<div className="flex items-center">
<div className="flex space-x-1.5 mr-3 ml-1">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<span className="text-sm font-medium text-gray-200">Terminal</span>
</div>
{exitCode !== null && (
<span className={`text-xs flex items-center ${isSuccess ? 'text-green-400' : 'text-red-400'}`}>
<span className="h-1.5 w-1.5 rounded-full mr-1.5 bg-current"></span>
Exit: {exitCode}
</span>
)}
</div>
<div className="terminal-container overflow-auto max-h-[500px] bg-zinc-900 text-gray-200 font-mono">
<div className="p-4 text-sm">
{command && output && (
<div className="space-y-2">
<div className="flex items-start">
<span className="text-green-400 shrink-0 mr-2">user@machine:~$</span>
<span className="text-gray-200">{command}</span>
</div>
<div className="whitespace-pre-wrap break-words text-gray-300 pl-0">
{output}
</div>
{isSuccess && <div className="text-green-400 mt-1">user@machine:~$ _</div>}
</div>
)}
{command && !output && (
<div className="space-y-2">
<div className="flex items-start">
<span className="text-green-400 shrink-0 mr-2">user@machine:~$</span>
<span className="text-gray-200">{command}</span>
</div>
<div className="flex items-center h-4">
<div className="w-2 h-4 bg-gray-300 animate-pulse"></div>
</div>
</div>
)}
{!command && !output && (
<div className="flex items-start">
<span className="text-green-400 shrink-0 mr-2">user@machine:~$</span>
<span className="w-2 h-4 bg-gray-300 animate-pulse"></span>
</div>
)}
</div>
</div>
{isSuccess && output && exitCode === 0 && (
<div className="border-t border-zinc-700 px-4 py-2 bg-zinc-800 flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
<span className="text-xs text-green-400">Command completed successfully</span>
</div>
)}
{exitCode !== null && !isSuccess && (
<div className="border-t border-zinc-700 px-4 py-2 bg-zinc-800 flex items-center">
<AlertTriangle className="h-4 w-4 text-red-500 mr-2" />
<span className="text-xs text-red-400">Command failed with exit code {exitCode}</span>
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,122 @@
import React from "react";
import { FileCode, FileSymlink, FolderPlus, CheckCircle, AlertTriangle } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractFilePath, extractFileContent, getFileType, formatTimestamp } from "./utils";
import { GenericToolView } from "./GenericToolView";
export function CreateFileToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
const filePath = extractFilePath(assistantContent);
const fileContent = extractFileContent(assistantContent, 'create-file');
if (!filePath || !fileContent) {
return (
<GenericToolView
name="create-file"
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
}
// Split content into lines for line numbering
const contentLines = fileContent.split('\n');
const fileType = getFileType(filePath);
const fileName = filePath.split('/').pop() || filePath;
const isMarkdown = fileName.endsWith('.md');
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<FileCode className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">Create File</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden shadow-sm">
{/* IDE Header */}
<div className="flex items-center p-2 bg-gray-800 text-white justify-between">
<div className="flex items-center">
{isMarkdown ?
<FileCode className="h-4 w-4 mr-2 text-blue-400" /> :
<FileSymlink className="h-4 w-4 mr-2 text-blue-400" />
}
<span className="text-sm font-medium">{fileName}</span>
</div>
<span className="text-xs text-gray-400 bg-gray-700 px-2 py-0.5 rounded">
{fileType}
</span>
</div>
{/* File Path Bar */}
<div className="px-3 py-1.5 border-t border-gray-700 bg-gray-700 flex items-center">
<div className="flex items-center space-x-1 text-gray-300">
<FolderPlus className="h-3.5 w-3.5" />
<code className="text-xs font-mono">{filePath}</code>
</div>
</div>
{/* IDE Content Area */}
<div className="overflow-auto bg-gray-900 max-h-[500px] text-gray-200">
<div className="min-w-full table">
{contentLines.map((line, idx) => (
<div key={idx} className="table-row hover:bg-gray-800/50 group">
<div className="table-cell text-right pr-4 py-0.5 text-xs font-mono text-gray-500 select-none w-12 border-r border-gray-700">
{idx + 1}
</div>
<div className="table-cell pl-4 py-0.5 text-xs font-mono whitespace-pre">
{line || ' '}
</div>
</div>
))}
{/* Add an empty line at the end */}
<div className="table-row h-16"></div>
</div>
</div>
{/* Status Footer */}
{isSuccess ? (
<div className="border-t border-gray-700 px-4 py-2 bg-green-800/20 flex items-center text-green-400">
<CheckCircle className="h-4 w-4 mr-2" />
<span className="text-xs">{fileName} created successfully</span>
</div>
) : (
<div className="border-t border-gray-700 px-4 py-2 bg-red-800/20 flex items-center text-red-400">
<AlertTriangle className="h-4 w-4 mr-2" />
<span className="text-xs">Failed to create file</span>
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,90 @@
import React from "react";
import { FileX, CheckCircle, AlertTriangle } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractFilePath, formatTimestamp } from "./utils";
import { GenericToolView } from "./GenericToolView";
export function DeleteFileToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
const filePath = extractFilePath(assistantContent);
if (!filePath) {
return (
<GenericToolView
name="delete-file"
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
}
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<FileX className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">Delete File</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden">
<div className="flex items-center p-2 bg-red-50 text-red-700 justify-between">
<div className="flex items-center">
<FileX className="h-4 w-4 mr-2" />
<span className="text-sm font-medium">File Deleted</span>
</div>
</div>
<div className="p-4 flex items-center justify-center">
<div className="bg-red-50 rounded-md p-3 text-center text-red-700">
<code className="text-sm font-mono">{filePath}</code>
<p className="text-xs mt-1">This file has been deleted</p>
</div>
</div>
{isSuccess && (
<div className="border-t px-4 py-2 bg-green-50 flex items-center">
<CheckCircle className="h-4 w-4 text-green-600 mr-2" />
<span className="text-xs text-green-700">File deleted successfully</span>
</div>
)}
{!isSuccess && (
<div className="border-t px-4 py-2 bg-red-50 flex items-center">
<AlertTriangle className="h-4 w-4 text-red-600 mr-2" />
<span className="text-xs text-red-700">Failed to delete file</span>
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,122 @@
import React from "react";
import { FileCode, FileSymlink, Replace, CheckCircle, AlertTriangle } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractFilePath, extractFileContent, getFileType, formatTimestamp } from "./utils";
import { GenericToolView } from "./GenericToolView";
export function FileRewriteToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
const filePath = extractFilePath(assistantContent);
const fileContent = extractFileContent(assistantContent, 'full-file-rewrite');
if (!filePath || !fileContent) {
return (
<GenericToolView
name="file-rewrite"
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
}
// Split content into lines for line numbering
const contentLines = fileContent.split('\n');
const fileType = getFileType(filePath);
const fileName = filePath.split('/').pop() || filePath;
const isMarkdown = fileName.endsWith('.md');
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<Replace className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">File Rewrite</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden shadow-sm">
{/* IDE Header */}
<div className="flex items-center p-2 bg-gray-800 text-white justify-between">
<div className="flex items-center">
{isMarkdown ?
<FileCode className="h-4 w-4 mr-2 text-blue-400" /> :
<FileSymlink className="h-4 w-4 mr-2 text-blue-400" />
}
<span className="text-sm font-medium">{fileName}</span>
</div>
<span className="text-xs text-gray-400 bg-gray-700 px-2 py-0.5 rounded">
{fileType}
</span>
</div>
{/* File Path Bar */}
<div className="px-3 py-1.5 border-t border-gray-700 bg-gray-700 flex items-center">
<div className="flex items-center space-x-1 text-gray-300">
<Replace className="h-3.5 w-3.5" />
<code className="text-xs font-mono">{filePath}</code>
</div>
</div>
{/* IDE Content Area */}
<div className="overflow-auto bg-gray-900 max-h-[500px] text-gray-200">
<div className="min-w-full table">
{contentLines.map((line, idx) => (
<div key={idx} className="table-row hover:bg-gray-800/50 group">
<div className="table-cell text-right pr-4 py-0.5 text-xs font-mono text-gray-500 select-none w-12 border-r border-gray-700">
{idx + 1}
</div>
<div className="table-cell pl-4 py-0.5 text-xs font-mono whitespace-pre">
{line || ' '}
</div>
</div>
))}
{/* Add an empty line at the end */}
<div className="table-row h-16"></div>
</div>
</div>
{/* Status Footer */}
{isSuccess ? (
<div className="border-t border-gray-700 px-4 py-2 bg-green-800/20 flex items-center text-green-400">
<CheckCircle className="h-4 w-4 mr-2" />
<span className="text-xs">{fileName} rewritten successfully</span>
</div>
) : (
<div className="border-t border-gray-700 px-4 py-2 bg-red-800/20 flex items-center text-red-400">
<AlertTriangle className="h-4 w-4 mr-2" />
<span className="text-xs">Failed to rewrite file</span>
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,66 @@
import React from "react";
import { ToolViewProps } from "./types";
import { formatTimestamp } from "./utils";
import { getToolIcon } from "../utils";
export function GenericToolView({
name,
assistantContent,
toolContent,
isSuccess = true,
assistantTimestamp,
toolTimestamp
}: ToolViewProps & { name?: string }) {
const toolName = name || 'Unknown Tool';
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
{React.createElement(getToolIcon(toolName), { className: "h-4 w-4" })}
</div>
<div>
<h4 className="text-sm font-medium">{toolName}</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
{/* Assistant Message */}
<div className="space-y-1">
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div>
{assistantTimestamp && (
<div className="text-xs text-muted-foreground">{formatTimestamp(assistantTimestamp)}</div>
)}
</div>
<div className="rounded-md border bg-muted/50 p-3">
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{assistantContent}</pre>
</div>
</div>
{/* Tool Result */}
{toolContent && (
<div className="space-y-1">
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-muted-foreground">Tool Result</div>
{toolTimestamp && (
<div className="text-xs text-muted-foreground">{formatTimestamp(toolTimestamp)}</div>
)}
</div>
<div className={`rounded-md border p-3 ${isSuccess ? 'bg-muted/50' : 'bg-red-50'}`}>
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{toolContent}</pre>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,149 @@
import React from "react";
import { FileSearch, FileDiff, CheckCircle, AlertTriangle } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractFilePath, extractStrReplaceContent, formatTimestamp } from "./utils";
import { GenericToolView } from "./GenericToolView";
export function StrReplaceToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
const filePath = extractFilePath(assistantContent);
const { oldStr, newStr } = extractStrReplaceContent(assistantContent);
if (!oldStr || !newStr) {
return (
<GenericToolView
name="str-replace"
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
}
// Perform a character-level diff to identify changes
const generateDiff = (oldText: string, newText: string) => {
let i = 0;
let j = 0;
// Find common prefix length
let prefixLength = 0;
while (prefixLength < oldText.length && prefixLength < newText.length &&
oldText[prefixLength] === newText[prefixLength]) {
prefixLength++;
}
// Find common suffix length
let oldSuffixStart = oldText.length;
let newSuffixStart = newText.length;
while (oldSuffixStart > prefixLength && newSuffixStart > prefixLength &&
oldText[oldSuffixStart - 1] === newText[newSuffixStart - 1]) {
oldSuffixStart--;
newSuffixStart--;
}
// Generate unified diff parts
const parts = [];
// Add common prefix
if (prefixLength > 0) {
parts.push({ text: oldText.substring(0, prefixLength), type: 'unchanged' });
}
// Add the changed middle parts
if (oldSuffixStart > prefixLength) {
parts.push({ text: oldText.substring(prefixLength, oldSuffixStart), type: 'removed' });
}
if (newSuffixStart > prefixLength) {
parts.push({ text: newText.substring(prefixLength, newSuffixStart), type: 'added' });
}
// Add common suffix
if (oldSuffixStart < oldText.length) {
parts.push({ text: oldText.substring(oldSuffixStart), type: 'unchanged' });
}
return parts;
};
const diffParts = generateDiff(oldStr, newStr);
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<FileSearch className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">String Replace</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden">
<div className="flex items-center p-2 bg-muted justify-between">
<div className="flex items-center">
<FileDiff className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-sm font-medium">File Changes</span>
</div>
</div>
<div className="px-3 py-2 border-t border-b bg-muted/50 flex items-center">
<code className="text-xs font-mono">{filePath || 'Unknown file'}</code>
</div>
<div className="p-3 bg-gray-50 font-mono text-sm">
{diffParts.map((part, i) => (
<span
key={i}
className={
part.type === 'removed' ? 'bg-red-200 text-red-800 line-through mx-0.5' :
part.type === 'added' ? 'bg-green-200 text-green-800 mx-0.5' : ''
}
>
{part.text}
</span>
))}
</div>
{isSuccess && (
<div className="border-t px-4 py-2 bg-green-50 flex items-center">
<CheckCircle className="h-4 w-4 text-green-600 mr-2" />
<span className="text-xs text-green-700">Replacement applied successfully</span>
</div>
)}
{!isSuccess && (
<div className="border-t px-4 py-2 bg-red-50 flex items-center">
<AlertTriangle className="h-4 w-4 text-red-600 mr-2" />
<span className="text-xs text-red-700">Replacement failed</span>
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,149 @@
import React from "react";
import { Globe, ArrowUpRight, Copy, CheckCircle, AlertTriangle } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractCrawlUrl, extractWebpageContent, formatTimestamp } from "./utils";
import { GenericToolView } from "./GenericToolView";
export function WebCrawlToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
const url = extractCrawlUrl(assistantContent);
const webpageContent = extractWebpageContent(toolContent);
if (!url) {
return (
<GenericToolView
name="crawl-webpage"
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
/>
);
}
// Format domain for display
const formatDomain = (url: string): string => {
try {
const urlObj = new URL(url);
return urlObj.hostname.replace('www.', '');
} catch (e) {
return url;
}
};
const domain = url ? formatDomain(url) : 'Unknown';
// Format the extracted text into paragraphs
const formatTextContent = (text: string): React.ReactNode[] => {
if (!text) return [<p key="empty" className="text-gray-400 italic">No content extracted</p>];
return text.split('\n\n').map((paragraph, idx) => {
if (!paragraph.trim()) return null;
return (
<p key={idx} className="mb-3">
{paragraph.trim()}
</p>
);
}).filter(Boolean);
};
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<Globe className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">Web Crawl</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden shadow-sm">
{/* Webpage Header */}
<div className="flex items-center p-2 bg-gray-800 text-white justify-between">
<div className="flex items-center">
<Globe className="h-4 w-4 mr-2 text-blue-400" />
<span className="text-sm font-medium line-clamp-1 pr-2">
{webpageContent?.title || domain}
</span>
</div>
<div className="flex items-center gap-2">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
>
Visit <ArrowUpRight className="h-3 w-3" />
</a>
</div>
</div>
{/* URL Bar */}
<div className="px-3 py-1.5 border-t border-gray-700 bg-gray-700 flex items-center justify-between">
<div className="flex-1 bg-gray-800 rounded px-2 py-1 text-gray-300 flex items-center">
<code className="text-xs font-mono truncate">{url}</code>
</div>
<button className="ml-2 text-gray-400 hover:text-gray-200" title="Copy URL">
<Copy className="h-3.5 w-3.5" />
</button>
</div>
{/* Webpage Content */}
<div className="overflow-auto bg-white max-h-[500px] p-4">
{webpageContent ? (
<div className="prose prose-sm max-w-none">
<h1 className="text-lg font-bold mb-4">{webpageContent.title}</h1>
<div className="text-sm">
{formatTextContent(webpageContent.text)}
</div>
</div>
) : (
<div className="p-6 text-center text-muted-foreground">
<Globe className="h-6 w-6 mx-auto mb-2 opacity-50" />
<p className="text-sm font-medium">No content extracted</p>
</div>
)}
</div>
{/* Status Footer */}
{isSuccess ? (
<div className="border-t px-4 py-2 bg-green-50 flex items-center">
<CheckCircle className="h-4 w-4 text-green-600 mr-2" />
<span className="text-xs text-green-700">Webpage crawled successfully</span>
</div>
) : (
<div className="border-t px-4 py-2 bg-red-50 flex items-center">
<AlertTriangle className="h-4 w-4 text-red-600 mr-2" />
<span className="text-xs text-red-700">Failed to crawl webpage</span>
</div>
)}
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,101 @@
import React from "react";
import { Search } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractSearchQuery, extractSearchResults, cleanUrl, formatTimestamp } from "./utils";
export function WebSearchToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true
}: ToolViewProps) {
const query = extractSearchQuery(assistantContent);
const searchResults = extractSearchResults(toolContent);
return (
<div className="space-y-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<Search className="h-4 w-4" />
</div>
<div>
<h4 className="text-sm font-medium">Web Search</h4>
</div>
</div>
{toolContent && (
<div className={`px-2 py-1 rounded-full text-xs ${
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{isSuccess ? 'Success' : 'Failed'}
</div>
)}
</div>
<div className="border rounded-md overflow-hidden">
<div className="flex items-center p-2 bg-muted justify-between">
<div className="flex items-center">
<Search className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-sm font-medium">Search Results</span>
</div>
</div>
<div className="px-4 py-3 border-t border-b bg-muted/50">
<div className="flex items-center">
<div className="text-sm font-medium mr-2">Query:</div>
<div className="text-sm bg-muted py-1 px-3 rounded-md flex-1">{query || 'Unknown query'}</div>
</div>
<div className="mt-1.5 text-xs text-muted-foreground">
{searchResults.length > 0 ? `Found ${searchResults.length} results` : 'No results found'}
</div>
</div>
<div className="overflow-auto bg-muted/20 max-h-[500px]">
{searchResults.length > 0 ? (
<div className="divide-y">
{searchResults.map((result, idx) => (
<div key={idx} className="p-4 space-y-1.5">
<div className="flex flex-col">
<div className="text-xs text-emerald-600 truncate">
{cleanUrl(result.url)}
</div>
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline font-medium"
>
{result.title}
</a>
</div>
{result.snippet && (
<p className="text-sm text-muted-foreground line-clamp-2">
{result.snippet}
</p>
)}
</div>
))}
</div>
) : (
<div className="p-6 text-center text-muted-foreground">
<Search className="h-6 w-6 mx-auto mb-2 opacity-50" />
<p className="text-sm font-medium">No results found</p>
<p className="text-xs mt-1">Try a different search query</p>
</div>
)}
</div>
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
{assistantTimestamp && (
<div>Called: {formatTimestamp(assistantTimestamp)}</div>
)}
{toolTimestamp && (
<div>Result: {formatTimestamp(toolTimestamp)}</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
import { Project } from "@/lib/api";
export interface ToolViewProps {
assistantContent?: string;
toolContent?: string;
assistantTimestamp?: string;
toolTimestamp?: string;
isSuccess?: boolean;
project?: Project;
}
export interface BrowserToolViewProps extends ToolViewProps {
name?: string;
}

View File

@ -0,0 +1,301 @@
// Helper function to format timestamp
export function formatTimestamp(isoString?: string): string {
if (!isoString) return '';
try {
const date = new Date(isoString);
return isNaN(date.getTime()) ? 'Invalid date' : date.toLocaleString();
} catch (e) {
return 'Invalid date';
}
}
// Helper to extract command from execute-command content
export function extractCommand(content: string | undefined): string | null {
if (!content) return null;
const commandMatch = content.match(/<execute-command>([\s\S]*?)<\/execute-command>/);
return commandMatch ? commandMatch[1].trim() : null;
}
// Helper to extract command output from tool result content
export function extractCommandOutput(content: string | undefined): string | null {
if (!content) return null;
try {
// First try to parse the JSON content
const parsedContent = JSON.parse(content);
if (parsedContent.content && typeof parsedContent.content === 'string') {
// Look for a tool_result tag
const toolResultMatch = parsedContent.content.match(/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/);
if (toolResultMatch) {
return toolResultMatch[1].trim();
}
// Look for output field in a ToolResult pattern
const outputMatch = parsedContent.content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/);
if (outputMatch) {
return outputMatch[1];
}
// Return the content itself as a fallback
return parsedContent.content;
}
} catch (e) {
// If JSON parsing fails, try regex directly
const toolResultMatch = content.match(/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/);
if (toolResultMatch) {
return toolResultMatch[1].trim();
}
const outputMatch = content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/);
if (outputMatch) {
return outputMatch[1];
}
}
return content;
}
// Helper to extract the exit code from tool result
export function extractExitCode(content: string | undefined): number | null {
if (!content) return null;
try {
const exitCodeMatch = content.match(/exit_code=(\d+)/);
if (exitCodeMatch && exitCodeMatch[1]) {
return parseInt(exitCodeMatch[1], 10);
}
return 0; // Assume success if no exit code found but command completed
} catch (e) {
return null;
}
}
// Helper to extract file path from commands
export function extractFilePath(content: string | undefined): string | null {
if (!content) return null;
// Try to parse JSON content first
try {
const parsedContent = JSON.parse(content);
if (parsedContent.content && typeof parsedContent.content === 'string') {
content = parsedContent.content;
}
} catch (e) {
// Continue with original content if parsing fails
}
// Look for file_path in different formats
const filePathMatch = content.match(/file_path=["']([\s\S]*?)["']/);
if (filePathMatch) return filePathMatch[1];
// Look for file_path in XML-like tags
const xmlFilePathMatch = content.match(/<str-replace\s+file_path=["']([\s\S]*?)["']/);
if (xmlFilePathMatch) return xmlFilePathMatch[1];
return null;
}
// Helper to extract str-replace old and new strings
export function extractStrReplaceContent(content: string | undefined): { oldStr: string | null, newStr: string | null } {
if (!content) return { oldStr: null, newStr: null };
const oldMatch = content.match(/<old_str>([\s\S]*?)<\/old_str>/);
const newMatch = content.match(/<new_str>([\s\S]*?)<\/new_str>/);
return {
oldStr: oldMatch ? oldMatch[1] : null,
newStr: newMatch ? newMatch[1] : null
};
}
// Helper to extract file content from create-file or file-rewrite
export function extractFileContent(content: string | undefined, toolType: 'create-file' | 'full-file-rewrite'): string | null {
if (!content) return null;
const tagName = toolType === 'create-file' ? 'create-file' : 'full-file-rewrite';
const contentMatch = content.match(new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i'));
return contentMatch ? contentMatch[1] : null;
}
// Helper to determine file type (for syntax highlighting)
export function getFileType(filePath: string): string {
const extension = filePath.split('.').pop()?.toLowerCase() || '';
switch (extension) {
case 'js': return 'JavaScript';
case 'ts': return 'TypeScript';
case 'jsx': case 'tsx': return 'React';
case 'py': return 'Python';
case 'html': return 'HTML';
case 'css': return 'CSS';
case 'json': return 'JSON';
case 'md': return 'Markdown';
default: return extension.toUpperCase() || 'Text';
}
}
// Helper to extract URL from browser navigate operations
export function extractBrowserUrl(content: string | undefined): string | null {
if (!content) return null;
const urlMatch = content.match(/url=["'](https?:\/\/[^"']+)["']/);
return urlMatch ? urlMatch[1] : null;
}
// Helper to extract browser operation type
export function extractBrowserOperation(toolName: string | undefined): string {
if (!toolName) return 'Browser Operation';
const operation = toolName.replace('browser-', '').replace(/-/g, ' ');
return operation.charAt(0).toUpperCase() + operation.slice(1);
}
// Helper to extract search query
export function extractSearchQuery(content: string | undefined): string | null {
if (!content) return null;
const queryMatch = content.match(/query=["']([\s\S]*?)["']/);
return queryMatch ? queryMatch[1] : null;
}
// Helper to extract search results from tool response
export function extractSearchResults(content: string | undefined): Array<{ title: string, url: string, snippet?: string }> {
if (!content) return [];
try {
// Try to parse JSON content first
const parsedContent = JSON.parse(content);
if (parsedContent.content && typeof parsedContent.content === 'string') {
// Look for a tool_result tag
const toolResultMatch = parsedContent.content.match(/<tool_result>\s*<web-search>([\s\S]*?)<\/web-search>\s*<\/tool_result>/);
if (toolResultMatch) {
// Try to parse the results array
try {
return JSON.parse(toolResultMatch[1]);
} catch (e) {
// Fallback to regex extraction of URLs and titles
return extractUrlsAndTitles(toolResultMatch[1]);
}
}
// Look for ToolResult pattern
const outputMatch = parsedContent.content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/);
if (outputMatch) {
try {
return JSON.parse(outputMatch[1]);
} catch (e) {
return extractUrlsAndTitles(outputMatch[1]);
}
}
// Try to find JSON array in the content
const jsonArrayMatch = parsedContent.content.match(/\[\s*{[\s\S]*}\s*\]/);
if (jsonArrayMatch) {
try {
return JSON.parse(jsonArrayMatch[0]);
} catch (e) {
return [];
}
}
}
} catch (e) {
// If JSON parsing fails, try regex direct extraction
const urlMatch = content.match(/https?:\/\/[^\s"]+/g);
if (urlMatch) {
return urlMatch.map(url => ({
title: cleanUrl(url),
url
}));
}
}
return [];
}
// Helper to extract URLs and titles with regex
export function extractUrlsAndTitles(content: string): Array<{ title: string, url: string, snippet?: string }> {
const results: Array<{ title: string, url: string, snippet?: string }> = [];
// Match URL and title pairs
const urlMatches = content.match(/https?:\/\/[^\s"]+/g) || [];
urlMatches.forEach(url => {
// Try to find a title near this URL
const urlIndex = content.indexOf(url);
const surroundingText = content.substring(Math.max(0, urlIndex - 100), urlIndex + url.length + 100);
// Look for "Title:" or similar patterns
const titleMatch = surroundingText.match(/Title[:\s]+([^\n]+)/i) ||
surroundingText.match(/\"(.*?)\"[\s\n]*?https?:\/\//);
const title = titleMatch ? titleMatch[1] : cleanUrl(url);
results.push({
title: title,
url: url
});
});
return results;
}
// Helper to clean URL for display
export function cleanUrl(url: string): string {
try {
const urlObj = new URL(url);
return urlObj.hostname.replace('www.', '') + (urlObj.pathname !== '/' ? urlObj.pathname : '');
} catch (e) {
return url;
}
}
// Helper to extract URL for webpage crawling
export function extractCrawlUrl(content: string | undefined): string | null {
if (!content) return null;
const urlMatch = content.match(/url=["'](https?:\/\/[^"']+)["']/);
return urlMatch ? urlMatch[1] : null;
}
// Helper to extract webpage content from crawl result
export function extractWebpageContent(content: string | undefined): { title: string, text: string } | null {
if (!content) return null;
try {
// Try to parse the JSON content
const parsedContent = JSON.parse(content);
if (parsedContent.content && typeof parsedContent.content === 'string') {
// Look for tool_result tag
const toolResultMatch = parsedContent.content.match(/<tool_result>\s*<crawl-webpage>([\s\S]*?)<\/crawl-webpage>\s*<\/tool_result>/);
if (toolResultMatch) {
try {
const crawlData = JSON.parse(toolResultMatch[1]);
return {
title: crawlData.title || '',
text: crawlData.text || crawlData.content || ''
};
} catch (e) {
// Fallback to basic text extraction
return {
title: 'Webpage Content',
text: toolResultMatch[1]
};
}
}
}
// Direct content extraction from parsed JSON
if (parsedContent.content) {
return {
title: 'Webpage Content',
text: parsedContent.content
};
}
} catch (e) {
// If JSON parsing fails, return the content as-is
if (content) {
return {
title: 'Webpage Content',
text: content
};
}
}
return null;
}