"use client"; import { useEffect, useState, useCallback, useMemo } from "react"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Plus, RefreshCw, FileText, Image as ImageIcon, Trash2, Save, Upload, File, X, BookOpen, } from "lucide-react"; import { useKeyboardShortcuts, ACTION_SHORTCUTS, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; interface ContextFile { name: string; type: "text" | "image"; content?: string; path: string; } export function ContextView() { const { currentProject } = useAppStore(); const [contextFiles, setContextFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [editedContent, setEditedContent] = useState(""); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [newFileName, setNewFileName] = useState(""); const [newFileType, setNewFileType] = useState<"text" | "image">("text"); const [uploadedImageData, setUploadedImageData] = useState( null ); const [newFileContent, setNewFileContent] = useState(""); const [isDropHovering, setIsDropHovering] = useState(false); // Keyboard shortcuts for this view const contextShortcuts: KeyboardShortcut[] = useMemo( () => [ { key: ACTION_SHORTCUTS.addContextFile, action: () => setIsAddDialogOpen(true), description: "Add new context file", }, ], [] ); useKeyboardShortcuts(contextShortcuts); // Get context directory path for user-added context files const getContextPath = useCallback(() => { if (!currentProject) return null; return `${currentProject.path}/.automaker/context`; }, [currentProject]); // Determine if a file is an image based on extension const isImageFile = (filename: string): boolean => { const imageExtensions = [ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ]; const ext = filename.toLowerCase().substring(filename.lastIndexOf(".")); return imageExtensions.includes(ext); }; // Load context files const loadContextFiles = useCallback(async () => { const contextPath = getContextPath(); if (!contextPath) return; setIsLoading(true); try { const api = getElectronAPI(); // Ensure context directory exists await api.mkdir(contextPath); // Read directory contents const result = await api.readdir(contextPath); if (result.success && result.entries) { const files: ContextFile[] = result.entries .filter((entry) => entry.isFile) .map((entry) => ({ name: entry.name, type: isImageFile(entry.name) ? "image" : "text", path: `${contextPath}/${entry.name}`, })); setContextFiles(files); } } catch (error) { console.error("Failed to load context files:", error); } finally { setIsLoading(false); } }, [getContextPath]); useEffect(() => { loadContextFiles(); }, [loadContextFiles]); // Load selected file content const loadFileContent = useCallback(async (file: ContextFile) => { try { const api = getElectronAPI(); const result = await api.readFile(file.path); if (result.success && result.content !== undefined) { setEditedContent(result.content); setSelectedFile({ ...file, content: result.content }); setHasChanges(false); } } catch (error) { console.error("Failed to load file content:", error); } }, []); // Select a file const handleSelectFile = (file: ContextFile) => { if (hasChanges) { // Could add a confirmation dialog here } loadFileContent(file); }; // Save current file const saveFile = async () => { if (!selectedFile) return; setIsSaving(true); try { const api = getElectronAPI(); await api.writeFile(selectedFile.path, editedContent); setSelectedFile({ ...selectedFile, content: editedContent }); setHasChanges(false); } catch (error) { console.error("Failed to save file:", error); } finally { setIsSaving(false); } }; // Handle content change const handleContentChange = (value: string) => { setEditedContent(value); setHasChanges(true); }; // Add new context file const handleAddFile = async () => { const contextPath = getContextPath(); if (!contextPath || !newFileName.trim()) return; try { const api = getElectronAPI(); let filename = newFileName.trim(); // Add default extension if not provided if (newFileType === "text" && !filename.includes(".")) { filename += ".md"; } const filePath = `${contextPath}/${filename}`; if (newFileType === "image" && uploadedImageData) { // Write image data await api.writeFile(filePath, uploadedImageData); } else { // Write text file with content (or empty if no content) await api.writeFile(filePath, newFileContent); } setIsAddDialogOpen(false); setNewFileName(""); setNewFileType("text"); setUploadedImageData(null); setNewFileContent(""); setIsDropHovering(false); await loadContextFiles(); } catch (error) { console.error("Failed to add file:", error); } }; // Delete selected file const handleDeleteFile = async () => { if (!selectedFile) return; try { const api = getElectronAPI(); await api.deleteFile(selectedFile.path); setIsDeleteDialogOpen(false); setSelectedFile(null); setEditedContent(""); setHasChanges(false); await loadContextFiles(); } catch (error) { console.error("Failed to delete file:", error); } }; // Handle image upload const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const base64 = event.target?.result as string; setUploadedImageData(base64); if (!newFileName) { setNewFileName(file.name); } }; reader.readAsDataURL(file); }; // Handle drag and drop for file upload const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); const files = Array.from(e.dataTransfer.files); if (files.length === 0) return; const contextPath = getContextPath(); if (!contextPath) return; const api = getElectronAPI(); for (const file of files) { const reader = new FileReader(); reader.onload = async (event) => { const content = event.target?.result as string; const filePath = `${contextPath}/${file.name}`; await api.writeFile(filePath, content); await loadContextFiles(); }; if (isImageFile(file.name)) { reader.readAsDataURL(file); } else { reader.readAsText(file); } } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; // Handle drag and drop for .txt and .md files in the add context dialog textarea const handleTextAreaDrop = async ( e: React.DragEvent ) => { e.preventDefault(); e.stopPropagation(); setIsDropHovering(false); const files = Array.from(e.dataTransfer.files); if (files.length === 0) return; const file = files[0]; // Only handle the first file const fileName = file.name.toLowerCase(); // Only accept .txt and .md files if (!fileName.endsWith(".txt") && !fileName.endsWith(".md")) { console.warn("Only .txt and .md files are supported for drag and drop"); return; } const reader = new FileReader(); reader.onload = (event) => { const content = event.target?.result as string; setNewFileContent(content); // Auto-fill filename if empty if (!newFileName) { setNewFileName(file.name); } }; reader.readAsText(file); }; const handleTextAreaDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDropHovering(true); }; const handleTextAreaDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDropHovering(false); }; if (!currentProject) { return (

No project selected

); } if (isLoading) { return (
); } return (
{/* Header */}

Context Files

Add context files to include in AI prompts

{/* Main content area with file list and editor */}
{/* Left Panel - File List */}

Context Files ({contextFiles.length})

{contextFiles.length === 0 ? (

No context files yet.
Drop files here or click Add File.

) : (
{contextFiles.map((file) => ( ))}
)}
{/* Right Panel - Editor/Preview */}
{selectedFile ? ( <> {/* File toolbar */}
{selectedFile.type === "image" ? ( ) : ( )} {selectedFile.name}
{selectedFile.type === "text" && ( )}
{/* Content area */}
{selectedFile.type === "image" ? (
{selectedFile.name}
) : (