diff --git a/frontend/src/components/dashboard/StartSessionInput.tsx b/frontend/src/components/dashboard/StartSessionInput.tsx index 62d8e8f9..6fb0c85a 100644 --- a/frontend/src/components/dashboard/StartSessionInput.tsx +++ b/frontend/src/components/dashboard/StartSessionInput.tsx @@ -1,27 +1,50 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { Sparkles, MessageCircle } from 'lucide-react' +import { Send, Paperclip, Terminal, Loader2, X, RotateCcw, ImagePlus } from 'lucide-react' import { cn } from '@/lib/utils' +import { uploadsApi } from '@/api/uploads' +import type { FileUploadResponse, PendingUpload } from '@/types/upload' -type SessionMode = 'guided' | 'chat' +const SUGGESTIONS = [ + 'VPN not connecting', + 'Outlook not syncing', + 'User locked out', + 'Slow internet', + 'Printer issues', + 'MFA problems', +] + +const ACCEPTED_FILE_TYPES = 'image/png,image/jpeg,image/gif,image/webp,.txt,.log,.csv,.pdf,.docx' export function StartSessionInput() { - const [mode, setMode] = useState('guided') const [value, setValue] = useState('') + const [showLogs, setShowLogs] = useState(false) + const [logContent, setLogContent] = useState('') + const [pendingUploads, setPendingUploads] = useState([]) + const [isDragOver, setIsDragOver] = useState(false) const navigate = useNavigate() - const inputRef = useRef(null) + const textareaRef = useRef(null) + const fileInputRef = useRef(null) + const dragCounterRef = useRef(0) - useEffect(() => { inputRef.current?.focus() }, []) + useEffect(() => { textareaRef.current?.focus() }, []) + + // Auto-grow textarea + useEffect(() => { + const el = textareaRef.current + if (!el) return + el.style.height = 'auto' + el.style.height = `${Math.min(el.scrollHeight, 200)}px` + }, [value]) const handleSubmit = () => { const trimmed = value.trim() if (!trimmed) return - - if (mode === 'guided') { - navigate('/pilot', { state: { prefill: trimmed } }) - } else { - navigate('/assistant', { state: { prefill: trimmed } }) + const state: Record = { prefill: trimmed } + if (logContent.trim()) { + state.logs = logContent.trim() } + navigate('/pilot', { state }) } const handleKeyDown = (e: React.KeyboardEvent) => { @@ -31,58 +54,290 @@ export function StartSessionInput() { } } + const handleSuggestionClick = (suggestion: string) => { + navigate('/pilot', { state: { prefill: suggestion } }) + } + + // ── File handling ────────────────────────────── + + const processFiles = useCallback((files: File[]) => { + if (files.length === 0) return + + const newUploads: PendingUpload[] = files.map((file) => ({ + id: crypto.randomUUID(), + file, + preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : '', + status: 'uploading' as const, + })) + + setPendingUploads((prev) => [...prev, ...newUploads]) + + newUploads.forEach((upload) => { + uploadsApi + .upload(upload.file) + .then((result) => { + setPendingUploads((prev) => + prev.map((u) => u.id === upload.id ? { ...u, status: 'done' as const, result } : u) + ) + }) + .catch((err) => { + const errorMsg = err?.response?.status === 503 + ? 'File uploads not available' + : err?.message || 'Upload failed' + setPendingUploads((prev) => + prev.map((u) => u.id === upload.id ? { ...u, status: 'error' as const, error: errorMsg } : u) + ) + }) + }) + }, []) + + const handlePaste = useCallback((e: React.ClipboardEvent) => { + const items = e.clipboardData?.items + if (!items) return + const imageFiles: File[] = [] + for (let i = 0; i < items.length; i++) { + if (items[i].type.startsWith('image/')) { + const file = items[i].getAsFile() + if (file) imageFiles.push(file) + } + } + if (imageFiles.length > 0) { + e.preventDefault() + processFiles(imageFiles) + } + }, [processFiles]) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.dataTransfer.dropEffect = 'copy' + }, []) + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault() + dragCounterRef.current++ + if (dragCounterRef.current === 1) setIsDragOver(true) + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + dragCounterRef.current-- + if (dragCounterRef.current === 0) setIsDragOver(false) + }, []) + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + dragCounterRef.current = 0 + setIsDragOver(false) + processFiles(Array.from(e.dataTransfer.files)) + }, [processFiles]) + + const handleFileSelect = useCallback((e: React.ChangeEvent) => { + if (e.target.files) { + processFiles(Array.from(e.target.files)) + e.target.value = '' // reset so same file can be re-selected + } + }, [processFiles]) + + const handleRemoveUpload = useCallback((uploadId: string) => { + setPendingUploads((prev) => { + const toRemove = prev.find((u) => u.id === uploadId) + if (toRemove?.preview) URL.revokeObjectURL(toRemove.preview) + return prev.filter((u) => u.id !== uploadId) + }) + }, []) + + const retryUpload = useCallback((uploadId: string) => { + const upload = pendingUploads.find((u) => u.id === uploadId) + if (!upload) return + setPendingUploads((prev) => + prev.map((u) => u.id === uploadId ? { ...u, status: 'uploading' as const, error: undefined } : u) + ) + uploadsApi + .upload(upload.file) + .then((result) => { + setPendingUploads((prev) => + prev.map((u) => u.id === uploadId ? { ...u, status: 'done' as const, result } : u) + ) + }) + .catch((err) => { + setPendingUploads((prev) => + prev.map((u) => u.id === uploadId ? { ...u, status: 'error' as const, error: err?.message || 'Upload failed' } : u) + ) + }) + }, [pendingUploads]) + + // Cleanup blob URLs on unmount + useEffect(() => { + return () => { + pendingUploads.forEach((u) => { if (u.preview) URL.revokeObjectURL(u.preview) }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const hasContent = value.trim().length > 0 + return ( -
-
-
- - setValue(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="What are you troubleshooting?" - className="w-full rounded-xl border border-border bg-background py-3.5 pl-11 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20" - /> -
-
-
- - +
+ {/* Main input area */} +
+ {/* Drag overlay */} + {isDragOver && ( +
+
+ + Drop files to attach +
- - Press Enter to start - + )} + + {/* Textarea */} +