mirror of https://github.com/kortix-ai/suna.git
update file ops
This commit is contained in:
parent
67f81d10a0
commit
d876086d21
|
@ -9,9 +9,7 @@ import { Slider } from "@/components/ui/slider";
|
|||
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 { FileOperationToolView } from "./tool-views/FileOperationToolView";
|
||||
import { BrowserToolView } from "./tool-views/BrowserToolView";
|
||||
import { WebSearchToolView } from "./tool-views/WebSearchToolView";
|
||||
import { WebCrawlToolView } from "./tool-views/WebCrawlToolView";
|
||||
|
@ -66,33 +64,32 @@ function getToolView(
|
|||
/>
|
||||
);
|
||||
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
|
||||
<FileOperationToolView
|
||||
assistantContent={assistantContent}
|
||||
toolContent={toolContent}
|
||||
assistantTimestamp={assistantTimestamp}
|
||||
toolTimestamp={toolTimestamp}
|
||||
isSuccess={isSuccess}
|
||||
name={normalizedToolName}
|
||||
/>
|
||||
);
|
||||
case 'browser-navigate':
|
||||
case 'browser-click':
|
||||
case 'browser-extract':
|
||||
case 'browser-fill':
|
||||
case 'browser-wait':
|
||||
return (
|
||||
<BrowserToolView
|
||||
name={normalizedToolName}
|
||||
assistantContent={assistantContent}
|
||||
toolContent={toolContent}
|
||||
assistantTimestamp={assistantTimestamp}
|
||||
toolTimestamp={toolTimestamp}
|
||||
isSuccess={isSuccess}
|
||||
project={project}
|
||||
/>
|
||||
);
|
||||
case 'web-search':
|
||||
|
@ -105,7 +102,7 @@ function getToolView(
|
|||
isSuccess={isSuccess}
|
||||
/>
|
||||
);
|
||||
case 'crawl-webpage':
|
||||
case 'web-crawl':
|
||||
return (
|
||||
<WebCrawlToolView
|
||||
assistantContent={assistantContent}
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
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,277 @@
|
|||
import React from "react";
|
||||
import { FileCode, FileSymlink, FolderPlus, FileX, Replace, CheckCircle, AlertTriangle } from "lucide-react";
|
||||
import { ToolViewProps } from "./types";
|
||||
import { extractFilePath, extractFileContent, getFileType, formatTimestamp } from "./utils";
|
||||
import { GenericToolView } from "./GenericToolView";
|
||||
|
||||
// Type for operation type
|
||||
type FileOperation = "create" | "rewrite" | "delete";
|
||||
|
||||
export function FileOperationToolView({
|
||||
assistantContent,
|
||||
toolContent,
|
||||
assistantTimestamp,
|
||||
toolTimestamp,
|
||||
isSuccess = true,
|
||||
name
|
||||
}: ToolViewProps & { name?: string }) {
|
||||
// Determine operation type from content or name
|
||||
const getOperationType = (): FileOperation => {
|
||||
// First check tool name if available
|
||||
if (name) {
|
||||
if (name.includes("create")) return "create";
|
||||
if (name.includes("rewrite")) return "rewrite";
|
||||
if (name.includes("delete")) return "delete";
|
||||
}
|
||||
|
||||
if (!assistantContent) return "create"; // default fallback
|
||||
|
||||
if (assistantContent.includes("<create-file>")) return "create";
|
||||
if (assistantContent.includes("<full-file-rewrite>")) return "rewrite";
|
||||
if (assistantContent.includes("delete-file") || assistantContent.includes("<delete>")) return "delete";
|
||||
|
||||
// Check for tool names as a fallback
|
||||
if (assistantContent.toLowerCase().includes("create file")) return "create";
|
||||
if (assistantContent.toLowerCase().includes("rewrite file")) return "rewrite";
|
||||
if (assistantContent.toLowerCase().includes("delete file")) return "delete";
|
||||
|
||||
// Default to create if we can't determine
|
||||
return "create";
|
||||
};
|
||||
|
||||
const operation = getOperationType();
|
||||
const filePath = extractFilePath(assistantContent);
|
||||
|
||||
// Only extract content for create and rewrite operations
|
||||
const fileContent = operation !== "delete"
|
||||
? extractFileContent(assistantContent, operation === "create" ? 'create-file' : 'full-file-rewrite')
|
||||
: null;
|
||||
|
||||
// For debugging - show raw content if file path can't be extracted for delete operations
|
||||
const showDebugInfo = !filePath && operation === "delete";
|
||||
|
||||
// Fall back to generic view if file path is missing or if content is missing for non-delete operations
|
||||
if ((!filePath && !showDebugInfo) || (operation !== "delete" && !fileContent)) {
|
||||
return (
|
||||
<GenericToolView
|
||||
name={`file-${operation}`}
|
||||
assistantContent={assistantContent}
|
||||
toolContent={toolContent}
|
||||
assistantTimestamp={assistantTimestamp}
|
||||
toolTimestamp={toolTimestamp}
|
||||
isSuccess={isSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Operation-specific configs
|
||||
const configs = {
|
||||
create: {
|
||||
title: "Create File",
|
||||
icon: FolderPlus,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-50",
|
||||
successMessage: "File created successfully"
|
||||
},
|
||||
rewrite: {
|
||||
title: "Rewrite File",
|
||||
icon: Replace,
|
||||
color: "text-amber-500",
|
||||
bgColor: "bg-amber-50",
|
||||
successMessage: "File rewritten successfully"
|
||||
},
|
||||
delete: {
|
||||
title: "Delete File",
|
||||
icon: FileX,
|
||||
color: "text-red-500",
|
||||
bgColor: "bg-red-50",
|
||||
successMessage: "File deleted successfully"
|
||||
}
|
||||
};
|
||||
|
||||
const config = configs[operation];
|
||||
|
||||
// Process file path - handle potential newlines and clean up
|
||||
const processedFilePath = filePath ? filePath.trim().replace(/\\n/g, '\n').split('\n')[0] : null;
|
||||
|
||||
// For create and rewrite, prepare content for display
|
||||
const contentLines = fileContent ? fileContent.replace(/\\n/g, '\n').split('\n') : [];
|
||||
const fileName = processedFilePath ? processedFilePath.split('/').pop() || processedFilePath : '';
|
||||
const fileType = processedFilePath ? getFileType(processedFilePath) : '';
|
||||
const isMarkdown = fileName.endsWith('.md');
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`h-8 w-8 rounded-full ${config.bgColor} flex items-center justify-center`}>
|
||||
<Icon className={`h-4 w-4 ${config.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{config.title}</h4>
|
||||
<p className="text-xs text-gray-500 break-all">{processedFilePath}</p>
|
||||
</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>
|
||||
|
||||
{/* File Content for create and rewrite operations */}
|
||||
{operation !== "delete" && fileContent && (
|
||||
<div className="border rounded-lg overflow-hidden shadow-sm bg-slate-900">
|
||||
{/* IDE Header */}
|
||||
<div className="flex items-center p-2 bg-slate-800 text-white justify-between border-b border-slate-700">
|
||||
<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-300 bg-slate-700 px-2 py-0.5 rounded">
|
||||
{fileType}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* File Content */}
|
||||
<div className="overflow-auto max-h-[400px] bg-slate-900 text-slate-200">
|
||||
<div className="min-w-full table">
|
||||
{contentLines.map((line, idx) => (
|
||||
<div key={idx} className="table-row hover:bg-slate-800 transition-colors">
|
||||
<div className="table-cell text-right pr-3 py-0.5 text-xs font-mono text-slate-500 select-none w-12 border-r border-slate-700">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div className="table-cell pl-3 py-0.5 text-xs font-mono whitespace-pre">
|
||||
{line || ' '}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="table-row h-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debug info for delete operations with missing file path */}
|
||||
{showDebugInfo && (
|
||||
<div className="border rounded-lg overflow-hidden shadow-sm mb-4">
|
||||
<div className="p-4 bg-yellow-50 border-b border-yellow-200">
|
||||
<h3 className="text-sm font-medium text-yellow-800">Debug Info: Unable to extract file path</h3>
|
||||
</div>
|
||||
<div className="p-4 bg-white">
|
||||
<h4 className="text-xs font-semibold mb-2">Raw Assistant Content:</h4>
|
||||
<pre className="text-xs bg-gray-50 p-3 rounded border overflow-auto max-h-[200px]">
|
||||
{assistantContent || "No content"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete view with unknown path */}
|
||||
{operation === "delete" && !processedFilePath && (
|
||||
<div className="border rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="p-6 flex flex-col items-center justify-center bg-gray-900 text-white">
|
||||
<div className="w-14 h-14 rounded-full bg-red-900/30 flex items-center justify-center mb-4">
|
||||
<FileX className="h-7 w-7 text-red-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-4 text-red-300">File Deleted</h3>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-md p-4 w-full max-w-md text-center mb-2">
|
||||
<p className="text-sm text-gray-300">Unknown file path</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-2">A file has been deleted but the path could not be determined</p>
|
||||
</div>
|
||||
|
||||
{/* Status footer */}
|
||||
<div className={`p-3 border-t ${
|
||||
isSuccess ? 'border-green-800 bg-green-900/20 text-green-400' : 'border-red-800 bg-red-900/20 text-red-400'
|
||||
}`}>
|
||||
<div className="flex items-center">
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{isSuccess ? config.successMessage : `Failed to delete file`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete view */}
|
||||
{operation === "delete" && processedFilePath && (
|
||||
<div className="border rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="p-6 flex flex-col items-center justify-center bg-gray-900 text-white">
|
||||
<div className="w-14 h-14 rounded-full bg-red-900/30 flex items-center justify-center mb-4">
|
||||
<FileX className="h-7 w-7 text-red-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-4 text-red-300">File Deleted</h3>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-md p-4 w-full max-w-md text-center mb-2">
|
||||
<code className="text-sm font-mono text-gray-300 break-all">{processedFilePath}</code>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-2">This file has been permanently removed</p>
|
||||
</div>
|
||||
|
||||
{/* Status footer */}
|
||||
<div className={`p-3 border-t ${
|
||||
isSuccess ? 'border-green-800 bg-green-900/20 text-green-400' : 'border-red-800 bg-red-900/20 text-red-400'
|
||||
}`}>
|
||||
<div className="flex items-center">
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{isSuccess ? config.successMessage : `Failed to delete file`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status footer - only show for non-delete operations as delete has its own */}
|
||||
{operation !== "delete" && (
|
||||
<div className={`mt-2 p-3 rounded-md border ${
|
||||
isSuccess ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'
|
||||
}`}>
|
||||
<div className="flex items-center">
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-600 mr-2 flex-shrink-0" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 text-red-600 mr-2 flex-shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm ${isSuccess ? 'text-green-700' : 'text-red-700'}`}>
|
||||
{isSuccess ? config.successMessage : `Failed to ${operation} file`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="flex justify-between items-center text-xs text-gray-500 pt-1">
|
||||
{assistantTimestamp && (
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium mr-1">Requested:</span>
|
||||
{formatTimestamp(assistantTimestamp)}
|
||||
</div>
|
||||
)}
|
||||
{toolTimestamp && (
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium mr-1">Completed:</span>
|
||||
{formatTimestamp(toolTimestamp)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -85,16 +85,53 @@ export function extractFilePath(content: string | undefined): string | null {
|
|||
}
|
||||
|
||||
// Look for file_path in different formats
|
||||
const filePathMatch = content.match(/file_path=["']([\s\S]*?)["']/);
|
||||
if (filePathMatch) return filePathMatch[1];
|
||||
const filePathMatch = content.match(/file_path=["']([\s\S]*?)["']/i) ||
|
||||
content.match(/target_file=["']([\s\S]*?)["']/i) ||
|
||||
content.match(/path=["']([\s\S]*?)["']/i);
|
||||
if (filePathMatch) {
|
||||
const path = filePathMatch[1].trim();
|
||||
// Handle newlines and return first line if multiple lines
|
||||
return cleanFilePath(path);
|
||||
}
|
||||
|
||||
// Look for file_path in XML-like tags
|
||||
const xmlFilePathMatch = content.match(/<str-replace\s+file_path=["']([\s\S]*?)["']/);
|
||||
if (xmlFilePathMatch) return xmlFilePathMatch[1];
|
||||
const xmlFilePathMatch = content.match(/<str-replace\s+file_path=["']([\s\S]*?)["']/i) ||
|
||||
content.match(/<delete[^>]*file_path=["']([\s\S]*?)["']/i) ||
|
||||
content.match(/<delete-file[^>]*>([^<]+)<\/delete-file>/i);
|
||||
if (xmlFilePathMatch) {
|
||||
return cleanFilePath(xmlFilePathMatch[1]);
|
||||
}
|
||||
|
||||
// Look for file paths in delete operations in particular
|
||||
if (content.toLowerCase().includes('delete') || content.includes('delete-file')) {
|
||||
// Look for patterns like "Deleting file: path/to/file.txt"
|
||||
const deletePathMatch = content.match(/(?:delete|remove|deleting)\s+(?:file|the file)?:?\s+["']?([\w\-./\\]+\.\w+)["']?/i);
|
||||
if (deletePathMatch) return cleanFilePath(deletePathMatch[1]);
|
||||
|
||||
// Look for isolated file paths with extensions
|
||||
const fileMatch = content.match(/["']?([\w\-./\\]+\.\w+)["']?/);
|
||||
if (fileMatch) return cleanFilePath(fileMatch[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to clean and process a file path string, handling escaped chars
|
||||
function cleanFilePath(path: string): string {
|
||||
if (!path) return path;
|
||||
|
||||
// Handle escaped newlines and other escaped characters
|
||||
return path
|
||||
.replace(/\\n/g, '\n') // Replace \n with actual newlines
|
||||
.replace(/\\t/g, '\t') // Replace \t with actual tabs
|
||||
.replace(/\\r/g, '') // Remove \r
|
||||
.replace(/\\\\/g, '\\') // Replace \\ with \
|
||||
.replace(/\\"/g, '"') // Replace \" with "
|
||||
.replace(/\\'/g, "'") // Replace \' with '
|
||||
.split('\n')[0] // Take only the first line if multiline
|
||||
.trim(); // Trim whitespace
|
||||
}
|
||||
|
||||
// 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 };
|
||||
|
@ -114,7 +151,26 @@ export function extractFileContent(content: string | undefined, toolType: 'creat
|
|||
|
||||
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;
|
||||
|
||||
if (contentMatch && contentMatch[1]) {
|
||||
return processFileContent(contentMatch[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to process and clean file content
|
||||
function processFileContent(content: string): string {
|
||||
if (!content) return content;
|
||||
|
||||
// Handle escaped characters
|
||||
return content
|
||||
.replace(/\\n/g, '\n') // Replace \n with actual newlines
|
||||
.replace(/\\t/g, '\t') // Replace \t with actual tabs
|
||||
.replace(/\\r/g, '') // Remove \r
|
||||
.replace(/\\\\/g, '\\') // Replace \\ with \
|
||||
.replace(/\\"/g, '"') // Replace \" with "
|
||||
.replace(/\\'/g, "'"); // Replace \' with '
|
||||
}
|
||||
|
||||
// Helper to determine file type (for syntax highlighting)
|
||||
|
|
Loading…
Reference in New Issue