You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
9.1 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* 아리 — 회의 도우미 드로어 (일정 페이지)
사전 브리핑(upcoming) · 실시간 기록(live) · 회의 노트 + 액션 추출(done) · 1:1 어시스턴트
window.CalMeet = { MeetDrawer } */
(function () {
const { useState, useEffect } = React;
const MP = {
spark: "M12 3l1.9 5.2L19 10l-5.1 1.8L12 17l-1.9-5.2L5 10l5.1-1.8z|M19 4v3|M5 17v3",
x: "M18 6 6 18M6 6l12 12",
tick: "M20 6 9 17l-5-5",
check: "M9 11l3 3L22 4|M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11",
clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 7v5l3 2",
mic: "M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z|M19 10v1a7 7 0 0 1-14 0v-1|M12 18v4M8 22h8",
file: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|M14 2v6h6M9 13h6M9 17h4",
users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z|M23 21v-2a4 4 0 0 0-3-3.87|M16 3.13a4 4 0 0 1 0 7.75",
list: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01",
msg: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",
target: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z|M12 12h.01",
flag: "M4 22V4M4 4s1.5-1 5-1 5 2 8 2 4-1 4-1v10s-1.5 1-4 1-5-2-8-2-5 1-5 1",
heart: "M19 14c1.5-1.5 3-3.4 3-5.5A4.5 4.5 0 0 0 12 5.5 4.5 4.5 0 0 0 2 8.5c0 2.1 1.5 4 3 5.5l7 7z",
plus: "M12 5v14M5 12h14",
zap: "M13 2 3 14h9l-1 8 10-12h-9z",
};
function MI({ d, w }) {
return (
<svg className="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth={w || 1.9} strokeLinecap="round" strokeLinejoin="round">
{MP[d].split("|").map((s, i) => <path key={i} d={s} />)}
</svg>
);
}
function Sec({ icon, title, hint, children }) {
return (
<section className="md-sec">
<div className="md-sh"><MI d={icon} /><b>{title}</b>{hint && <span className="md-hint">{hint}</span>}</div>
{children}
</section>
);
}
/* 액션 아이템 한 줄 — '작업으로' 보내기 */
function ActionRow({ a, onAdd }) {
return (
<div className={"md-act" + (a.added ? " added" : "")}>
<span className="md-act-check"><MI d="tick" w="3" /></span>
<span className="md-act-text">{a.text}</span>
<span className="md-act-who">{a.who} · {a.when}</span>
{a.added
? <span className="md-act-done"><MI d="tick" w="2.6" />작업에 있음</span>
: <button className="md-act-add" onClick={onAdd}><MI d="plus" />작업으로</button>}
</div>
);
}
function MeetDrawer({ ev, meet, calName, tone, onClose }) {
const [actions, setActions] = useState(meet.actions || []);
useEffect(() => setActions(meet.actions || []), [ev.id]);
useEffect(() => {
const h = (e) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
}, [onClose]);
const addOne = (i) => setActions((as) => as.map((a, j) => (j === i ? { ...a, added: true } : a)));
const addAll = () => setActions((as) => as.map((a) => ({ ...a, added: true })));
const remaining = actions.filter((a) => !a.added).length;
const phaseBadge = meet.phase === "done"
? <span className="md-phase done"><MI d="tick" w="2.6" />회의 노트 정리됨</span>
: meet.phase === "live"
? <span className="md-phase live"><span className="md-pulse" />기록 </span>
: <span className="md-phase up"><MI d="clock" />{meet.startsIn}</span>;
return (
<>
<div className="dp-backdrop" onClick={onClose} />
<aside className={"mdrawer tint-" + tone} role="dialog" aria-label="회의 도우미">
<div className="md-head">
<div className="md-ic"><MI d={meet.oneOnOne ? "heart" : "spark"} /></div>
<div className="md-ht">
<span className="md-k">{meet.oneOnOne ? "1:1 어시스턴트" : meet.phase === "upcoming" ? "사전 브리핑" : "회의 도우미"}</span>
<b>{ev.title}</b>
<span className="md-sub">{ev.start}{ev.end}{ev.loc ? " · " + ev.loc : ""} · {calName}</span>
</div>
<button className="md-x" onClick={onClose} aria-label="닫기"><MI d="x" /></button>
</div>
<div className="md-bar">{phaseBadge}{meet.meta && <span className="md-meta">{meet.meta}</span>}</div>
<div className="md-body">
{/* ---- 1:1 — 지난 대화 · 약속 · 시그널 · 대화 주제 ---- */}
{meet.oneOnOne && (
<>
<Sec icon="msg" title="지난 1:1 요약" hint={meet.lastMeeting.when}>
<p className="md-note">{meet.lastMeeting.note}</p>
</Sec>
<Sec icon="flag" title="지난번 약속한 것">
{meet.promises.map((p, i) => (
<div key={i} className={"md-chk" + (p.done ? " done" : "")}>
<span className="md-cbx"><MI d="tick" w="3" /></span>{p.text}
{!p.done && <span className="md-tag warn">아직</span>}
</div>
))}
</Sec>
<Sec icon="zap" title={meet.person.name + "님 시그널"} hint="아리가 작업·메일에서 정리">
{meet.signals.map((s, i) => (
<div key={i} className="md-sig">
<span className="md-sig-dot" style={{ background: `var(--${s.tone})` }} />
<span className="md-sig-k">{s.k}</span>
<span className="md-sig-v">{s.v}</span>
</div>
))}
</Sec>
<Sec icon="spark" title="이런 이야기를 나눠보세요">
<ol className="md-points">
{meet.talkingPoints.map((t, i) => <li key={i}>{t}</li>)}
</ol>
</Sec>
</>
)}
{/* ---- 예정 회의 — 안건 · 지난 회의 · 지난 액션 · 인사이트 ---- */}
{!meet.oneOnOne && meet.phase === "upcoming" && (
<>
<Sec icon="list" title="오늘 안건">
<ol className="md-points">{meet.agenda.map((a, i) => <li key={i}>{a}</li>)}</ol>
</Sec>
<Sec icon="msg" title="지난 회의에서" hint={meet.lastMeeting.when}>
<p className="md-note">{meet.lastMeeting.note}</p>
</Sec>
<Sec icon="target" title="지난 액션 아이템"
hint={meet.lastActions.filter((a) => a.done).length + "/" + meet.lastActions.length + " 완료"}>
{meet.lastActions.map((a, i) => (
<div key={i} className={"md-chk" + (a.done ? " done" : "")}>
<span className="md-cbx"><MI d="tick" w="3" /></span>{a.text}
<span className="md-who">{a.who}</span>
{!a.done && <span className="md-tag warn">미완</span>}
</div>
))}
</Sec>
<Sec icon="spark" title="아리 인사이트">
{meet.insights.map((t, i) => (
<p key={i} className="md-insight" dangerouslySetInnerHTML={{ __html: t }} />
))}
</Sec>
{meet.docs && (
<div className="md-docs">
{meet.docs.map((d, i) => <span key={i} className="md-doc"><MI d="file" />{d}</span>)}
</div>
)}
</>
)}
{/* ---- 끝난/진행 중 회의 — 요약 · 결정 · 액션 추출 ---- */}
{!meet.oneOnOne && meet.phase !== "upcoming" && (
<>
<Sec icon={meet.phase === "live" ? "mic" : "msg"}
title={meet.phase === "live" ? "지금까지 논의" : "핵심 논의 요약"}
hint={meet.phase === "live" ? "실시간" : "음성 기록 기반"}>
<ul className="md-sum">{meet.summary.map((s, i) => <li key={i}>{s}</li>)}</ul>
</Sec>
{meet.decisions.length > 0 && (
<Sec icon="flag" title="결정 사항">
{meet.decisions.map((d, i) => (
<div key={i} className="md-decision"><MI d="tick" w="2.6" />{d}</div>
))}
</Sec>
)}
<Sec icon="target" title="액션 아이템" hint="아리가 대화에서 추출">
{actions.map((a, i) => <ActionRow key={i} a={a} onAdd={() => addOne(i)} />)}
{remaining > 0 ? (
<button className="md-addall" onClick={addAll}>
<MI d="check" />남은 {remaining} 모두 작업으로 보내기
</button>
) : (
<div className="md-allok"><MI d="tick" w="2.6" />모든 액션이 담당자 작업으로 등록됐어요</div>
)}
</Sec>
</>
)}
</div>
</aside>
</>
);
}
window.CalMeet = { MeetDrawer };
})();