"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { useAppStore } from "@/store/app-store"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ImageDropZone } from "@/components/ui/image-drop-zone"; import { Bot, Send, User, Loader2, Sparkles, Wrench, Trash2, PanelLeftClose, PanelLeft, Paperclip, X, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useElectronAgent } from "@/hooks/use-electron-agent"; import { SessionManager } from "@/components/session-manager"; import type { ImageAttachment } from "@/store/app-store"; export function AgentView() { const { currentProject } = useAppStore(); const [input, setInput] = useState(""); const [selectedImages, setSelectedImages] = useState([]); const [showImageDropZone, setShowImageDropZone] = useState(false); const [currentTool, setCurrentTool] = useState(null); const [currentSessionId, setCurrentSessionId] = useState(null); const [showSessionManager, setShowSessionManager] = useState(true); const [isDragOver, setIsDragOver] = useState(false); // Scroll management for auto-scroll const messagesContainerRef = useRef(null); const [isUserAtBottom, setIsUserAtBottom] = useState(true); // Input ref for auto-focus const inputRef = useRef(null); // Use the Electron agent hook (only if we have a session) const { messages, isProcessing, isConnected, sendMessage, clearHistory, error: agentError, } = useElectronAgent({ sessionId: currentSessionId || "", workingDirectory: currentProject?.path, onToolUse: (toolName) => { setCurrentTool(toolName); setTimeout(() => setCurrentTool(null), 2000); }, }); const handleSend = useCallback(async () => { if ((!input.trim() && selectedImages.length === 0) || isProcessing) return; const messageContent = input; const messageImages = selectedImages; setInput(""); setSelectedImages([]); setShowImageDropZone(false); await sendMessage(messageContent, messageImages); }, [input, selectedImages, isProcessing, sendMessage]); const handleImagesSelected = useCallback((images: ImageAttachment[]) => { setSelectedImages(images); }, []); const toggleImageDropZone = useCallback(() => { setShowImageDropZone(!showImageDropZone); }, [showImageDropZone]); // Helper function to convert file to base64 const fileToBase64 = useCallback((file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(new Error("Failed to read file as base64")); } }; reader.onerror = () => reject(new Error("Failed to read file")); reader.readAsDataURL(file); }); }, []); // Process dropped files const processDroppedFiles = useCallback( async (files: FileList) => { if (isProcessing) return; const ACCEPTED_IMAGE_TYPES = [ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", ]; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_FILES = 5; const newImages: ImageAttachment[] = []; const errors: string[] = []; for (const file of Array.from(files)) { // Validate file type if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { errors.push( `${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.` ); continue; } // Validate file size if (file.size > MAX_FILE_SIZE) { const maxSizeMB = MAX_FILE_SIZE / (1024 * 1024); errors.push( `${file.name}: File too large. Maximum size is ${maxSizeMB}MB.` ); continue; } // Check if we've reached max files if (newImages.length + selectedImages.length >= MAX_FILES) { errors.push(`Maximum ${MAX_FILES} images allowed.`); break; } try { const base64 = await fileToBase64(file); const imageAttachment: ImageAttachment = { id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, data: base64, mimeType: file.type, filename: file.name, size: file.size, }; newImages.push(imageAttachment); } catch (error) { errors.push(`${file.name}: Failed to process image.`); } } if (errors.length > 0) { console.warn("Image upload errors:", errors); } if (newImages.length > 0) { setSelectedImages((prev) => [...prev, ...newImages]); } }, [isProcessing, selectedImages, fileToBase64] ); // Remove individual image const removeImage = useCallback((imageId: string) => { setSelectedImages((prev) => prev.filter((img) => img.id !== imageId)); }, []); // Drag and drop handlers for the input area const handleDragEnter = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (isProcessing || !isConnected) return; console.log( "[agent-view] Drag enter types:", Array.from(e.dataTransfer.types) ); // Check if dragged items contain files if (e.dataTransfer.types.includes("Files")) { setIsDragOver(true); } }, [isProcessing, isConnected] ); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // Only set dragOver to false if we're leaving the input container const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { setIsDragOver(false); } }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (isProcessing || !isConnected) return; console.log("[agent-view] Drop event:", { filesCount: e.dataTransfer.files.length, itemsCount: e.dataTransfer.items.length, types: Array.from(e.dataTransfer.types), }); // Check if we have files const files = e.dataTransfer.files; if (files && files.length > 0) { console.log("[agent-view] Processing files from dataTransfer.files"); processDroppedFiles(files); return; } // Handle file paths (from screenshots or other sources) // This is common on macOS when dragging screenshots const items = e.dataTransfer.items; if (items && items.length > 0) { console.log("[agent-view] Processing items"); for (let i = 0; i < items.length; i++) { const item = items[i]; console.log(`[agent-view] Item ${i}:`, { kind: item.kind, type: item.type, }); if (item.kind === "file") { const file = item.getAsFile(); if (file) { console.log("[agent-view] Got file from item:", { name: file.name, type: file.type, size: file.size, }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); processDroppedFiles(dataTransfer.files); } } } } }, [isProcessing, isConnected, processDroppedFiles] ); const handlePaste = useCallback( async (e: React.ClipboardEvent) => { // Check if clipboard contains files const items = e.clipboardData?.items; if (items) { const files: File[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; console.log("[agent-view] Paste item:", { kind: item.kind, type: item.type, }); if (item.kind === "file") { const file = item.getAsFile(); if (file && file.type.startsWith("image/")) { e.preventDefault(); // Prevent default paste of file path files.push(file); } } } if (files.length > 0) { console.log( "[agent-view] Processing pasted image files:", files.length ); const dataTransfer = new DataTransfer(); files.forEach((file) => dataTransfer.items.add(file)); await processDroppedFiles(dataTransfer.files); } } }, [processDroppedFiles] ); const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleClearChat = async () => { if (!confirm("Are you sure you want to clear this conversation?")) return; await clearHistory(); }; // Scroll position detection const checkIfUserIsAtBottom = useCallback(() => { const container = messagesContainerRef.current; if (!container) return; const threshold = 50; // 50px threshold for "near bottom" const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= threshold; setIsUserAtBottom(isAtBottom); }, []); // Scroll to bottom function const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { const container = messagesContainerRef.current; if (!container) return; container.scrollTo({ top: container.scrollHeight, behavior: behavior, }); }, []); // Handle scroll events const handleScroll = useCallback(() => { checkIfUserIsAtBottom(); }, [checkIfUserIsAtBottom]); // Auto-scroll effect when messages change useEffect(() => { // Only auto-scroll if user was already at bottom if (isUserAtBottom && messages.length > 0) { // Use a small delay to ensure DOM is updated setTimeout(() => { scrollToBottom("smooth"); }, 100); } }, [messages, isUserAtBottom, scrollToBottom]); // Initial scroll to bottom when session changes useEffect(() => { if (currentSessionId && messages.length > 0) { // Scroll immediately without animation when switching sessions setTimeout(() => { scrollToBottom("auto"); setIsUserAtBottom(true); }, 100); } }, [currentSessionId, scrollToBottom]); // Auto-focus input when session is selected/changed useEffect(() => { if (currentSessionId && inputRef.current) { // Small delay to ensure UI has updated setTimeout(() => { inputRef.current?.focus(); }, 200); } }, [currentSessionId]); if (!currentProject) { return (

No Project Selected

Open or create a project to start working with the AI agent.

); } // Show welcome message if no messages yet const displayMessages = messages.length === 0 ? [ { id: "welcome", role: "assistant" as const, content: "Hello! I'm the Automaker Agent. I can help you build software autonomously. I can read and modify files in this project, run commands, and execute tests. What would you like to create today?", timestamp: new Date().toISOString(), }, ] : messages; return (
{/* Session Manager Sidebar */} {showSessionManager && currentProject && (
)} {/* Chat Area */}
{/* Header */}

AI Agent

{currentProject.name} {currentSessionId && !isConnected && " ยท Connecting..."}

{/* Status indicators & actions */}
{currentTool && (
{currentTool}
)} {agentError && ( {agentError} )} {currentSessionId && messages.length > 0 && ( )}
{/* Messages */} {!currentSessionId ? (

No Session Selected

Create or select a session to start chatting

) : (
{displayMessages.map((message) => (
{message.role === "assistant" ? ( ) : ( )}

{message.content}

{new Date(message.timestamp).toLocaleTimeString()}

))} {isProcessing && (
Thinking...
)}
)} {/* Input */} {currentSessionId && (
{/* Image Drop Zone (when visible) */} {showImageDropZone && ( )} {/* Text Input and Controls - with drag and drop support */}
setInput(e.target.value)} onKeyPress={handleKeyPress} onPaste={handlePaste} disabled={isProcessing || !isConnected} data-testid="agent-input" className={cn( selectedImages.length > 0 && "border-blue-200 bg-blue-50/50 dark:bg-blue-950/20", isDragOver && "border-blue-400 bg-blue-50/50 dark:bg-blue-950/20" )} /> {selectedImages.length > 0 && !isDragOver && (
{selectedImages.length} image {selectedImages.length > 1 ? "s" : ""}
)} {isDragOver && (
Drop here
)}
{/* Image Attachment Button */} {/* Send Button */}
{/* Selected Images Preview */} {selectedImages.length > 0 && (

{selectedImages.length} image {selectedImages.length > 1 ? "s" : ""} attached

{selectedImages.map((image) => (
{/* Image thumbnail */}
{image.filename}
{/* Image info */}

{image.filename}

{formatFileSize(image.size)}

{/* Remove button */}
))}
)}
)}
); } // Helper function to format file size function formatFileSize(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; }