import React, { useEffect, useRef, useState, useMemo } from 'react'; import { Mic, MicOff, ArrowDown, Sparkles, Calendar, Check, CheckSquare, ShoppingCart, FileText, Layers, ChevronDown, AlertTriangle, Flame, Loader2, } from 'lucide-react'; import { PriorityLevel, SharedList, Thought } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; import { useVoiceInput } from '../../lib/useVoiceInput'; import { VoiceWave } from '../../components/VoiceWave'; export type CaptureTarget = 'inbox' | 'task' | 'list_item' | 'document'; interface InstantCaptureBarProps { onCaptured: (thought: Thought, target?: CaptureTarget) => void; lists?: SharedList[]; autoFocusTrigger?: number; inlineOptions?: boolean; defaultShowOptions?: boolean; hideTargetPicker?: boolean; fixedTarget?: CaptureTarget; } export const InstantCaptureBar: React.FC = ({ onCaptured, lists = [], autoFocusTrigger = 0, inlineOptions = false, defaultShowOptions = false, hideTargetPicker = false, fixedTarget, }) => { // Read-only shared lists cannot take new items; stable reference for the effect below const writableLists = useMemo(() => lists.filter((l) => l.isOwner || l.role !== 'viewer'), [lists]); const [content, setContent] = useState(''); const [priority, setPriority] = useState('normal'); const [dueDate, setDueDate] = useState(''); const [target, setTarget] = useState(fixedTarget || 'inbox'); const [targetListId, setTargetListId] = useState(''); const [showOptions, setShowOptions] = useState(defaultShowOptions || hideTargetPicker); const [voiceError, setVoiceError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [justSavedLabel, setJustSavedLabel] = useState(null); const [isMultiline, setIsMultiline] = useState(false); const inputRef = useRef(null); const containerRef = useRef(null); const baseVoiceTextRef = useRef(''); const hasVoiceInputRef = useRef(false); // Voice input: own Whisper server or the browser's recognition (lib/voice.ts) const voice = useVoiceInput({ onText: (text, final) => { const base = baseVoiceTextRef.current; if (text) setContent(base ? `${base} ${text}` : text); if (final) inputRef.current?.focus(); }, onError: (message) => { setVoiceError(message); window.setTimeout(() => setVoiceError(null), 4000); }, }); const isListening = voice.state === 'listening'; const isTranscribing = voice.state === 'transcribing'; const [isOverMaxHeight, setIsOverMaxHeight] = useState(false); const adjustHeight = (el?: HTMLTextAreaElement | null) => { const targetEl = el || inputRef.current; if (!targetEl) return; targetEl.style.height = 'auto'; if (targetEl.value) { const scrollH = targetEl.scrollHeight; targetEl.style.height = `${Math.min(scrollH, 140)}px`; setIsMultiline(targetEl.value.includes('\n') || scrollH > 28); setIsOverMaxHeight(scrollH > 140); } else { setIsMultiline(false); setIsOverMaxHeight(false); } }; useEffect(() => { adjustHeight(); }, [content]); useEffect(() => { inputRef.current?.focus(); }, [autoFocusTrigger]); useEffect(() => { if (!targetListId && writableLists.length > 0) { setTargetListId(writableLists[0].id); } }, [writableLists, targetListId]); // Close dropdown on outside click useEffect(() => { if (inlineOptions) return; const handleClickOutside = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setShowOptions(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [inlineOptions]); // Global keyboard shortcut: 'c' or '/' focuses the Instant Capture input immediately useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const el = e.target as HTMLElement; const isEditing = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable; if (!isEditing && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); inputRef.current?.focus(); } else if (e.key === 'Escape' && document.activeElement === inputRef.current) { setContent(''); setIsMultiline(false); setShowOptions(false); inputRef.current?.blur(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); const toggleVoiceDictation = () => { if (voice.state === 'idle') { baseVoiceTextRef.current = content.replace(/\s*\[…\]$/, '').trim(); hasVoiceInputRef.current = true; setVoiceError(null); triggerHaptic(20); } voice.toggle(); }; const executeCapture = async ( overrideTarget?: CaptureTarget, overrideListId?: string ) => { const trimmed = content.trim(); if (!trimmed || isSubmitting) return; const effectiveTarget = fixedTarget || overrideTarget || target; const effectiveListId = overrideListId || targetListId || (writableLists.length > 0 ? writableLists[0].id : ''); const capturedPriority = priority; const capturedDueDate = dueDate; const capturedSource = hasVoiceInputRef.current || isListening ? 'voice' : 'manual'; hasVoiceInputRef.current = false; // A recognition still running would refill the field after it was saved if (voice.state !== 'idle') voice.cancel(); // Optimistically clear input immediately so the UI feels instant (<10ms) in all browsers setContent(''); setIsMultiline(false); setPriority('normal'); setDueDate(''); setTarget(fixedTarget || 'inbox'); setShowOptions(defaultShowOptions || hideTargetPicker); setIsSubmitting(true); triggerHaptic(15); if (inputRef.current) { inputRef.current.style.height = 'auto'; } const savedMsg = effectiveTarget === 'task' ? 'Als Aufgabe!' : effectiveTarget === 'list_item' ? 'Auf Liste!' : effectiveTarget === 'document' ? 'Als Notiz!' : 'In Triage!'; setJustSavedLabel(savedMsg); setTimeout(() => setJustSavedLabel(null), 1600); inputRef.current?.focus(); try { const created = await api.captureThought( trimmed, capturedPriority, capturedSource, capturedDueDate, { target: effectiveTarget, targetListId: effectiveTarget === 'list_item' ? effectiveListId : '', } ); onCaptured(created, effectiveTarget); } catch { // Restore text if capture unexpectedly failed completely setContent(trimmed); } finally { setIsSubmitting(false); } }; const handleSubmit = async (e?: React.FormEvent) => { if (e) e.preventDefault(); await executeCapture(); }; const selectedList = writableLists.find((l) => l.id === targetListId) || writableLists[0]; const getTargetLabel = () => { if (target === 'task') return 'Direkt-Aufgabe'; if (target === 'list_item') return selectedList ? `${selectedList.icon} ${selectedList.title}` : 'Auf Liste'; if (target === 'document') return 'Direkt-Notiz'; return 'Quick Dump'; }; return (
{/* Quick-Capture Input Bar (Radius controlled by --radius-pill / multiline) */}