/** * JARVIS — Image Panel * Drag-and-drop image upload, visual analysis with BLIP + CLIP, then chat. */ import React, { useState, useCallback } from 'react' import { useDropzone } from 'react-dropzone' import { motion, AnimatePresence } from 'framer-motion' import { Upload, Image, X, Loader2, Eye, Tag, ChevronDown, ChevronUp, Sparkles, } from 'lucide-react' import clsx from 'clsx' import toast from 'react-hot-toast' import useAppStore from '../../stores/appStore' import { imageAPI } from '../../services/api' export default function ImagePanel() { const { activeSessionId, addImageResult, setPendingImage, setInputMode } = useAppStore() const [file, setFile] = useState(null) const [preview, setPreview] = useState(null) const [question, setQuestion] = useState('') const [result, setResult] = useState(null) const [loading, setLoading] = useState(false) const [expanded, setExpanded] = useState(true) // ── Dropzone ─────────────────────────────────── const onDrop = useCallback((acceptedFiles) => { const f = acceptedFiles[0] if (!f) return setFile(f) setResult(null) const url = URL.createObjectURL(f) setPreview(url) // Auto-set pending image for chat context setPendingImage({ file: f, preview: url, caption: null }) }, [setPendingImage]) const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'] }, maxFiles: 1, maxSize: 50 * 1024 * 1024, }) const clearImage = () => { setFile(null) setPreview(null) setResult(null) setQuestion('') setPendingImage(null) } // ── Analyze ──────────────────────────────────── const analyze = async () => { if (!file) return setLoading(true) try { const data = await imageAPI.analyzeImage(file, question || null, activeSessionId) setResult(data) // Update pending image caption for chat context setPendingImage({ file, preview, caption: data.caption }) // Add to message feed addImageResult( data.session_id, question || 'Analyze this image', data, ) toast.success('Image analyzed!') } catch (err) { toast.error(err.message || 'Analysis failed') } finally { setLoading(false) } } // ── Use in chat ──────────────────────────────── const useInChat = () => { setInputMode('text') toast('Image context attached to chat', { icon: '📎' }) } return (
{/* Header */}
setExpanded((v) => !v)} >
Vision Analysis BLIP + CLIP
{expanded ? : }
{expanded && (
{!file ? ( // ── Drop zone ────────────────────────

{isDragActive ? 'Drop image here…' : 'Drag & drop or click to upload'}

PNG, JPG, WEBP · max 50MB

) : ( // ── Image preview + controls ─────────
{/* Preview */}
Preview
{/* Question input */} setQuestion(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && analyze()} placeholder="Ask about this image… (optional)" className="w-full input-field text-xs py-2" />
{result && ( )}
)} {/* Results */} {result && ( {/* Caption */}

Caption

{result.caption}

{/* Categories */} {result.top_categories?.length > 0 && (

CLIP Categories

{result.top_categories.map((cat) => (
{(cat.score * 100).toFixed(0)}% {cat.label}
))}
)} )}
)}
) }