File size: 4,738 Bytes
05b4ab1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | // src/components/InputPanel.jsx
import React, { useRef, useState } from 'react'
export default function InputPanel({ text, onTextChange, onImageChange, onAnalyze, loading, imagePreview }) {
const fileRef = useRef(null)
const [dragOver, setDragOver] = useState(false)
function handleDrop(e) {
e.preventDefault()
setDragOver(false)
const file = e.dataTransfer.files[0]
if (file && file.type.startsWith('image/')) onImageChange(file)
}
function handleFile(e) {
const file = e.target.files[0]
if (file) onImageChange(file)
}
return (
<div className="card overflow-hidden">
<div className="panel-header">
<span className="w-[7px] h-[7px] rounded-full bg-[#cfe2f7] flex-shrink-0" />
<span>Input Stream</span>
<span className="ml-auto font-mono text-[9px] text-muted">Vision + NLP</span>
</div>
<div className="p-4 flex flex-col gap-4">
{/* Image drop zone */}
<div
className={`relative border rounded-lg transition-all duration-200 cursor-pointer overflow-hidden
${dragOver
? 'border-teal bg-[#0ec9a815]'
: 'border-[#1a3a6e50] hover:border-[#1a3a6e] bg-[#081527]'}`}
style={{ minHeight: '120px' }}
onClick={() => fileRef.current?.click()}
onDragOver={e => { e.preventDefault(); setDragOver(true) }}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}
>
<input
ref={fileRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleFile}
/>
{imagePreview ? (
<img
src={imagePreview}
alt="Face preview"
className="w-full h-full object-cover"
style={{ maxHeight: '200px' }}
/>
) : (
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
<div className="font-mono text-3xl opacity-30">⬡</div>
<p className="font-mono text-[11px] text-muted">Drop face image or click to upload</p>
<p className="font-mono text-[9px] text-[#3a5a7a]">JPEG · PNG · WEBP</p>
</div>
)}
{/* scanline overlay while no image */}
{!imagePreview && (
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="scanline-animate absolute left-0 right-0 h-px bg-teal opacity-20" />
</div>
)}
</div>
{/* Text area */}
<div className="flex flex-col gap-1">
<label className="font-mono text-[10px] text-muted tracking-[0.1em] uppercase">
Transcript / Message
</label>
<textarea
value={text}
onChange={e => onTextChange(e.target.value)}
placeholder="Enter customer speech or chat message…"
rows={4}
className="
w-full bg-[#081527] border border-[#1a3a6e40] rounded-lg p-3
font-mono text-[12px] text-[#cfe2f7] leading-[1.8] resize-none
placeholder:text-[#2a4a6a] outline-none
focus:border-teal focus:bg-[#08182e]
transition-all duration-200
"
/>
</div>
{/* Analyze button */}
<button
onClick={onAnalyze}
disabled={loading || (!text.trim())}
className="
relative overflow-hidden w-full py-3 rounded-md
font-display font-semibold text-sm
transition-all duration-200
disabled:opacity-40 disabled:cursor-not-allowed
enabled:cursor-pointer
"
style={{
background: loading
? 'linear-gradient(135deg, #0a6b5b, #0a4b7a)'
: 'linear-gradient(135deg, #0ec9a8, #0a8aff)',
color: '#050d1b',
}}
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="inline-block w-3 h-3 border-2 border-[#050d1b] border-t-transparent rounded-full animate-spin" />
Analyzing…
</span>
) : (
'▶ Analyze Emotion'
)}
{/* shimmer on idle */}
{!loading && (
<div className="absolute inset-0 bg-white opacity-0 hover:opacity-10 transition-opacity duration-200" />
)}
</button>
{/* API status hint */}
<p className="font-mono text-[9px] text-[#2a4a6a] text-center">
Demo mode active — connects to FastAPI on :8000 when available
</p>
</div>
</div>
)
}
|