diff --git a/frontend/src/components/thread/file-attachment.tsx b/frontend/src/components/thread/file-attachment.tsx index f773ed5c..7dcfdf06 100644 --- a/frontend/src/components/thread/file-attachment.tsx +++ b/frontend/src/components/thread/file-attachment.tsx @@ -254,7 +254,7 @@ export function FileAttachment({ const handleDownload = async (e: React.MouseEvent) => { e.stopPropagation(); // Prevent triggering the main click handler - + try { if (!sandboxId || !session?.access_token) { // Fallback: open file URL in new tab @@ -451,8 +451,9 @@ export function FileAttachment({ className={cn( "group relative w-full", "rounded-xl border bg-card overflow-hidden pt-10", // Consistent card styling with header space - isPdf ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[500px] sm:!min-w-[300px]" : - standalone ? "min-h-[300px] h-auto" : "h-[300px]", // Better height handling for standalone + isPdf ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[500px] sm:!min-w-[300px]" : + isHtmlOrMd ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[600px] sm:!min-w-[300px]" : + standalone ? "min-h-[300px] h-auto" : "h-[300px]", // Better height handling for standalone className )} style={{ @@ -469,8 +470,8 @@ export function FileAttachment({ style={{ minWidth: 0, width: '100%', - containIntrinsicSize: isPdf ? '100% 500px' : undefined, - contain: isPdf ? 'layout size' : undefined + containIntrinsicSize: (isPdf || isHtmlOrMd) ? '100% 500px' : undefined, + contain: (isPdf || isHtmlOrMd) ? 'layout size' : undefined }} > {/* Render PDF or text-based previews */} diff --git a/frontend/src/components/thread/tool-views/file-operation/FileEditToolView.tsx b/frontend/src/components/thread/tool-views/file-operation/FileEditToolView.tsx index fea802f5..9cca435f 100644 --- a/frontend/src/components/thread/tool-views/file-operation/FileEditToolView.tsx +++ b/frontend/src/components/thread/tool-views/file-operation/FileEditToolView.tsx @@ -3,29 +3,68 @@ import { FileDiff, CheckCircle, AlertTriangle, + ExternalLink, Loader2, + Code, + Eye, File, - ChevronDown, - ChevronUp, + 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 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 + DiffStats, + getLanguageFromFileName, + getOperationType, + getOperationConfigs, + getFileIcon, + processFilePath, + getFileName, + getFileExtension, + isFileType, + hasLanguageHighlighting, + splitContentIntoLines, + type FileOperation, + type OperationConfig, } from './_utils'; -import { formatTimestamp, getToolTitle } 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 }) => ( @@ -60,40 +99,6 @@ const UnifiedDiffView: React.FC<{ oldCode: string; newCode: string }> = ({ oldCo /> ); -const SplitDiffView: React.FC<{ oldCode: string; newCode: string }> = ({ oldCode, newCode }) => ( - -); - const ErrorState: React.FC<{ message?: string }> = ({ message }) => (
@@ -116,8 +121,42 @@ export function FileEditToolView({ toolTimestamp, isSuccess = true, isStreaming = false, + project, }: ToolViewProps): JSX.Element { - const [viewMode, setViewMode] = useState<'unified' | 'split'>('unified'); + 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, @@ -135,103 +174,275 @@ export function FileEditToolView({ ); 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))); - return ( - - -
-
-
- -
- - {toolTitle} - -
+ if (!isStreaming && !processedFilePath && !updatedContent) { + return ( + + ); + } - {!isStreaming && ( - - {actualIsSuccess ? ( - - ) : ( - - )} - {actualIsSuccess ? 'Edit applied' : 'Edit failed'} - - )} + const renderFilePreview = () => { + if (!updatedContent) { + return ( +
+
+ +

No content to preview

+
- + ); + } - - {isStreaming ? ( - +