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.

157 lines
8.3 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.

/* 아리 — 스마트 인박스 위젯
입력하면 아리가 실시간으로 분류: 작업/일정/아이디어 × 업무/개인 컨텍스트 + 배치 + 자동화.
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;
})();