mirror of https://github.com/kortix-ai/suna.git
tools components
This commit is contained in:
parent
2b3556facc
commit
6570ce2b62
|
@ -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'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>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue