/* 회의록 브레인 — 화면 ① 회의록 입력 */ function ScreenInput({ onSave, recent, onOpenMeeting }) { const DS = window.CalComDesignSystem_436c9d; const { Button, Input, Avatar } = DS; const D = window.MB_DATA; const [title, setTitle] = React.useState(""); const [date, setDate] = React.useState(""); const [attendees, setAttendees] = React.useState(""); const [note, setNote] = React.useState(""); const [phase, setPhase] = React.useState("idle"); // idle | saving | done | error const [result, setResult] = React.useState(null); const [aiSource, setAiSource] = React.useState(null); const [errorCode, setErrorCode] = React.useState(null); const [sttPhase, setSttPhase] = React.useState("idle"); // idle | recording | transcribing const [sttError, setSttError] = React.useState(false); const recorderRef = React.useRef(null); const chunksRef = React.useRef([]); const fillSample = () => { setTitle("신제품 출시 점검 회의"); setDate("2026-02-19"); setAttendees("박서준, 최민서, 김하늘, 한지우"); setNote(D.SAMPLE_NOTE); }; const save = async () => { if (!note.trim() || phase === "saving") return; setPhase("saving"); setResult(null); setErrorCode(null); try { const out = await window.MB_AI.extractMeeting(title, date, attendees, note); // 백엔드 응답(summary: str, actions: [{text,start_date,due_date}])을 화면 형태로 정규화 const summary = Array.isArray(out.summary) ? out.summary : out.summary ? [out.summary] : []; const actions = (out.actions || []).map((a) => ({ title: a.title || a.text, owner: a.owner, start: a.start || a.start_date, end: a.end || a.due_date, })); const ex = { summary, decisions: out.decisions || [], actions, _source: out._source }; setResult(ex); setAiSource(ex._source); setPhase("done"); const meeting = { id: out.id != null ? "srv-" + out.id : "new-" + Date.now(), title: title.trim() || "제목 없는 회의", date: date || new Date().toISOString().slice(0, 10), attendees: attendees.split(",").map((s) => s.trim()).filter(Boolean), summary: ex.summary, decisions: ex.decisions, tags: ["신규"], }; onSave && onSave({ meeting, actions: ex.actions }); } catch (err) { setErrorCode(err.message || "network"); setPhase("error"); } }; const toggleMic = async () => { if (sttPhase === "recording") { recorderRef.current?.stop(); return; } if (!navigator.mediaDevices) return; setSttError(false); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg"; const rec = new MediaRecorder(stream, { mimeType }); chunksRef.current = []; rec.ondataavailable = (e) => { if (e.data.size) chunksRef.current.push(e.data); }; rec.onstop = async () => { stream.getTracks().forEach((t) => t.stop()); setSttPhase("transcribing"); setSttError(false); const blob = new Blob(chunksRef.current, { type: mimeType }); try { const res = await fetch("/stt", { method: "POST", headers: { "Content-Type": mimeType }, body: blob, }); if (!res.ok) throw new Error("stt-error"); const data = await res.json(); if (data.text) { setNote((prev) => prev ? prev + "\n" + data.text : data.text); } else { setSttError(true); } } catch (_) { setSttError(true); } setSttPhase("idle"); }; rec.start(); recorderRef.current = rec; setSttPhase("recording"); } catch (_) { setSttPhase("idle"); } }; const canSave = note.trim().length > 0 && phase !== "saving"; return (

회의록 입력

회의록을 붙여넣으면 AI가 정리합니다

요약 · 결정사항 · 담당자별 액션을 자동으로 추출해 팀의 기억으로 쌓습니다.

{/* 입력 폼 */}
setTitle(e.target.value)} /> setDate(e.target.value)} />
setAttendees(e.target.value)} />