import React, { useEffect, useRef, useState } from 'react'; import { Mic, MicOff, ArrowUp, Sparkles, Calendar, Check, CheckSquare, ShoppingCart, FileText, Layers, ChevronDown, AlertTriangle, Flame, } from 'lucide-react'; import { PriorityLevel, SharedList, Thought } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; 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, }) => { 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 [isListening, setIsListening] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [justSavedLabel, setJustSavedLabel] = useState(null); const [isMultiline, setIsMultiline] = useState(false); const inputRef = useRef(null); const containerRef = useRef(null); const recognitionRef = useRef(null); const baseVoiceTextRef = useRef(''); const hasVoiceInputRef = useRef(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); } else { setIsMultiline(false); } }; useEffect(() => { adjustHeight(); }, [content]); useEffect(() => { inputRef.current?.focus(); }, [autoFocusTrigger]); useEffect(() => { if (!targetListId && lists.length > 0) { setTargetListId(lists[0].id); } }, [lists, 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.key === '/')) { 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 = () => { const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (!SpeechRecognition) { alert('Spracheingabe wird von diesem Browser nicht direkt unterstützt (bitte Chrome, Edge oder Safari nutzen).'); return; } if (isListening && recognitionRef.current) { recognitionRef.current.stop(); setIsListening(false); return; } try { baseVoiceTextRef.current = content.replace(/\s*\[…\]$/, '').trim(); hasVoiceInputRef.current = true; const recognition = new SpeechRecognition(); recognition.lang = 'de-DE'; recognition.continuous = false; recognition.interimResults = true; recognition.onstart = () => { setIsListening(true); triggerHaptic(20); }; recognition.onresult = (event: any) => { let transcript = ''; for (let i = 0; i < event.results.length; i++) { transcript += event.results[i][0].transcript; } const base = baseVoiceTextRef.current; setContent(base ? `${base} ${transcript}` : transcript); }; recognition.onerror = () => { setIsListening(false); }; recognition.onend = () => { setIsListening(false); inputRef.current?.focus(); }; recognitionRef.current = recognition; recognition.start(); } catch { setIsListening(false); } }; const executeCapture = async ( overrideTarget?: CaptureTarget, overrideListId?: string ) => { const trimmed = content.trim(); if (!trimmed || isSubmitting) return; const effectiveTarget = fixedTarget || overrideTarget || target; const effectiveListId = overrideListId || targetListId || (lists.length > 0 ? lists[0].id : ''); const capturedPriority = priority; const capturedDueDate = dueDate; const capturedSource = hasVoiceInputRef.current || isListening ? 'voice' : 'manual'; hasVoiceInputRef.current = false; // 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 = lists.find((l) => l.id === targetListId) || lists[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) */}