| 'use client' |
|
|
| import { useState, useEffect, useRef } from 'react' |
| import type { Encoder, Decoder } from '@/lib/api' |
|
|
| const ENCODERS: { value: Encoder; label: string }[] = [ |
| { value: 'xlm-roberta-large', label: 'xlm-roberta-large' }, |
| ] |
|
|
| const DECODERS: { value: Decoder; label: string }[] = [ |
| { value: 'gemma3', label: 'gemma-3-1b-it' }, |
| { value: 'qwen3', label: 'Qwen3-4B-Instruct-2507' }, |
| ] |
|
|
| const MAX_CHARS = 10000 |
|
|
| const LOADING_MESSAGES = [ |
| '🏷️✍️ Annotating...', |
| '⏳⚙️ Processing...', |
| '🧠🤔💭 Thinking...', |
| '🔍📊 Analyzing...', |
| '🔤➡️😄 Emojinizing...', |
| ] |
|
|
| interface Props { |
| onSubmit: (encoder: Encoder, decoder: Decoder, text: string) => void |
| onStop: () => void |
| loading: boolean |
| } |
|
|
| export default function AnnotationForm({ onSubmit, onStop, loading }: Props) { |
| const [encoder, setEncoder] = useState<Encoder>('xlm-roberta-large') |
| const [decoder, setDecoder] = useState<Decoder>('gemma3') |
| const [text, setText] = useState('') |
| const [msgIdx, setMsgIdx] = useState(0) |
|
|
| useEffect(() => { |
| if (!loading) return |
| setMsgIdx(0) |
| const id = setInterval( |
| () => setMsgIdx((i) => (i + 1) % LOADING_MESSAGES.length), |
| 3000 |
| ) |
| return () => clearInterval(id) |
| }, [loading]) |
|
|
| function handleSubmit(e: React.FormEvent) { |
| e.preventDefault() |
| if (!text.trim()) return |
| onSubmit(encoder, decoder, text) |
| } |
|
|
| return ( |
| <form onSubmit={handleSubmit} className="bg-white/80 dark:bg-black/40 backdrop-blur-md dark:backdrop-blur-xl border border-gray-300 dark:border-white/10 rounded-2xl shadow-md p-6 space-y-5"> |
| <div className="flex gap-4"> |
| <div className="flex-1"> |
| <label className="block text-sm font-bold text-gray-600 dark:text-white uppercase tracking-widest mb-2"> |
| Encoder |
| </label> |
| <Dropdown |
| options={ENCODERS} |
| value={encoder} |
| onChange={(v) => setEncoder(v as Encoder)} |
| /> |
| </div> |
| <div className="flex-1"> |
| <label className="block text-sm font-bold text-gray-600 dark:text-white uppercase tracking-widest mb-2"> |
| Decoder |
| </label> |
| <Dropdown |
| options={DECODERS} |
| value={decoder} |
| onChange={(v) => setDecoder(v as Decoder)} |
| /> |
| </div> |
| </div> |
| |
| <div> |
| <label className="block text-sm font-bold text-gray-600 dark:text-white uppercase tracking-widest mb-2"> |
| Text |
| </label> |
| <textarea |
| value={text} |
| onChange={(e) => setText(e.target.value.slice(0, MAX_CHARS))} |
| rows={5} |
| placeholder="Enter text to annotate…" |
| className="w-full bg-gray-50 dark:bg-gray-700/60 border border-gray-300 dark:border-gray-600 rounded-xl px-4 py-3 text-[15px] text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 resize-y min-h-[7rem] focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 transition" |
| /> |
| <p className="text-[13px] text-gray-400 text-right mt-1"> |
| {text.length} / {MAX_CHARS} |
| </p> |
| </div> |
| |
| <div className="flex gap-3"> |
| <button |
| type="submit" |
| disabled={loading || !text.trim()} |
| className={`flex-1 text-base text-white rounded-xl py-3 font-semibold transition-all shadow-sm ${ |
| loading |
| ? 'bg-indigo-900 cursor-not-allowed' |
| : !text.trim() |
| ? 'bg-indigo-600 dark:bg-indigo-800 opacity-50 cursor-not-allowed' |
| : 'bg-indigo-600 dark:bg-indigo-800 hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:shadow-indigo-200 hover:shadow-md active:bg-indigo-700' |
| }`} |
| > |
| {loading ? LOADING_MESSAGES[msgIdx] : '🚀 Annotate'} |
| </button> |
| |
| {loading && ( |
| <button |
| type="button" |
| onClick={onStop} |
| className="px-7 text-base font-semibold text-red-600 dark:text-white border border-red-200 dark:border-red-700 rounded-xl bg-red-50 dark:bg-red-900 hover:bg-red-100 dark:hover:bg-red-800 active:bg-red-200 dark:active:bg-red-700 transition-colors" |
| > |
| ✋ Stop |
| </button> |
| )} |
| </div> |
| </form> |
| ) |
| } |
|
|
| interface DropdownProps { |
| options: { value: string; label: string }[] |
| value: string |
| onChange: (value: string) => void |
| } |
|
|
| function Dropdown({ options, value, onChange }: DropdownProps) { |
| const [open, setOpen] = useState(false) |
| const ref = useRef<HTMLDivElement>(null) |
|
|
| useEffect(() => { |
| if (!open) return |
| function handleClick(e: MouseEvent) { |
| if (ref.current && !ref.current.contains(e.target as Node)) { |
| setOpen(false) |
| } |
| } |
| document.addEventListener('mousedown', handleClick) |
| return () => document.removeEventListener('mousedown', handleClick) |
| }, [open]) |
|
|
| const selected = options.find((o) => o.value === value) |
|
|
| return ( |
| <div ref={ref} className="relative"> |
| <button |
| type="button" |
| onClick={() => setOpen((o) => !o)} |
| className="w-full flex items-center justify-between border border-gray-300 dark:border-gray-600 rounded-xl px-4 py-2.5 text-[15px] bg-gray-50 dark:bg-gray-700/60 text-gray-800 dark:text-gray-100 hover:border-gray-400 dark:hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 transition cursor-pointer" |
| > |
| <span>{selected?.label}</span> |
| <svg |
| className={`ml-2 shrink-0 text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`} |
| xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" |
| fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" |
| > |
| <polyline points="6 9 12 15 18 9" /> |
| </svg> |
| </button> |
| |
| {open && ( |
| <ul className="absolute z-20 mt-1 w-full bg-white/80 dark:bg-gray-800/90 backdrop-blur-md border border-gray-300 dark:border-gray-600 rounded-xl shadow-xl py-1 text-[15px]"> |
| {options.map((o) => ( |
| <li key={o.value}> |
| <button |
| type="button" |
| onClick={() => { onChange(o.value); setOpen(false) }} |
| className="w-full flex items-center justify-between px-3 py-2 text-left text-gray-800 dark:text-gray-100 hover:bg-gray-200/50 dark:hover:bg-gray-700/60 transition-colors" |
| > |
| <span>{o.label}</span> |
| {o.value === value && <span className="text-base">✅</span>} |
| </button> |
| </li> |
| ))} |
| </ul> |
| )} |
| </div> |
| ) |
| } |
|
|