|
|
/* 아리 — 스마트 인박스 위젯
|
|
|
입력하면 아리가 실시간으로 분류: 작업/일정/아이디어 × 업무/개인 컨텍스트 + 배치 + 자동화.
|
|
|
dash-v2.jsx에서 <SmartInbox I={I} /> 로 사용 (I = 아이콘 컴포넌트). */
|
|
|
(function () {
|
|
|
const { useState, useRef } = React;
|
|
|
const SD = window.AriSmartInbox;
|
|
|
|
|
|
const SPHERE = {
|
|
|
work: { label: "업무", tone: "var(--blue)" },
|
|
|
life: { label: "개인", tone: "var(--green)" },
|
|
|
};
|
|
|
const TYPE_ICON = { task: "check", event: "cal", idea: "brain" };
|
|
|
|
|
|
/* ---------- 데모 파서 — 규칙 기반이지만 '아리의 판단'을 보여주는 용도 ---------- */
|
|
|
function parseCapture(text) {
|
|
|
const t = text.trim();
|
|
|
const base = {
|
|
|
type: "task", typeLabel: "작업", sphere: "life",
|
|
|
proj: "개인 · 분류 중", tone: "green", due: null, when: null, extra: null, reason: "",
|
|
|
};
|
|
|
const hasTime = /(\d{1,2})\s*시|오전|오후|내일|모레|이번\s*주|다음\s*주|[월화수목금토일]요일/.test(t);
|
|
|
|
|
|
if (/비행기|항공|티켓|예매|숙소|호텔/.test(t)) return { ...base,
|
|
|
proj: "개인 › 여행 — 한국", tone: "coral", due: "출발 전", when: "오늘 21:00 빈 시간 추천", extra: "가격 추적 알림 켜둠",
|
|
|
reason: "구매 행동이 필요해서 '작업'으로 — 작업 트리의 '개인 › 여행' 프로젝트에 다른 작업과 똑같이 들어가요. 저녁 빈 시간에 하라고 추천만 해두고, 가격 알림도 걸어뒀어요." };
|
|
|
|
|
|
if (/미팅|회의|싱크|리뷰/.test(t) && hasTime) return { ...base,
|
|
|
type: "event", typeLabel: "일정", sphere: "work", proj: "업무 캘린더", tone: "blue",
|
|
|
when: "캘린더 등록 + 참석자 초대 준비", reason: "시간이 정해진 업무 일은 일정으로 바로 등록해요." };
|
|
|
|
|
|
if (/약속|예약|만나|생일|모임/.test(t) && hasTime) return { ...base,
|
|
|
type: "event", typeLabel: "일정", sphere: "life", proj: "개인 캘린더", tone: "blue",
|
|
|
when: "캘린더 등록 완료", reason: "시간이 정해진 일은 작업이 아니라 일정으로 바로 등록해요." };
|
|
|
|
|
|
if (/어떨까|해볼까|아이디어|괜찮을 듯|좋을 것 같/.test(t)) return { ...base,
|
|
|
type: "idea", typeLabel: "아이디어", sphere: "work", proj: "아이디어 보드", tone: "violet",
|
|
|
reason: "아직 행동이 정해지지 않아 보드에 보관해요 — 관련 프로젝트에 연결해뒀어요." };
|
|
|
|
|
|
if (/사기|사야|주문|구매|선물|장보/.test(t)) return { ...base,
|
|
|
proj: "쇼핑 리스트", tone: "amber", when: "주말 오전 장보기 블록",
|
|
|
reason: "구매 작업은 쇼핑 리스트에 모아 주말 블록에 한 번에 처리해요." };
|
|
|
|
|
|
if (/리포트|온보딩|회신|피드백|배포|QA|문서/.test(t)) return { ...base,
|
|
|
sphere: "work", proj: "업무 · 관련 프로젝트", tone: "blue", when: "내일 오전 빈 블록",
|
|
|
reason: "업무 작업으로 분류해 내일 오전 빈 블록에 배치했어요." };
|
|
|
|
|
|
return { ...base, when: "이번 주 개인 시간",
|
|
|
reason: "마감이 또렷하지 않아 이번 주 개인 시간에 느슨하게 두었어요 — 아니면 칩을 눌러 바꿔주세요." };
|
|
|
}
|
|
|
|
|
|
const cycleType = (r) => {
|
|
|
const next = { task: "event", event: "idea", idea: "task" }[r.type];
|
|
|
const label = { task: "작업", event: "일정", idea: "아이디어" }[next];
|
|
|
return { ...r, type: next, typeLabel: label };
|
|
|
};
|
|
|
|
|
|
function SmartInbox({ I }) {
|
|
|
const [items, setItems] = useState(SD.items);
|
|
|
const [input, setInput] = useState("");
|
|
|
const [filter, setFilter] = useState("all");
|
|
|
const seq = useRef(0);
|
|
|
|
|
|
const submit = () => {
|
|
|
const text = input.trim();
|
|
|
if (!text) return;
|
|
|
const id = "u" + (++seq.current);
|
|
|
setItems((xs) => [{ id, kind: "text", time: "방금", status: "thinking", raw: text, route: null }, ...xs]);
|
|
|
setInput("");
|
|
|
setTimeout(() => {
|
|
|
setItems((xs) => xs.map((x) => x.id === id ? { ...x, status: "new", route: parseCapture(text) } : x));
|
|
|
}, 900);
|
|
|
};
|
|
|
|
|
|
const confirm = (id) => setItems((xs) => xs.map((x) => x.id === id ? { ...x, status: "done" } : x));
|
|
|
const reType = (id) => setItems((xs) => xs.map((x) => x.id === id ? { ...x, route: cycleType(x.route) } : x));
|
|
|
|
|
|
const shown = items.filter((x) => filter === "all" || (x.route && x.route.sphere === filter));
|
|
|
|
|
|
return (
|
|
|
<section className="card sbox sp2">
|
|
|
<div className="ch">
|
|
|
<div className="ico"><I d="inbox" /></div>
|
|
|
<div className="htext">
|
|
|
<h3>스마트 인박스</h3>
|
|
|
<div className="sub">업무든 일상이든 일단 적기 — 분류는 아리가 해요</div>
|
|
|
</div>
|
|
|
<span className="count">오늘 {SD.todayRouted}건 정리</span>
|
|
|
</div>
|
|
|
|
|
|
{/* 입력 */}
|
|
|
<div className="sb-cmd">
|
|
|
<I d="spark" />
|
|
|
<input
|
|
|
value={input}
|
|
|
onChange={(e) => setInput(e.target.value)}
|
|
|
onKeyDown={(e) => e.key === "Enter" && submit()}
|
|
|
placeholder="갑자기 생각난 것 아무거나… 예) 다음 주 한국 가는 비행기 티켓 사기"
|
|
|
/>
|
|
|
<button className="sb-mode" aria-label="음성"><I d="mic" /></button>
|
|
|
<button className="sb-mode" aria-label="이미지"><I d="image" /></button>
|
|
|
<button className="sb-send" onClick={submit} aria-label="보내기"><I d="arrow" /></button>
|
|
|
</div>
|
|
|
|
|
|
{/* 컨텍스트 필터 */}
|
|
|
<div className="sb-filters">
|
|
|
{[["all", "전체"], ["work", "업무"], ["life", "개인"]].map(([k, l]) => (
|
|
|
<button key={k} className={"sb-f" + (filter === k ? " on" : "")} onClick={() => setFilter(k)}>
|
|
|
{k !== "all" && <span className="fdot" style={{ background: SPHERE[k].tone }} />}{l}
|
|
|
</button>
|
|
|
))}
|
|
|
<span className="sb-hint">업무/개인은 필터일 뿐 — 작업은 한 곳에서 다 보여요</span>
|
|
|
</div>
|
|
|
|
|
|
{/* 캡처 목록 */}
|
|
|
<div className="sb-list">
|
|
|
{shown.map((c) => (
|
|
|
<div className={"sb-cap" + (c.status === "new" ? " fresh" : "")} key={c.id}>
|
|
|
<div className={"cap-k " + c.kind}><I d={c.kind === "voice" ? "mic" : c.kind === "image" ? "image" : "pen"} /></div>
|
|
|
<div className="sb-body">
|
|
|
<div className="sb-raw">{c.raw}</div>
|
|
|
|
|
|
{c.status === "thinking" ? (
|
|
|
<div className="sb-think"><span className="tdot" /><span className="tdot" /><span className="tdot" />아리가 분류하고 있어요</div>
|
|
|
) : (
|
|
|
<>
|
|
|
<div className="sb-route">
|
|
|
<span className="r-arrow"><I d="arrow" /></span>
|
|
|
<button className={"r-chip type " + c.route.type} onClick={() => c.status === "new" && reType(c.id)}>
|
|
|
<I d={TYPE_ICON[c.route.type]} />{c.route.typeLabel}
|
|
|
</button>
|
|
|
<span className="r-chip sphere"><span className="pdot" style={{ background: SPHERE[c.route.sphere].tone }} />{SPHERE[c.route.sphere].label}</span>
|
|
|
<span className="r-chip proj"><span className="pdot" style={{ background: `var(--${c.route.tone})` }} />{c.route.proj}</span>
|
|
|
{c.route.due && <span className="r-chip"><I d="cal" />{c.route.due}</span>}
|
|
|
{c.route.when && <span className="r-chip"><I d="clock" />{c.route.when}</span>}
|
|
|
{c.route.extra && <span className="r-chip auto"><I d="zap" />{c.route.extra}</span>}
|
|
|
</div>
|
|
|
<div className="sb-reason">{c.route.reason}</div>
|
|
|
{c.status === "new" && (
|
|
|
<div className="sb-acts">
|
|
|
<button className="sb-ok" onClick={() => confirm(c.id)}><I d="tick" w="3" />좋아요, 그렇게 해줘</button>
|
|
|
<button className="sb-alt" onClick={() => reType(c.id)}><I d="swap" />다르게 분류</button>
|
|
|
</div>
|
|
|
)}
|
|
|
</>
|
|
|
)}
|
|
|
</div>
|
|
|
<span className="cap-time">{c.status === "done" ? <span className="sb-done"><I d="tick" w="3" /></span> : c.time}</span>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</section>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
window.SmartInbox = SmartInbox;
|
|
|
})();
|