// frontend/components/inbox/CaptureComposer.tsx "use client"; import { useRef, useState } from "react"; import { Icon } from "@/components/Icon"; import { transcribeAudio, captionImage } from "@/lib/inbox/multimodal"; // 멀티모달 미가용/거부 시 phase-4 톤 폴백 (graceful degrade) const VOICE_FALLBACK = "음성 메모 — (텍스트로 적어주세요)"; const IMAGE_FALLBACK = "이미지 캡처 — (자동 인식 결과 없음)"; export default function CaptureComposer({ onSubmit, }: { onSubmit: (raw: string, kind: "text" | "voice" | "image") => void; }) { const [input, setInput] = useState(""); const [recording, setRecording] = useState(false); const recRef = useRef(null); const fileRef = useRef(null); const submitText = () => { const t = input.trim(); if (!t) return; onSubmit(t, "text"); setInput(""); }; /* 음성: 실제 녹음 → /inbox/transcribe → raw 텍스트로 capture */ const toggleMic = async () => { if (recording) { recRef.current?.stop(); return; } if (!navigator.mediaDevices?.getUserMedia) { onSubmit(VOICE_FALLBACK, "voice"); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const rec = new MediaRecorder(stream); const chunks: Blob[] = []; rec.ondataavailable = (e) => chunks.push(e.data); rec.onstop = async () => { stream.getTracks().forEach((t) => t.stop()); setRecording(false); const blob = new Blob(chunks, { type: "audio/webm" }); try { const { text } = await transcribeAudio(blob); onSubmit(text || VOICE_FALLBACK, "voice"); } catch { onSubmit(VOICE_FALLBACK, "voice"); } }; recRef.current = rec; rec.start(); setRecording(true); } catch { onSubmit(VOICE_FALLBACK, "voice"); // 권한 거부 → 폴백 } }; /* 이미지: 파일 선택 → /inbox/caption → raw 캡션으로 capture */ const onImage = async (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (!f) return; try { const { text } = await captionImage(f); onSubmit(text || IMAGE_FALLBACK, "image"); } catch { onSubmit(IMAGE_FALLBACK, "image"); } finally { e.target.value = ""; } }; return (
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitText()} placeholder="갑자기 생각난 것 아무거나… 예) 다음 주 한국 가는 비행기 티켓 사기" aria-label="인박스에 빠르게 캡처" />
); }