import { useState, useRef, useEffect, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { Send, Paperclip, Terminal, Loader2, X, RotateCcw, ImagePlus, Globe, Mail, Lock, Printer, Shield } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import { cn } from '@/lib/utils' import { uploadsApi } from '@/api/uploads' import { toast } from '@/lib/toast' import type { PendingUpload } from '@/types/upload' const SUGGESTIONS: { icon: LucideIcon; label: string }[] = [ { icon: Globe, label: 'VPN not connecting' }, { icon: Mail, label: 'Outlook not syncing' }, { icon: Lock, label: 'User locked out' }, { icon: Globe, label: 'Slow internet' }, { icon: Printer, label: 'Printer issues' }, { icon: Shield, label: 'MFA problems' }, ] const ACCEPTED_FILE_TYPES = 'image/png,image/jpeg,image/gif,image/webp,.txt,.log,.csv,.pdf,.docx' export function StartSessionInput() { 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 textareaRef = useRef(null) const fileInputRef = useRef(null) const dragCounterRef = useRef(0) 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 const state: Record = { prefill: trimmed } if (logContent.trim()) { state.logs = logContent.trim() } const completedUploadIds = pendingUploads .filter((u) => u.status === 'done' && u.result?.id) .map((u) => u.result!.id) if (completedUploadIds.length > 0) { state.uploadIds = completedUploadIds } navigate('/assistant', { state }) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSubmit() } } const handleSuggestionClick = (suggestion: string) => { navigate('/assistant', { 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 is503 = err?.response?.status === 503 const errorMsg = is503 ? 'File uploads not available' : err?.message || 'Upload failed' if (is503) { toast.warning('Image attachments are not available yet — describe the issue in text instead') } else { toast.error(`Upload failed: ${errorMsg}`) } setPendingUploads((prev) => prev.filter((u) => u.id !== upload.id)) }) }) }, []) 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 (
{/* Main input area */}
{/* Drag overlay */} {isDragOver && (
Drop files to attach
)} {/* Textarea */}