Download frontend/src/components/image/ImagePanel.jsx from Rixhot/ZARAI: direct link, hf CLI and curl.
- Browser
- Download file 9.58 kB
-
https://huggingface.co/spaces/Rixhot/ZARAI/resolve/main/frontend/src/components/image/ImagePanel.jsx
- Command line
-
hf download hf://spaces/Rixhot/ZARAI/frontend/src/components/image/ImagePanel.jsx
-
curl -L -o ImagePanel.jsx https://huggingface.co/spaces/Rixhot/ZARAI/resolve/main/frontend/src/components/image/ImagePanel.jsx
9.58 kB
| /** | |
| * 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 ( | |
| <div className="bg-jarvis-surface border-t border-jarvis-border"> | |
| {/* Header */} | |
| <div | |
| className="flex items-center justify-between px-4 py-2.5 cursor-pointer | |
| hover:bg-jarvis-panel/30 transition-colors" | |
| onClick={() => setExpanded((v) => !v)} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Eye size={13} className="text-jarvis-accent" /> | |
| <span className="text-xs font-medium text-jarvis-text">Vision Analysis</span> | |
| <span className="text-[10px] font-mono text-jarvis-dim">BLIP + CLIP</span> | |
| </div> | |
| {expanded ? <ChevronDown size={13} className="text-jarvis-dim" /> : | |
| <ChevronUp size={13} className="text-jarvis-dim" />} | |
| </div> | |
| <AnimatePresence> | |
| {expanded && ( | |
| <motion.div | |
| initial={{ height: 0 }} | |
| animate={{ height: 'auto' }} | |
| exit={{ height: 0 }} | |
| className="overflow-hidden" | |
| > | |
| <div className="px-4 pb-4 space-y-3"> | |
| {!file ? ( | |
| // ββ Drop zone ββββββββββββββββββββββββ | |
| <div | |
| {...getRootProps()} | |
| className={clsx( | |
| 'border-2 border-dashed rounded-xl p-6 text-center cursor-pointer', | |
| 'transition-all duration-200', | |
| isDragActive | |
| ? 'border-jarvis-accent bg-jarvis-accent/10' | |
| : 'border-jarvis-border hover:border-jarvis-accent/50 hover:bg-jarvis-panel/30', | |
| )} | |
| > | |
| <input {...getInputProps()} /> | |
| <Upload size={24} className={clsx('mx-auto mb-2', isDragActive ? 'text-jarvis-accent' : 'text-jarvis-dim')} /> | |
| <p className="text-xs text-jarvis-muted"> | |
| {isDragActive ? 'Drop image hereβ¦' : 'Drag & drop or click to upload'} | |
| </p> | |
| <p className="text-[10px] text-jarvis-dim mt-1">PNG, JPG, WEBP Β· max 50MB</p> | |
| </div> | |
| ) : ( | |
| // ββ Image preview + controls βββββββββ | |
| <div className="flex gap-3"> | |
| {/* Preview */} | |
| <div className="relative flex-shrink-0"> | |
| <img | |
| src={preview} | |
| alt="Preview" | |
| className="w-28 h-28 object-cover rounded-lg border border-jarvis-border" | |
| /> | |
| <button | |
| onClick={clearImage} | |
| className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full | |
| bg-jarvis-error text-white flex items-center justify-center | |
| hover:brightness-110 transition-all" | |
| > | |
| <X size={10} /> | |
| </button> | |
| </div> | |
| <div className="flex-1 space-y-2"> | |
| {/* Question input */} | |
| <input | |
| type="text" | |
| value={question} | |
| onChange={(e) => 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" | |
| /> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={analyze} | |
| disabled={loading} | |
| className="flex items-center gap-1.5 btn-accent text-xs py-1.5 flex-1" | |
| > | |
| {loading | |
| ? <Loader2 size={12} className="animate-spin" /> | |
| : <Sparkles size={12} />} | |
| {loading ? 'Analyzingβ¦' : 'Analyze'} | |
| </button> | |
| {result && ( | |
| <button | |
| onClick={useInChat} | |
| className="btn-ghost text-xs py-1.5 flex items-center gap-1" | |
| > | |
| Use in Chat | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Results */} | |
| <AnimatePresence> | |
| {result && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 4 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="space-y-2" | |
| > | |
| {/* Caption */} | |
| <div className="panel p-3"> | |
| <p className="text-[10px] font-mono text-jarvis-dim uppercase tracking-widest mb-1">Caption</p> | |
| <p className="text-xs text-jarvis-text">{result.caption}</p> | |
| </div> | |
| {/* Categories */} | |
| {result.top_categories?.length > 0 && ( | |
| <div className="panel p-3"> | |
| <p className="text-[10px] font-mono text-jarvis-dim uppercase tracking-widest mb-2"> | |
| CLIP Categories | |
| </p> | |
| <div className="space-y-1.5"> | |
| {result.top_categories.map((cat) => ( | |
| <div key={cat.label} className="flex items-center gap-2"> | |
| <div | |
| className="h-1 rounded-full bg-jarvis-accent/30" | |
| style={{ width: '100%' }} | |
| > | |
| <div | |
| className="h-1 rounded-full bg-jarvis-accent transition-all" | |
| style={{ width: `${(cat.score * 100).toFixed(0)}%` }} | |
| /> | |
| </div> | |
| <span className="text-[10px] text-jarvis-muted whitespace-nowrap font-mono"> | |
| {(cat.score * 100).toFixed(0)}% | |
| </span> | |
| <span className="text-[10px] text-jarvis-dim whitespace-nowrap truncate"> | |
| {cat.label} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ) | |
| } | |