llama1's picture
Upload 781 files
5da4770 verified
import React, { useState } from 'react';
import {
CheckCircle,
AlertTriangle,
ExternalLink,
Loader2,
Code,
Eye,
File,
} 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 { 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 {
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';
export function FileOperationToolView({
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true,
isStreaming = false,
name,
project,
}: ToolViewProps) {
const { resolvedTheme } = useTheme();
const isDarkTheme = resolvedTheme === 'dark';
const operation = getOperationType(name, assistantContent);
const configs = getOperationConfigs();
const config = configs[operation];
const Icon = config.icon;
let filePath: string | null = null;
let fileContent: string | null = null;
const assistantToolData = extractToolData(assistantContent);
const toolToolData = extractToolData(toolContent);
if (assistantToolData.toolResult) {
filePath = assistantToolData.filePath;
fileContent = assistantToolData.fileContent;
} else if (toolToolData.toolResult) {
filePath = toolToolData.filePath;
fileContent = toolToolData.fileContent;
}
if (!filePath) {
filePath = extractFilePath(assistantContent);
}
if (!fileContent && operation !== 'delete') {
fileContent = isStreaming
? extractStreamingFileContent(
assistantContent,
operation === 'create' ? 'create-file' : 'full-file-rewrite',
) || ''
: extractFileContent(
assistantContent,
operation === 'create' ? 'create-file' : 'full-file-rewrite',
);
}
const toolTitle = getToolTitle(name || `file-${operation}`);
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 language = getLanguageFromFileName(fileName);
const hasHighlighting = hasLanguageHighlighting(language);
const contentLines = splitContentIntoLines(fileContent);
const htmlPreviewUrl =
isHtml && project?.sandbox?.sandbox_url && processedFilePath
? constructHtmlPreviewUrl(project.sandbox.sandbox_url, processedFilePath)
: undefined;
const FileIcon = getFileIcon(fileName);
if (!isStreaming && !processedFilePath && !fileContent) {
return (
<GenericToolView
name={name || `file-${operation}`}
assistantContent={assistantContent}
toolContent={toolContent}
assistantTimestamp={assistantTimestamp}
toolTimestamp={toolTimestamp}
isSuccess={isSuccess}
isStreaming={isStreaming}
/>
);
}
const renderFilePreview = () => {
if (!fileContent) {
return (
<div className="flex items-center justify-center h-full p-12">
<div className="text-center">
<FileIcon className="h-12 w-12 mx-auto mb-4 text-zinc-400" />
<p className="text-sm text-zinc-500 dark:text-zinc-400">No content to preview</p>
</div>
</div>
);
}
if (isHtml && htmlPreviewUrl) {
return (
<div className="flex flex-col h-[calc(100vh-16rem)]">
<iframe
src={htmlPreviewUrl}
title={`HTML Preview of ${fileName}`}
className="flex-grow border-0"
sandbox="allow-same-origin allow-scripts"
/>
</div>
);
}
if (isMarkdown) {
return (
<div className="p-1 py-0 prose dark:prose-invert prose-zinc max-w-none">
<MarkdownRenderer
content={processUnicodeContent(fileContent)}
/>
</div>
);
}
if (isCsv) {
return (
<div className="h-full w-full p-4">
<div className="h-[calc(100vh-17rem)] w-full bg-muted/20 border rounded-xl overflow-auto">
<CsvRenderer content={processUnicodeContent(fileContent)} />
</div>
</div>
);
}
return (
<div className="p-4">
<div className='w-full h-full bg-muted/20 border rounded-xl px-4 py-2 pb-6'>
<pre className="text-sm font-mono text-zinc-800 dark:text-zinc-300 whitespace-pre-wrap break-words">
{processUnicodeContent(fileContent)}
</pre>
</div>
</div>
);
};
const renderDeleteOperation = () => (
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
<div className={cn("w-20 h-20 rounded-full flex items-center justify-center mb-6", config.bgColor)}>
<Icon className={cn("h-10 w-10", config.color)} />
</div>
<h3 className="text-xl font-semibold mb-6 text-zinc-900 dark:text-zinc-100">
File Deleted
</h3>
<div className="bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-4 w-full max-w-md text-center mb-4 shadow-sm">
<code className="text-sm font-mono text-zinc-700 dark:text-zinc-300 break-all">
{processedFilePath || 'Unknown file path'}
</code>
</div>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
This file has been permanently removed
</p>
</div>
);
const renderSourceCode = () => {
if (!fileContent) {
return (
<div className="flex items-center justify-center h-full p-12">
<div className="text-center">
<FileIcon className="h-12 w-12 mx-auto mb-4 text-zinc-400" />
<p className="text-sm text-zinc-500 dark:text-zinc-400">No source code to display</p>
</div>
</div>
);
}
if (hasHighlighting) {
return (
<div className="relative">
<div className="absolute left-0 top-0 bottom-0 w-12 border-r border-zinc-200 dark:border-zinc-800 z-10 flex flex-col bg-zinc-50 dark:bg-zinc-900">
{contentLines.map((_, idx) => (
<div
key={idx}
className="h-6 text-right pr-3 text-xs font-mono text-zinc-500 dark:text-zinc-500 select-none"
>
{idx + 1}
</div>
))}
</div>
<div className="pl-12">
<CodeBlockCode
code={processUnicodeContent(fileContent)}
language={language}
className="text-xs"
/>
</div>
</div>
);
}
return (
<div className="min-w-full table">
{contentLines.map((line, idx) => (
<div
key={idx}
className={cn("table-row transition-colors", config.hoverColor)}
>
<div className="table-cell text-right pr-3 pl-6 py-0.5 text-xs font-mono text-zinc-500 dark:text-zinc-500 select-none w-12 border-r border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900">
{idx + 1}
</div>
<div className="table-cell pl-3 py-0.5 pr-4 text-xs font-mono whitespace-pre-wrap text-zinc-800 dark:text-zinc-300">
{processUnicodeContent(line) || ' '}
</div>
</div>
))}
<div className="table-row h-4"></div>
</div>
);
};
return (
<Card className="flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card">
<Tabs defaultValue={'preview'} className="w-full h-full">
<CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2 mb-0">
<div className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<div className={cn("relative p-2 rounded-lg border", config.gradientBg, config.borderColor)}>
<Icon className={cn("h-5 w-5", config.color)} />
</div>
<div>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{toolTitle}
</CardTitle>
</div>
</div>
<div className='flex items-center gap-2'>
{isHtml && htmlPreviewUrl && !isStreaming && (
<Button variant="outline" size="sm" className="h-8 text-xs bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" asChild>
<a href={htmlPreviewUrl} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3.5 w-3.5 mr-1.5" />
Open in Browser
</a>
</Button>
)}
<TabsList className="-mr-2 h-7 bg-zinc-100/70 dark:bg-zinc-800/70 rounded-lg">
<TabsTrigger value="code" className="rounded-md data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-900 data-[state=active]:text-primary">
<Code className="h-4 w-4" />
Source
</TabsTrigger>
<TabsTrigger value="preview" className="rounded-md data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-900 data-[state=active]:text-primary">
<Eye className="h-4 w-4" />
Preview
</TabsTrigger>
</TabsList>
</div>
</div>
</CardHeader>
<CardContent className="p-0 -my-2 h-full flex-1 overflow-hidden relative">
<TabsContent value="code" className="flex-1 h-full mt-0 p-0 overflow-hidden">
<ScrollArea className="h-screen w-full min-h-0">
{isStreaming && !fileContent ? (
<LoadingState
icon={Icon}
iconColor={config.color}
bgColor={config.bgColor}
title={config.progressMessage}
filePath={processedFilePath || 'Processing file...'}
subtitle="Please wait while the file is being processed"
showProgress={false}
/>
) : operation === 'delete' ? (
<div className="flex flex-col items-center justify-center h-full py-12 px-6">
<div className={cn("w-20 h-20 rounded-full flex items-center justify-center mb-6", config.bgColor)}>
<Icon className={cn("h-10 w-10", config.color)} />
</div>
<h3 className="text-xl font-semibold mb-6 text-zinc-900 dark:text-zinc-100">
Delete Operation
</h3>
<div className="bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-4 w-full max-w-md text-center">
<code className="text-sm font-mono text-zinc-700 dark:text-zinc-300 break-all">
{processedFilePath || 'Unknown file path'}
</code>
</div>
</div>
) : (
renderSourceCode()
)}
</ScrollArea>
</TabsContent>
<TabsContent value="preview" className="w-full flex-1 h-full mt-0 p-0 overflow-hidden">
<ScrollArea className="h-full w-full min-h-0">
{isStreaming && !fileContent ? (
<LoadingState
icon={Icon}
iconColor={config.color}
bgColor={config.bgColor}
title={config.progressMessage}
filePath={processedFilePath || 'Processing file...'}
subtitle="Please wait while the file is being processed"
showProgress={false}
/>
) : operation === 'delete' ? (
renderDeleteOperation()
) : (
renderFilePreview()
)}
{isStreaming && fileContent && (
<div className="sticky bottom-4 right-4 float-right mr-4 mb-4">
<Badge className="bg-blue-500/90 text-white border-none shadow-lg animate-pulse">
<Loader2 className="h-3 w-3 animate-spin mr-1" />
Streaming...
</Badge>
</div>
)}
</ScrollArea>
</TabsContent>
</CardContent>
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
<Badge variant="outline" className="py-0.5 h-6">
<FileIcon className="h-3 w-3" />
{hasHighlighting ? language.toUpperCase() : fileExtension.toUpperCase() || 'TEXT'}
</Badge>
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{toolTimestamp && !isStreaming
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
</div>
</Tabs>
</Card>
);
}