import React, { useState } from 'react';
import {
FileDiff,
CheckCircle,
AlertTriangle,
ExternalLink,
Loader2,
Code,
Eye,
File,
Copy,
Check,
Minus,
Plus,
} from 'lucide-react';
import {
extractFilePath,
extractFileContent,
extractStreamingFileContent,
formatTimestamp,
getToolTitle,
normalizeContentToString,
extractToolData,
} from '../utils';
import {
MarkdownRenderer,
processUnicodeContent,
} from '@/components/file-renderers/markdown-renderer';
import { CsvRenderer } from '@/components/file-renderers/csv-renderer';
import { XlsxRenderer } from '@/components/file-renderers/xlsx-renderer';
import { cn } from '@/lib/utils';
import { useTheme } from 'next-themes';
import { CodeBlockCode } from '@/components/ui/code-block';
import { constructHtmlPreviewUrl } from '@/lib/utils/url';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
extractFileEditData,
generateLineDiff,
calculateDiffStats,
LineDiff,
DiffStats,
getLanguageFromFileName,
getOperationType,
getOperationConfigs,
getFileIcon,
processFilePath,
getFileName,
getFileExtension,
isFileType,
hasLanguageHighlighting,
splitContentIntoLines,
type FileOperation,
type OperationConfig,
} from './_utils';
import { ToolViewProps } from '../types';
import { GenericToolView } from '../GenericToolView';
import { LoadingState } from '../shared/LoadingState';
import { toast } from 'sonner';
import ReactDiffViewer from 'react-diff-viewer-continued';
const UnifiedDiffView: React.FC<{ oldCode: string; newCode: string }> = ({ oldCode, newCode }) => (
);
const ErrorState: React.FC<{ message?: string }> = ({ message }) => (
Invalid File Edit
{message || "Could not extract the file changes from the tool result."}
);
export function FileEditToolView({
name = 'edit-file',
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true,
isStreaming = false,
project,
}: ToolViewProps): JSX.Element {
const { resolvedTheme } = useTheme();
const isDarkTheme = resolvedTheme === 'dark';
// Add copy functionality state
const [isCopyingContent, setIsCopyingContent] = useState(false);
// Copy functions
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy text: ', err);
return false;
}
};
const handleCopyContent = async () => {
if (!updatedContent) return;
setIsCopyingContent(true);
const success = await copyToClipboard(updatedContent);
if (success) {
toast.success('File content copied to clipboard');
} else {
toast.error('Failed to copy file content');
}
setTimeout(() => setIsCopyingContent(false), 500);
};
const operation = getOperationType(name, assistantContent);
const configs = getOperationConfigs();
const config = configs[operation] || configs['edit']; // fallback to edit config
const Icon = FileDiff; // Always use FileDiff for edit operations
const {
filePath,
originalContent,
updatedContent,
actualIsSuccess,
actualToolTimestamp,
errorMessage,
} = extractFileEditData(
assistantContent,
toolContent,
isSuccess,
toolTimestamp,
assistantTimestamp
);
const toolTitle = getToolTitle(name);
const processedFilePath = processFilePath(filePath);
const fileName = getFileName(processedFilePath);
const fileExtension = getFileExtension(fileName);
const isMarkdown = isFileType.markdown(fileExtension);
const isHtml = isFileType.html(fileExtension);
const isCsv = isFileType.csv(fileExtension);
const isXlsx = isFileType.xlsx(fileExtension);
const language = getLanguageFromFileName(fileName);
const hasHighlighting = hasLanguageHighlighting(language);
const contentLines = splitContentIntoLines(updatedContent);
const htmlPreviewUrl =
isHtml && project?.sandbox?.sandbox_url && processedFilePath
? constructHtmlPreviewUrl(project.sandbox.sandbox_url, processedFilePath)
: undefined;
const FileIcon = getFileIcon(fileName);
const lineDiff = originalContent && updatedContent ? generateLineDiff(originalContent, updatedContent) : [];
const stats: DiffStats = calculateDiffStats(lineDiff);
const shouldShowError = !isStreaming && (!actualIsSuccess || (actualIsSuccess && (originalContent === null || updatedContent === null)));
if (!isStreaming && !processedFilePath && !updatedContent) {
return (
);
}
const renderFilePreview = () => {
if (!updatedContent) {
return (
);
}
if (isHtml && htmlPreviewUrl) {
return (
);
}
if (isMarkdown) {
return (
);
}
if (isCsv) {
return (
);
}
if (isXlsx) {
return (
);
}
return (
{processUnicodeContent(updatedContent)}
);
};
const renderSourceCode = () => {
if (!originalContent || !updatedContent) {
return (
);
}
// Show unified diff view in source tab
return (
);
};
return (
{isHtml && htmlPreviewUrl && !isStreaming && (
)}
{/* Copy button - only show when there's file content */}
{updatedContent && !isStreaming && (
)}
{/* Diff mode selector for source tab */}
{originalContent && updatedContent && (
{stats.additions === 0 && stats.deletions === 0 && (
No changes
)}
)}
Source
Preview
{isStreaming && !updatedContent ? (
) : shouldShowError ? (
) : (
renderSourceCode()
)}
{isStreaming && !updatedContent ? (
) : shouldShowError ? (
) : (
renderFilePreview()
)}
{isStreaming && updatedContent && (
Streaming...
)}
{hasHighlighting ? language.toUpperCase() : fileExtension.toUpperCase() || 'TEXT'}
{actualToolTimestamp && !isStreaming
? formatTimestamp(actualToolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
);
}