yana-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ // Yana Mobile — Memory Garden + Skills
2
+ const M_MEM_KINDS = [
3
+ { id: "Fact", color: "var(--primary)", soft: "var(--primary-soft)" },
4
+ { id: "Knowledge", color: "var(--gold)", soft: "var(--gold-soft)" },
5
+ { id: "Experience", color: "var(--pink)", soft: "var(--pink-soft)" },
6
+ { id: "Context", color: "var(--ink-2)", soft: "rgba(var(--surface-rgb), .6)" },
7
+ ];
8
+ const M_KIND_VI = { All: "Tất cả", Fact: "Dữ kiện", Knowledge: "Kiến thức", Experience: "Trải nghiệm", Context: "Ngữ cảnh" };
9
+
10
+ function MMemoryCard({ m }) {
11
+ // L1 kinds outside the four buckets render with the neutral Context style
12
+ const kind = M_MEM_KINDS.find((k) => k.id === m.kind) || M_MEM_KINDS[3];
13
+ return (
14
+ <div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "15px 16px", display: "flex", flexDirection: "column", gap: 9 }}>
15
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
16
+ <span className="chip" style={{ background: kind.soft, color: kind.color, fontSize: 11 }}>{L(m.kind, M_KIND_VI[m.kind])}</span>
17
+ {m.pinned && <span style={{ color: "var(--gold)", display: "inline-flex" }}>{Icons.pin(13)}</span>}
18
+ {m.fresh && !m.pinned && <span className="chip pink" style={{ fontSize: 10.5, padding: "1px 8px" }}>{L("New", "Mới")}</span>}
19
+ </div>
20
+ <div style={{ fontSize: 13.5, lineHeight: 1.55, color: "var(--ink)" }}>{m.text}</div>
21
+ <div style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{m.source}</div>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ function MMemoryGarden() {
27
+ const D = window.YANA;
28
+ const [filter, setFilter] = React.useState("All");
29
+ const kinds = ["All", ...M_MEM_KINDS.map((k) => k.id)];
30
+ const list = filter === "All" ? D.memories : D.memories.filter((m) => m.kind === filter);
31
+ return (
32
+ <div data-screen-label="Memory Garden" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
33
+ <MHead title={L("Memory Garden", "Vườn ký ức")} sub={D.stats.memories.toLocaleString() + L(" memories · +", " ký ức · +") + D.stats.memoriesToday + L(" planted today", " trồng hôm nay")} />
34
+ <div className="hscroll">
35
+ {kinds.map((k) => (
36
+ <button key={k} className="fchip" data-on={filter === k ? "1" : "0"} onClick={() => setFilter(k)}>{L(k, M_KIND_VI[k])}</button>
37
+ ))}
38
+ </div>
39
+ <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
40
+ {list.map((m) => <MMemoryCard key={m.id} m={m} />)}
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ /* ---------- Skills ---------- */
47
+ function MSkills() {
48
+ const D = window.YANA;
49
+ const max = Math.max(...D.skillCategories.map((c) => c.usage));
50
+ return (
51
+ <div data-screen-label="Skills" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
52
+ <MHead title={L("Skills", "Kỹ năng")} sub={D.stats.skills.toLocaleString() + L(" installed · ", " đã cài · ") + D.stats.skillsUsedToday + L(" invoked today · deny-by-default", " được gọi hôm nay · mặc định từ chối")} />
53
+ <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
54
+ {D.skillCategories.map((c) => (
55
+ <div key={c.name} className="glass" style={{ borderRadius: "var(--r-lg)", padding: "15px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
56
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
57
+ <span style={{ fontSize: 14.5, fontWeight: 500 }}>{c.name}</span>
58
+ <span style={{ fontSize: 12.5, color: "var(--ink-3)" }}>{c.count.toLocaleString()}</span>
59
+ </div>
60
+ <div className="bar"><i style={{ width: (c.usage / max) * 100 + "%" }}></i></div>
61
+ <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, color: "var(--ink-3)", gap: 10 }}>
62
+ <span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{L("Top:", "Dùng nhiều:")} <code style={{ fontFamily: "ui-monospace, monospace", fontSize: 11.5, color: "var(--ink-2)" }}>{c.top}</code></span>
63
+ <span style={{ flex: "none" }}>{c.usage}%</span>
64
+ </div>
65
+ </div>
66
+ ))}
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ Object.assign(window, { MMemoryGarden, MSkills });
@@ -0,0 +1,190 @@
1
+ // Yana Mobile — Lake (Dashboard): compact composer + stacked control cards
2
+ function MStat({ label, value, sub, accent }) {
3
+ return (
4
+ <div className="glass" style={{ borderRadius: "var(--r-md)", padding: "13px 14px", display: "flex", flexDirection: "column", gap: 3 }}>
5
+ <span className="label-xs" style={{ fontSize: 10 }}>{label}</span>
6
+ <span style={{ fontSize: 25, fontWeight: 300, letterSpacing: "-0.02em", lineHeight: 1.1 }}>{value}</span>
7
+ <span style={{ fontSize: 11.5, color: accent ? "var(--primary)" : "var(--ink-3)" }}>{sub}</span>
8
+ </div>
9
+ );
10
+ }
11
+
12
+ /* Compact composer: one tappable bar that drops into the Mission flow */
13
+ function MComposer({ onNav }) {
14
+ const D = window.YANA;
15
+ const [v, setV] = React.useState("");
16
+ const suggestions = [
17
+ ["Ship v0.9 safely", "Phát hành v0.9 an toàn"],
18
+ ["Summarize overnight", "Tóm tắt qua đêm"],
19
+ ["Prune stale memories", "Dọn ký ức cũ"],
20
+ ];
21
+ async function begin(text) {
22
+ const goal = (text || v).trim();
23
+ if (!goal) return;
24
+ onNav("missions");
25
+ // POST to real API — server classifies the goal and creates starter tasks
26
+ try {
27
+ const r = await fetch("/api/missions", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify({ goal }),
31
+ });
32
+ if (r.ok) {
33
+ const { mission } = await r.json();
34
+ D.missions.unshift(mission);
35
+ D._openMission = mission.id;
36
+ D.stats.missionsActive += 1;
37
+ window.dispatchEvent(new Event("yana:data"));
38
+ }
39
+ } catch (_) {
40
+ // Offline fallback — local optimistic entry shown until server responds
41
+ const id = "m" + Date.now();
42
+ D.missions.unshift({
43
+ id, name: goal, owner: "Navigator", progress: 0, due: "Planning", status: "analyzing",
44
+ tasks: [
45
+ { name: "Understanding the goal", agent: "Navigator", state: "active" },
46
+ { name: "Choosing agents & skills", agent: "Navigator", state: "queued" },
47
+ { name: "Drafting a plan for your review", agent: "Navigator", state: "queued" },
48
+ ],
49
+ });
50
+ D._openMission = id;
51
+ D.stats.missionsActive += 1;
52
+ window.dispatchEvent(new Event("yana:data"));
53
+ }
54
+ }
55
+ return (
56
+ <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
57
+ <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 10 }}>
58
+ <h1 className="h-display" style={{ margin: 0, fontSize: 23 }}>{window.YANA.username ? L("Good morning, " + window.YANA.username, "Chào " + window.YANA.username) : L("Good morning", "Xin chào")}</h1>
59
+ <span style={{ fontSize: 11.5, color: "var(--ink-3)", display: "inline-flex", alignItems: "center", gap: 6, flex: "none" }}>
60
+ <span className="dot on pulse"></span>{L("Calm", "Tĩnh lặng")}
61
+ </span>
62
+ </div>
63
+ <div className="glass-strong" style={{ borderRadius: 16, padding: "7px 7px 7px 15px", display: "flex", alignItems: "center", gap: 9 }}>
64
+ <input
65
+ value={v}
66
+ onChange={(e) => setV(e.target.value)}
67
+ onKeyDown={(e) => { if (e.key === "Enter") begin(); }}
68
+ placeholder={L("What should we accomplish today?", "Hôm nay ta làm gì?")}
69
+ style={{ flex: 1, minWidth: 0, border: "none", outline: "none", background: "transparent", fontSize: 14.5, fontFamily: "inherit", color: "var(--ink)" }}
70
+ />
71
+ <button onClick={() => begin()} aria-label="New Mission" style={{
72
+ width: 38, height: 38, borderRadius: 12, border: "none", cursor: "pointer", flex: "none",
73
+ background: "var(--primary)", color: "white", display: "grid", placeItems: "center",
74
+ boxShadow: "0 4px 14px color-mix(in oklab, var(--primary) 32%, transparent)",
75
+ }}>{Icons.spark(17)}</button>
76
+ </div>
77
+ <div className="hscroll" style={{ margin: 0, padding: 0 }}>
78
+ {suggestions.map(([en, vi]) => (
79
+ <button key={en} onClick={() => begin(en)} className="chip neutral" style={{ cursor: "pointer", fontSize: 12 }}>{L(en, vi)}</button>
80
+ ))}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ function MModelRow({ m }) {
87
+ return (
88
+ <div className="mrow" style={{ display: "grid", gridTemplateColumns: "14px 1fr 70px 42px", alignItems: "center", gap: 11 }}>
89
+ <span className={"dot " + (m.status === "active" ? "on" : "idle")}></span>
90
+ <div style={{ lineHeight: 1.3, minWidth: 0 }}>
91
+ <div style={{ fontSize: 13.5, fontWeight: 500 }}>{m.name} <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>{m.model}</span></div>
92
+ <div style={{ fontSize: 11.5, color: "var(--ink-3)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.role}</div>
93
+ </div>
94
+ <div className="bar"><i style={{ width: m.load + "%" }}></i></div>
95
+ <span style={{ fontSize: 11.5, color: "var(--ink-2)", textAlign: "right" }}>{m.latency}</span>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ function MMissionRow({ m, onOpen }) {
101
+ return (
102
+ <button onClick={onOpen} className="mrow" style={{
103
+ display: "grid", gridTemplateColumns: "1fr 64px 38px", alignItems: "center", gap: 11,
104
+ width: "100%", textAlign: "left", background: "none", border: "none", cursor: "pointer", color: "inherit",
105
+ borderBottom: "1px solid var(--border)",
106
+ }}>
107
+ <div style={{ lineHeight: 1.3, minWidth: 0 }}>
108
+ <div style={{ fontSize: 13.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.name}</div>
109
+ <div style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{m.owner} · {L("due", "hạn")} {m.due}</div>
110
+ </div>
111
+ <div className="bar"><i style={{ width: m.progress + "%" }}></i></div>
112
+ <span style={{ fontSize: 11.5, color: "var(--ink-2)", textAlign: "right" }}>{m.progress}%</span>
113
+ </button>
114
+ );
115
+ }
116
+
117
+ function MLake({ t, onNav }) {
118
+ const D = window.YANA;
119
+ const activeAgents = D.agents.filter((a) => a.status === "active");
120
+ return (
121
+ <div data-screen-label="Lake" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
122
+ <MComposer onNav={onNav} />
123
+
124
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 9 }}>
125
+ <MStat label={L("Agents", "Tác nhân")} value={<>{D.stats.agentsActive}<span style={{ fontSize: 15, color: "var(--ink-3)" }}> / {D.stats.agents}</span></>} sub={L("active now", "đang hoạt động")} accent />
126
+ <MStat label={L("Missions", "Nhiệm vụ")} value={D.stats.missionsActive} sub={L("in motion", "đang diễn ra")} />
127
+ <MStat label={L("Skills", "Kỹ năng")} value={D.stats.skills.toLocaleString()} sub={D.stats.skillsUsedToday + L(" today", " hôm nay")} />
128
+ <MStat label={L("Memories", "Ký ức")} value={D.stats.memories.toLocaleString()} sub={"+" + D.stats.memoriesToday + L(" today", " hôm nay")} />
129
+ </div>
130
+
131
+ <MCard title={L("Active AI Models", "Mô hình đang chạy")} aside={<span className="chip neutral">{D.models.length} {L("providers", "NCC")}</span>}>
132
+ <div style={{ display: "flex", flexDirection: "column" }}>
133
+ {D.models.length === 0 && <span style={{ fontSize: 12.5, color: "var(--ink-3)" }}>{L("Loading providers…", "Đang tải nhà cung cấp…")}</span>}
134
+ {D.models.map((m) => <MModelRow key={m.id} m={m} />)}
135
+ </div>
136
+ </MCard>
137
+
138
+ {t.showMissions && (
139
+ <MCard title={L("Missions", "Nhiệm vụ")} aside={<SeeAll label={L("Mission Center", "Trung tâm")} onClick={() => onNav("missions")} />}>
140
+ {D.missions.filter((m) => m.status !== "recurring").slice(0, 4).map((m) => (
141
+ <MMissionRow key={m.id} m={m} onOpen={() => { D._openMission = m.id; onNav("missions"); }} />
142
+ ))}
143
+ </MCard>
144
+ )}
145
+
146
+ {t.showAgents && (
147
+ <MCard title={L("Running Agents", "Tác nhân đang chạy")} aside={<SeeAll label={L("Agent Space", "Tác nhân")} onClick={() => onNav("agents")} />}>
148
+ {activeAgents.slice(0, 5).map((a) => (
149
+ <div key={a.id} className="mrow" style={{ display: "flex", alignItems: "center", gap: 11 }}>
150
+ <span className={"dot " + (a.status === "active" ? "on" : "idle")}></span>
151
+ <span style={{ fontSize: 13.5, fontWeight: 500, width: 78, flex: "none" }}>{a.name}</span>
152
+ <span style={{ fontSize: 12, color: "var(--ink-3)", flex: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.load}</span>
153
+ </div>
154
+ ))}
155
+ </MCard>
156
+ )}
157
+
158
+ {t.showMemory && (
159
+ <MCard title={L("Memory Garden", "Vườn ký ức")} aside={<span className="chip pink">{Icons.memory(13)} +{D.stats.memoriesToday}</span>}>
160
+ {D.memories.filter((m) => m.fresh).slice(0, 3).map((m) => (
161
+ <div key={m.id} className="mrow" style={{ display: "flex", gap: 10, alignItems: "baseline" }}>
162
+ <span className="chip neutral" style={{ flex: "none", fontSize: 10.5 }}>{L(m.kind, { Fact: "Dữ kiện", Knowledge: "Kiến thức", Experience: "Trải nghiệm", Context: "Ngữ cảnh" }[m.kind])}</span>
163
+ <span style={{ fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.45 }}>{m.text}</span>
164
+ </div>
165
+ ))}
166
+ </MCard>
167
+ )}
168
+
169
+ {t.showSystem && (
170
+ <MCard title={L("System Health", "Sức khỏe hệ thống")}>
171
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "11px 14px" }}>
172
+ {[
173
+ [L("Safety checks", "Kiểm tra an toàn"), D.safety.checksToday.toLocaleString() + L(" today", " hôm nay")],
174
+ [L("Blocked", "Bị chặn"), D.safety.blocked + L(" · ", " · ") + D.safety.pendingReview + L(" in review", " chờ duyệt")],
175
+ [L("Last incident", "Sự cố gần nhất"), D.safety.lastIncident],
176
+ [L("Uptime", "Thời gian chạy"), D.stats.uptimeDays + L(" days", " ngày")],
177
+ ].map(([k, v]) => (
178
+ <div key={k} style={{ lineHeight: 1.35 }}>
179
+ <div style={{ fontSize: 11, color: "var(--ink-3)" }}>{k}</div>
180
+ <div style={{ fontSize: 13, fontWeight: 500 }}>{v}</div>
181
+ </div>
182
+ ))}
183
+ </div>
184
+ </MCard>
185
+ )}
186
+ </div>
187
+ );
188
+ }
189
+
190
+ window.MLake = MLake;
@@ -0,0 +1,81 @@
1
+ // Yana Mobile — Mission Center
2
+ const M_TASK_STATE = {
3
+ done: { label: () => L("Done", "Xong"), color: "var(--good)" },
4
+ active: { label: () => L("Active", "Đang chạy"), color: "var(--primary)" },
5
+ queued: { label: () => L("Queued", "Đang chờ"), color: "var(--ink-3)" },
6
+ };
7
+
8
+ function MMissionCard({ m, open, onToggle }) {
9
+ const statusColor = m.status === "recurring" ? "var(--gold)" : m.status === "analyzing" ? "var(--primary)" : "var(--good)";
10
+ return (
11
+ <div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "var(--pad-card)" }}>
12
+ <button onClick={onToggle} style={{
13
+ display: "flex", flexDirection: "column", gap: 9,
14
+ width: "100%", background: "none", border: "none", cursor: "pointer", textAlign: "left", color: "inherit", padding: 0,
15
+ }}>
16
+ <div style={{ display: "flex", alignItems: "flex-start", gap: 10, width: "100%" }}>
17
+ <div style={{ lineHeight: 1.3, minWidth: 0, flex: 1 }}>
18
+ <div style={{ fontSize: 14.5, fontWeight: 500 }}>{m.name}</div>
19
+ <div style={{ fontSize: 11.5, color: "var(--ink-3)", marginTop: 2 }}>
20
+ {m.owner} · {L("due", "hạn")} {m.due} · <span style={{ color: statusColor }}>{m.status}</span>
21
+ </div>
22
+ </div>
23
+ <span style={{ color: "var(--ink-3)", transform: open ? "rotate(90deg)" : "none", transition: "transform .18s", display: "inline-flex", flex: "none", marginTop: 2 }}>{Icons.chevron(15)}</span>
24
+ </div>
25
+ <div style={{ display: "flex", alignItems: "center", gap: 10, width: "100%" }}>
26
+ <div className="bar" style={{ flex: 1 }}><i style={{ width: m.progress + "%" }}></i></div>
27
+ <span style={{ fontSize: 12, color: "var(--ink-2)", flex: "none", width: 36, textAlign: "right" }}>{m.progress}%</span>
28
+ </div>
29
+ </button>
30
+ {open && (
31
+ <div style={{ marginTop: 13, paddingTop: 11, borderTop: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 2 }}>
32
+ {m.tasks.map((tk) => {
33
+ const st = M_TASK_STATE[tk.state];
34
+ return (
35
+ <div key={tk.name} style={{ display: "grid", gridTemplateColumns: "16px 1fr auto", alignItems: "center", gap: 10, padding: "7px 0", fontSize: 13 }}>
36
+ <span style={{ color: st.color, display: "inline-flex" }}>
37
+ {tk.state === "done" ? Icons.check(14) : tk.state === "active" ? Icons.spark(14) : Icons.clock(14)}
38
+ </span>
39
+ <div style={{ minWidth: 0 }}>
40
+ <div style={{ color: tk.state === "done" ? "var(--ink-3)" : "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{tk.name}</div>
41
+ <div style={{ fontSize: 11, color: "var(--ink-3)" }}>{tk.agent}</div>
42
+ </div>
43
+ <span style={{ fontSize: 11.5, fontWeight: 500, color: st.color, textAlign: "right", flex: "none" }}>{st.label()}</span>
44
+ </div>
45
+ );
46
+ })}
47
+ </div>
48
+ )}
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function MMissions() {
54
+ const D = window.YANA;
55
+ const [open, setOpen] = React.useState(() => window.YANA._openMission || "m1");
56
+ const [filter, setFilter] = React.useState("active");
57
+ const filters = [
58
+ ["active", L("Active", "Đang chạy")],
59
+ ["all", L("All", "Tất cả")],
60
+ ["recurring", L("Recurring", "Định kỳ")],
61
+ ];
62
+ const list = D.missions.filter((m) =>
63
+ filter === "all" ? true : filter === "recurring" ? m.status === "recurring" : m.status !== "recurring");
64
+ return (
65
+ <div data-screen-label="Mission Center" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
66
+ <MHead title={L("Missions", "Nhiệm vụ")} sub={L("Multi-agent work, visible end to end.", "Công việc đa tác nhân, theo dõi đầu đến cuối.")} />
67
+ <div className="hscroll">
68
+ {filters.map(([id, lbl]) => (
69
+ <button key={id} className="fchip" data-on={filter === id ? "1" : "0"} onClick={() => setFilter(id)}>{lbl}</button>
70
+ ))}
71
+ </div>
72
+ <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
73
+ {list.map((m) => (
74
+ <MMissionCard key={m.id} m={m} open={open === m.id} onToggle={() => setOpen(open === m.id ? null : m.id)} />
75
+ ))}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ window.MMissions = MMissions;
@@ -0,0 +1,237 @@
1
+ // Yana Mobile — shell: icons, wordmark, bilingual helper, mobile chrome, atoms
2
+ const { useState, useEffect, useMemo, useRef } = React;
3
+
4
+ /* Bilingual helper: L("English", "Tiếng Việt") */
5
+ window.L = (en, vi) => (window.YANA_LANG === "vi" ? vi : en);
6
+
7
+ /* ---------- Icons: minimal 1.5px stroke, 20px grid ---------- */
8
+ function Ic({ d, size = 18, ...rest }) {
9
+ return (
10
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="none"
11
+ stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...rest}>
12
+ {d}
13
+ </svg>
14
+ );
15
+ }
16
+ const Icons = {
17
+ dashboard: (s) => <Ic size={s} d={<><path d="M10 3.2 16.5 8v8.2a.8.8 0 0 1-.8.8h-3.2v-4.4H7.5V17H4.3a.8.8 0 0 1-.8-.8V8L10 3.2Z"/></>} />,
18
+ chat: (s) => <Ic size={s} d={<path d="M17 9.5c0 3.3-3.1 6-7 6-.9 0-1.8-.14-2.6-.4L3 16.5l1.2-3.1C3.4 12.3 3 11 3 9.5c0-3.3 3.1-6 7-6s7 2.7 7 6Z"/>} />,
19
+ agents: (s) => <Ic size={s} d={<><circle cx="7" cy="7.5" r="3"/><path d="M2.5 16.5c.6-2.6 2.4-4 4.5-4s3.9 1.4 4.5 4"/><circle cx="14.5" cy="8.5" r="2.2"/><path d="M13.3 12.6c2 .2 3.6 1.5 4.2 3.9"/></>} />,
20
+ missions: (s) => <Ic size={s} d={<><circle cx="10" cy="10" r="7"/><circle cx="10" cy="10" r="3.4"/><circle cx="10" cy="10" r="0.4" fill="currentColor"/></>} />,
21
+ memory: (s) => <Ic size={s} d={<><path d="M10 17c4-1.6 6.2-4.4 6.2-8.1C16.2 6 14.5 4 12.3 4 11.3 4 10.4 4.5 10 5.3 9.6 4.5 8.7 4 7.7 4 5.5 4 3.8 6 3.8 8.9 3.8 12.6 6 15.4 10 17Z"/><path d="M10 17V9.5"/></>} />,
22
+ skills: (s) => <Ic size={s} d={<><path d="m10 3 7 3.5L10 10 3 6.5 10 3Z"/><path d="m3 10.5 7 3.5 7-3.5"/><path d="m3 14.5 7 3.5 7-3.5"/></>} />,
23
+ safety: (s) => <Ic size={s} d={<><path d="M10 2.8 16 5v4.7c0 3.6-2.4 6.4-6 7.8-3.6-1.4-6-4.2-6-7.8V5l6-2.2Z"/><path d="m7.4 9.8 1.8 1.8 3.4-3.6"/></>} />,
24
+ search: (s) => <Ic size={s} d={<><circle cx="9" cy="9" r="5.5"/><path d="m17 17-4-4"/></>} />,
25
+ send: (s) => <Ic size={s} d={<path d="M3.5 10 17 3.5 13.5 17l-3-5.5-7-1.5Zm7 1.5L17 3.5"/>} />,
26
+ check: (s) => <Ic size={s} d={<path d="m4.5 10.5 3.5 3.5 7.5-8"/>} />,
27
+ clock: (s) => <Ic size={s} d={<><circle cx="10" cy="10" r="7"/><path d="M10 6v4.2l2.6 1.6"/></>} />,
28
+ pin: (s) => <Ic size={s} d={<path d="m11.5 3 5.5 5.5-2.8.7-2.5 2.5-.4 3.8-3-3L4 16.3 7.5 12l-3-3 3.8-.4 2.5-2.5.7-2.6Z"/>} />,
29
+ plus: (s) => <Ic size={s} d={<path d="M10 4v12M4 10h12"/>} />,
30
+ chevron: (s) => <Ic size={s} d={<path d="m7.5 4.5 5 5.5-5 5.5"/>} />,
31
+ pause: (s) => <Ic size={s} d={<path d="M7.5 5v10M12.5 5v10"/>} />,
32
+ providers: (s) => <Ic size={s} d={<><circle cx="10" cy="10" r="2.4"/><path d="M10 3v2.6M10 14.4V17M3 10h2.6M14.4 10H17M5.2 5.2l1.8 1.8M13 13l1.8 1.8M14.8 5.2 13 7M7 13l-1.8 1.8"/></>} />,
33
+ settings: (s) => <Ic size={s} d={<><circle cx="10" cy="10" r="2.6"/><path d="M10 2.8v2.4m0 9.6v2.4M2.8 10h2.4m9.6 0h2.4M4.9 4.9l1.7 1.7m6.8 6.8 1.7 1.7m0-10.2-1.7 1.7M6.6 13.4l-1.7 1.7"/></>} />,
34
+ spark: (s) => <Ic size={s} d={<path d="M10 3c.5 3.9 2.6 6.1 7 7-4.4.9-6.5 3.1-7 7-.5-3.9-2.6-6.1-7-7 4.4-.9 6.5-3.1 7-7Z"/>} />,
35
+ more: (s) => <Ic size={s} d={<><circle cx="4.5" cy="10" r="1.3" fill="currentColor" stroke="none"/><circle cx="10" cy="10" r="1.3" fill="currentColor" stroke="none"/><circle cx="15.5" cy="10" r="1.3" fill="currentColor" stroke="none"/></>} />,
36
+ bell: (s) => <Ic size={s} d={<><path d="M6 8.5a4 4 0 0 1 8 0c0 3 1 4.2 1.6 4.8H4.4C5 12.7 6 11.5 6 8.5Z"/><path d="M8.4 15.5a1.8 1.8 0 0 0 3.2 0"/></>} />,
37
+ back: (s) => <Ic size={s} d={<path d="m11.5 5-5 5 5 5"/>} />,
38
+ };
39
+
40
+ /* ---------- Wordmark: lotus bud resting on the water ---------- */
41
+ function YanaMark({ size = 30 }) {
42
+ return (
43
+ <div aria-label="Yana" style={{
44
+ width: size, height: size, borderRadius: size * 0.32, flex: "none",
45
+ background: "linear-gradient(145deg, color-mix(in oklab, var(--primary) 88%, white), color-mix(in oklab, var(--primary) 60%, var(--pink)))",
46
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,.5), 0 4px 12px color-mix(in oklab, var(--primary) 28%, transparent)",
47
+ display: "grid", placeItems: "center",
48
+ }}>
49
+ <svg width={size * 0.62} height={size * 0.62} viewBox="0 0 20 20" fill="none"
50
+ stroke="rgba(255,255,255,.95)" strokeWidth="1.6" strokeLinecap="round">
51
+ <path d="M10 2.8C7.5 5.3 7.5 8.7 10 10.7c2.5-2 2.5-5.4 0-7.9Z" />
52
+ <path d="M4.8 13.3c1.6 1.5 3.3 2.2 5.2 2.2s3.6-.7 5.2-2.2" />
53
+ <path d="M7.3 16.5c.85.6 1.75.9 2.7.9s1.85-.3 2.7-.9" />
54
+ </svg>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ /* ---------- Mobile navigation model ---------- */
60
+ // Primary tabs live in the thumb zone; the rest fold into More.
61
+ const TABS = [
62
+ { id: "dashboard", label: "Lake", vi: "Mặt hồ", icon: "dashboard" },
63
+ { id: "missions", label: "Missions", vi: "Nhiệm vụ", icon: "missions" },
64
+ { id: "chat", label: "Chat", vi: "Trò chuyện", icon: "chat" },
65
+ { id: "agents", label: "Agents", vi: "Tác nhân", icon: "agents" },
66
+ ];
67
+ const MORE_ITEMS = [
68
+ { id: "memory", label: "Memory Garden", vi: "Vườn ký ức", icon: "memory" },
69
+ { id: "skills", label: "Skills", vi: "Kỹ năng", icon: "skills" },
70
+ { id: "providers", label: "Providers", vi: "Nhà cung cấp", icon: "providers" },
71
+ { id: "settings", label: "Settings", vi: "Cài đặt", icon: "settings" },
72
+ ];
73
+ const ALL_PAGES = [...TABS, ...MORE_ITEMS];
74
+ const PAGE_TITLE = (id) => {
75
+ const p = ALL_PAGES.find((x) => x.id === id);
76
+ return p ? L(p.label, p.vi) : "Yana";
77
+ };
78
+
79
+ /* ---------- Top bar ---------- */
80
+ function TopBar({ page, lang, onLang, onMore }) {
81
+ const onLake = page === "dashboard";
82
+ return (
83
+ <header className="mtopbar">
84
+ <div className="mtopbar-l">
85
+ {onLake
86
+ ? <YanaMark size={30} />
87
+ : <span style={{ color: "var(--primary)", display: "inline-flex" }}>{Icons[ALL_PAGES.find((x) => x.id === page)?.icon || "dashboard"](20)}</span>}
88
+ <div style={{ minWidth: 0 }}>
89
+ <div className="mtopbar-title">{onLake ? "Yana" : PAGE_TITLE(page)}</div>
90
+ {onLake && <div style={{ fontSize: 10.5, color: "var(--ink-3)", letterSpacing: ".06em" }}>YAMTAM ENGINE</div>}
91
+ </div>
92
+ </div>
93
+ <div className="mtopbar-r">
94
+ <button className="lang-pill" onClick={onLang}>{lang === "vi" ? "VI" : "EN"}</button>
95
+ <button className="icon-btn" aria-label="Notifications">{Icons.bell(18)}</button>
96
+ <button className="avatar-btn" onClick={onMore} aria-label="Menu">
97
+ {(window.YANA.username || "Y")[0].toUpperCase()}
98
+ </button>
99
+ </div>
100
+ </header>
101
+ );
102
+ }
103
+
104
+ /* ---------- Bottom tab bar ---------- */
105
+ function TabBar({ page, onNav, onMore }) {
106
+ const moreActive = MORE_ITEMS.some((m) => m.id === page);
107
+ return (
108
+ <nav className="mtabbar">
109
+ {TABS.map((tb) => (
110
+ <button key={tb.id} className="mtab" data-on={page === tb.id ? "1" : "0"} onClick={() => onNav(tb.id)}>
111
+ <span className="tab-ic">{Icons[tb.icon](22)}</span>
112
+ <span>{L(tb.label, tb.vi)}</span>
113
+ </button>
114
+ ))}
115
+ <button className="mtab" data-on={moreActive ? "1" : "0"} onClick={onMore}>
116
+ <span className="tab-ic">{Icons.more(22)}</span>
117
+ <span>{L("More", "Thêm")}</span>
118
+ </button>
119
+ </nav>
120
+ );
121
+ }
122
+
123
+ /* ---------- Generic slide-up sheet ---------- */
124
+ function Sheet({ open, title, onClose, children }) {
125
+ const [mounted, setMounted] = React.useState(open);
126
+ React.useEffect(() => {
127
+ if (open) setMounted(true);
128
+ else { const t = setTimeout(() => setMounted(false), 320); return () => clearTimeout(t); }
129
+ }, [open]);
130
+ if (!mounted) return null;
131
+ return (
132
+ <>
133
+ <div className="sheet-backdrop" data-show={open ? "1" : "0"} onClick={onClose}></div>
134
+ <div className="sheet" data-show={open ? "1" : "0"} role="dialog">
135
+ <div className="sheet-grab" onClick={onClose}></div>
136
+ {title && <div className="sheet-title">{title}</div>}
137
+ <div className="sheet-body">{children}</div>
138
+ </div>
139
+ </>
140
+ );
141
+ }
142
+
143
+ /* ---------- More sheet (secondary nav) ---------- */
144
+ function MoreSheet({ open, page, onNav, onClose }) {
145
+ const D = window.YANA;
146
+ const username = D.username || L("My account", "Tài khoản");
147
+ function logout() {
148
+ fetch("/api/auth/logout", { method: "POST" }).finally(() => location.replace("/"));
149
+ }
150
+ return (
151
+ <Sheet open={open} title={L("All sections", "Tất cả mục")} onClose={onClose}>
152
+ {/* User identity card */}
153
+ <div className="glass" style={{ borderRadius: "var(--r-md)", padding: "11px 13px", display: "flex", alignItems: "center", gap: 11, marginBottom: 8 }}>
154
+ <div style={{ width: 36, height: 36, borderRadius: 11, background: "var(--primary)", color: "#fff", display: "grid", placeItems: "center", fontSize: 15, fontWeight: 600, flex: "none" }}>
155
+ {username[0].toUpperCase()}
156
+ </div>
157
+ <div style={{ flex: 1, minWidth: 0 }}>
158
+ <div style={{ fontSize: 14, fontWeight: 500 }}>{username}</div>
159
+ <div style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{L("Sovereign account", "Tài khoản chủ sở hữu")}</div>
160
+ </div>
161
+ <button onClick={logout} style={{ fontSize: 12, color: "var(--ink-3)", background: "none", border: "none", cursor: "pointer", padding: "6px 8px", borderRadius: 8, flex: "none" }}>
162
+ {L("Sign out", "Đăng xuất")}
163
+ </button>
164
+ </div>
165
+ <div className="more-grid">
166
+ {MORE_ITEMS.map((m) => (
167
+ <button key={m.id} className="more-item" onClick={() => { onNav(m.id); onClose(); }}
168
+ style={page === m.id ? { background: "var(--primary-soft)" } : null}>
169
+ <span className="more-ic">{Icons[m.icon](19)}</span>
170
+ <span className="more-lbl">{L(m.label, m.vi)}</span>
171
+ <span className="more-chev">{Icons.chevron(16)}</span>
172
+ </button>
173
+ ))}
174
+ </div>
175
+ <div style={{
176
+ marginTop: 6, borderRadius: "var(--r-md)", padding: "13px 14px",
177
+ background: "var(--primary-soft)", display: "flex", flexDirection: "column", gap: 7,
178
+ }}>
179
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
180
+ <span style={{ color: "var(--primary)" }}>{Icons.safety(16)}</span>
181
+ <span style={{ fontSize: 13, fontWeight: 500, color: "var(--primary)" }}>YAMTAM Core</span>
182
+ </div>
183
+ <div style={{ fontSize: 12.5, color: "var(--ink-2)" }}>{D.stats.agents} {L("agents supervised", "tác nhân được giám sát")}</div>
184
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
185
+ <span className="dot on"></span>
186
+ <span style={{ fontSize: 12, color: "var(--ink-3)" }}>{L("All gates active", "Mọi cổng an toàn đang bật")}</span>
187
+ </div>
188
+ </div>
189
+ </Sheet>
190
+ );
191
+ }
192
+
193
+ /* ---------- Large-title header for a page ---------- */
194
+ function MHead({ title, sub, children }) {
195
+ return (
196
+ <div className="mhead">
197
+ <div className="mhead-row">
198
+ <h1 className="h-display mhead-title">{title}</h1>
199
+ {children}
200
+ </div>
201
+ {sub && <p className="mhead-sub">{sub}</p>}
202
+ </div>
203
+ );
204
+ }
205
+
206
+ /* ---------- Card (mobile) ---------- */
207
+ function MCard({ title, aside, children, style, pad = true, onClick }) {
208
+ return (
209
+ <section className="glass" onClick={onClick} style={{
210
+ borderRadius: "var(--r-lg)", padding: pad ? "var(--pad-card)" : 0,
211
+ ...(onClick ? { cursor: "pointer" } : null), ...style,
212
+ }}>
213
+ {(title || aside) && (
214
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 12, padding: pad ? 0 : "var(--pad-card) var(--pad-card) 0" }}>
215
+ {title && <h2 className="label-xs" style={{ margin: 0 }}>{title}</h2>}
216
+ {aside}
217
+ </div>
218
+ )}
219
+ {children}
220
+ </section>
221
+ );
222
+ }
223
+
224
+ /* A "see all" link used in card headers */
225
+ function SeeAll({ label, onClick }) {
226
+ return (
227
+ <button onClick={onClick} style={{
228
+ background: "none", border: "none", cursor: "pointer", color: "var(--primary)",
229
+ fontSize: 12.5, fontWeight: 500, display: "flex", alignItems: "center", gap: 1, padding: 0,
230
+ }}>{label} {Icons.chevron(13)}</button>
231
+ );
232
+ }
233
+
234
+ Object.assign(window, {
235
+ Icons, YanaMark, TopBar, TabBar, Sheet, MoreSheet, MHead, MCard, SeeAll,
236
+ TABS, MORE_ITEMS, ALL_PAGES, PAGE_TITLE,
237
+ });