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.

268 lines
11 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.

/* 아리 — 자동화 규칙 빌더 페이지
좌측 레일로 하위 화면 전환: 내 규칙 · 새 자동화 · 아리 제안 · 실행 기록 */
const { useState } = React;
const I = window.AriShell.Icon;
const Topbar = window.AriShell.Topbar;
const SubRail = window.AriShell.SubRail;
const AU = window.AriAuto;
const AU_TONE = { mail: "var(--blue)", cal: "var(--violet)", focus: "var(--coral)", life: "var(--green)" };
const AU_CAT = { mail: "메일", cal: "일정", focus: "집중", life: "라이프" };
function Flow({ p, small }) {
return (
<div className={"fl" + (small ? " rl-flow" : "")}>
<span className="fl-chip tg"><span className="k">트리거</span><span className="v">{p.trigger}</span></span>
{p.cond && (
<React.Fragment>
<span className="fl-arr"><I d="arrow" /></span>
<span className="fl-chip cd"><span className="k">조건</span><span className="v">{p.cond}</span></span>
</React.Fragment>
)}
<span className="fl-arr"><I d="arrow" /></span>
<span className="fl-chip ac"><span className="k">동작</span><span className="v">{p.action}</span></span>
</div>
);
}
/* ===== 내 규칙 ===== */
function RulesView({ rules, toggle }) {
const activeN = rules.filter((r) => r.on).length;
return (
<div className="board">
<section className="card sp2 r2">
<div className="ch">
<div className="ico"><I d="zap" /></div>
<div className="htext"><h3> 규칙</h3><div className="sub"> </div></div>
<span className="count">{activeN}/{rules.length} 켜짐</span>
</div>
<div className="rl-list">
{rules.map((r) => (
<div key={r.id} className={"rl-row" + (r.on ? "" : " off") + (r.fresh ? " rl-new" : "")}>
<button
className={"sw" + (r.on ? " on" : "")}
onClick={() => toggle(r.id)}
aria-label={r.name + (r.on ? " 끄기" : " 켜기")}
></button>
<div className="rl-body">
<div className="rl-top">
<span className="rl-name">{r.name}</span>
<span className="rl-cat" style={{ background: AU_TONE[r.cat] }}>{AU_CAT[r.cat]}</span>
</div>
<Flow small p={{ trigger: r.trigger, cond: r.cond, action: r.action }} />
<div className="rl-meta">
<span>마지막 실행 {r.last}</span>
<span>·</span>
<span>이번 {r.runs}</span>
</div>
</div>
</div>
))}
</div>
</section>
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="shield" /></div>
<div className="htext"><h3>자동화 작동 방식</h3><div className="sub"> </div></div>
</div>
<div className="how-list">
<div className="how"><span className="how-ic"><I d="tick" /></span><div><b> </b><span> .</span></div></div>
<div className="how"><span className="how-ic"><I d="shield" /></span><div><b> </b><span>·· .</span></div></div>
<div className="how"><span className="how-ic"><I d="brain" /></span><div><b> </b><span> .</span></div></div>
</div>
</section>
</div>
);
}
/* ===== 새 자동화 (작성 플로우) ===== */
function NewView({ onCreate }) {
const [text, setText] = useState("");
const match = AU.examples.find((e) => e.text === text.trim());
return (
<div className="board">
<section className="card sp2 hero">
<span className="hero-spark"><I d="zap" /> 자동화 만들기</span>
<p className="brief">하고 싶은 일을 <b> 문장</b> . ·· .</p>
<div className="cmd">
<I d="pen" />
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && match) onCreate(match); }}
placeholder="예: 출장 전날엔 저녁 일정 비워줘"
aria-label="새 규칙 문장"
autoFocus
/>
<button className="send" disabled={!match} onClick={() => match && onCreate(match)} aria-label="규칙 만들기"><I d="arrow" /></button>
</div>
<div className="chips">
{AU.examples.map((e) => (
<button key={e.text} className="chip" onClick={() => setText(e.text)}>
<I d="spark" />{e.text}
</button>
))}
</div>
</section>
{/* 미리보기 */}
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="spark" /></div>
<div className="htext"><h3>미리보기</h3><div className="sub">{match ? " " : " "}</div></div>
{match && <span className="rl-cat" style={{ background: AU_TONE[match.cat] }}>{AU_CAT[match.cat]}</span>}
</div>
{match ? (
<React.Fragment>
<div className="nv-name">{match.name}</div>
<Flow p={match.parse} />
<button className="rb-make-btn" onClick={() => onCreate(match)}>
<I d="plus" /> 규칙 만들기
</button>
<div className="nv-hint"><I d="shield" />만든 규칙은 규칙에서 언제든 끄거나 지울 있어요.</div>
</React.Fragment>
) : (
<div className="nv-empty">
<span className="nv-empty-ic"><I d="zap" /></span>
<p>예시 문장을 누르거나 직접 적어보세요. 아리가 <b>트리거 조건 동작</b> .</p>
</div>
)}
</section>
</div>
);
}
/* ===== 아리 제안 ===== */
function SuggestView({ suggests, accept, dismiss }) {
return (
<div className="board">
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="brain" /></div>
<div className="htext"><h3>아리 제안</h3><div className="sub"> </div></div>
<span className="count">{suggests.length}</span>
</div>
<div className="sg">
{suggests.map((s) => (
<div className="sg-card" key={s.id}>
<div className="sg-pat"><I d="spark" /><span>{s.pattern}</span></div>
<div className="sg-offer">
<div className="sg-name">{s.offer.name}</div>
<Flow small p={s.offer} />
</div>
<div className="sg-btns">
<button className="sg-yes" onClick={() => accept(s)}><I d="plus" />규칙 만들기</button>
<button className="sg-no" onClick={() => dismiss(s)}>무시</button>
</div>
</div>
))}
{suggests.length === 0 && (
<div className="sg-empty"><I d="tick" />지금은 제안이 없어요. 패턴이 보이면 알려드릴게요.</div>
)}
</div>
</section>
</div>
);
}
/* ===== 실행 기록 ===== */
function LogView() {
return (
<div className="board">
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="clock" /></div>
<div className="htext"><h3>실행 기록</h3><div className="sub"> </div></div>
<span className="count">오늘 {AU.log.filter((l) => !l.time.startsWith("어제")).length}</span>
</div>
<div className="rb-log">
{AU.log.map((l, i) => (
<div className="lg-row" key={i}>
<span className="lg-time">{l.time}</span>
<div className="lg-body">
<span className="lg-rule">{l.rule}</span>
<div className="lg-text">{l.text}</div>
</div>
<button className="lg-undo">되돌리기</button>
</div>
))}
</div>
</section>
</div>
);
}
const AU_RAIL = (suggestN) => [
{ id: "rules", icon: "zap", label: "내 규칙" },
{ id: "new", icon: "plus", label: "새 자동화" },
{ sep: true },
{ id: "suggest", icon: "brain", label: "아리 제안", dot: suggestN > 0 },
{ id: "log", icon: "clock", label: "실행 기록" },
];
const AU_HEAD = {
rules: { eye: ["규칙으로 굴러가는 하루"], title: "내 규칙", em: "켜두면 알아서 굴러가요" },
new: { eye: ["한 문장이면 충분해요"], title: "새 자동화", em: "말하듯 적으면 규칙이 돼요" },
suggest: { eye: ["아리가 지켜보다 발견한 것"], title: "아리 제안", em: "반복되는 일을 자동으로" },
log: { eye: ["조용히 일한 기록"], title: "실행 기록", em: "규칙이 움직인 순간들" },
};
function App() {
const [view, setView] = useState("rules");
const [rules, setRules] = useState(AU.rules);
const [suggests, setSuggests] = useState(AU.suggests);
const [toast, setToast] = useState(null);
const activeN = rules.filter((r) => r.on).length;
const toggle = (id) => setRules(rules.map((r) => (r.id === id ? { ...r, on: !r.on } : r)));
const addRule = (cat, name, parse) => {
setRules((rs) => [
{ id: "new-" + Date.now(), on: true, cat, name, trigger: parse.trigger, cond: parse.cond, action: parse.action, last: "방금 만듦", runs: 0, fresh: true },
...rs,
]);
setToast("" + name + " 규칙을 켰어요.");
setView("rules");
setTimeout(() => setToast(null), 3200);
};
const createFromExample = (m) => addRule(m.cat, m.name, m.parse);
const accept = (s) => { addRule(s.offer.cat, s.offer.name, s.offer); setSuggests((x) => x.filter((y) => y.id !== s.id)); };
const dismiss = (s) => setSuggests((x) => x.filter((y) => y.id !== s.id));
const h = AU_HEAD[view];
return (
<div className="dash" data-screen-label="자동화">
<Topbar current="auto" />
<div className="pagehead">
<div>
<div className="ph-eyebrow">
<span>활성 규칙 {activeN}</span>
<span className="sep"></span>
<span>이번 {AU.stats.runsWeek} 실행</span>
<span className="sep"></span>
<span>{AU.stats.saved} 아꼈어요</span>
</div>
<h1 className="ph-title">{h.title} <em>{h.em}</em></h1>
</div>
</div>
<div className="work">
<SubRail items={AU_RAIL(suggests.length)} active={view} onPick={setView} />
{view === "rules" && <RulesView rules={rules} toggle={toggle} />}
{view === "new" && <NewView onCreate={createFromExample} />}
{view === "suggest" && <SuggestView suggests={suggests} accept={accept} dismiss={dismiss} />}
{view === "log" && <LogView />}
</div>
{toast && <div className="au-toast"><I d="tick" />{toast}</div>}
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);