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.
- package/PUBLISH.md +26 -0
- package/README.md +46 -0
- package/auth.js +204 -0
- package/bin/yana.js +20 -0
- package/desktop/app.jsx +240 -0
- package/desktop/chat.jsx +524 -0
- package/desktop/components.jsx +203 -0
- package/desktop/dashboard.jsx +304 -0
- package/desktop/index.html +29 -0
- package/desktop/login.html +474 -0
- package/desktop/spaces.jsx +248 -0
- package/desktop/system.jsx +540 -0
- package/desktop/themes.css +275 -0
- package/desktop/welcome.html +251 -0
- package/logo.png +0 -0
- package/memory.js +112 -0
- package/missions.js +140 -0
- package/mobile/index.html +34 -0
- package/mobile/m-app.jsx +152 -0
- package/mobile/m-chat.jsx +298 -0
- package/mobile/m-garden.jsx +72 -0
- package/mobile/m-lake.jsx +190 -0
- package/mobile/m-missions.jsx +81 -0
- package/mobile/m-shell.jsx +237 -0
- package/mobile/m-system.jsx +355 -0
- package/mobile/mobile.css +180 -0
- package/mobile/themes.css +228 -0
- package/package.json +29 -0
- package/server.js +977 -0
- package/shared/crypto-store.js +160 -0
- package/shared/data.js +154 -0
- package/shared/tweaks-panel.jsx +558 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Yana AI — shared components: icons, wordmark, sidebar, 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={<><rect x="3" y="3" width="6" height="6" rx="1.6"/><rect x="11" y="3" width="6" height="6" rx="1.6"/><rect x="3" y="11" width="6" height="6" rx="1.6"/><rect x="11" y="11" width="6" height="6" rx="1.6"/></>} />,
|
|
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
|
+
menu: (s) => <Ic size={s} d={<path d="M3.5 6h13M3.5 10h13M3.5 14h13"/>} />,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/* ---------- Wordmark: lotus in bloom on the water (matches login.html) ---------- */
|
|
39
|
+
function YanaMark({ size = 30 }) {
|
|
40
|
+
return (
|
|
41
|
+
<div aria-label="Yana" style={{
|
|
42
|
+
width: size, height: size, borderRadius: size * 0.32, flex: "none",
|
|
43
|
+
background: "linear-gradient(150deg, color-mix(in oklab, var(--primary) 92%, white), color-mix(in oklab, var(--primary) 72%, #1d3530))",
|
|
44
|
+
boxShadow: "inset 0 1px 0 rgba(255,255,255,.4), 0 4px 12px color-mix(in oklab, var(--primary) 28%, transparent)",
|
|
45
|
+
display: "grid", placeItems: "center",
|
|
46
|
+
}}>
|
|
47
|
+
<img src="/logo.png" alt="" width={Math.round(size * 0.74)} height={Math.round(size * 0.74)}
|
|
48
|
+
style={{ display: "block" }} />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Wordmark({ compact }) {
|
|
54
|
+
return (
|
|
55
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "4px 2px" }}>
|
|
56
|
+
<YanaMark />
|
|
57
|
+
{!compact && (
|
|
58
|
+
<div style={{ lineHeight: 1.15 }}>
|
|
59
|
+
<div style={{ fontSize: 17, fontWeight: 500, letterSpacing: "-0.01em" }}>Yana</div>
|
|
60
|
+
<div style={{ fontSize: 10.5, color: "var(--ink-3)", letterSpacing: "0.06em" }}>YAMTAM ENGINE</div>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ---------- Sign out ---------- */
|
|
68
|
+
async function signOut() {
|
|
69
|
+
try { await fetch("/api/auth/logout", { method: "POST" }); } catch (_) {}
|
|
70
|
+
location.replace("/login.html");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ---------- Sidebar ---------- */
|
|
74
|
+
const NAV = [
|
|
75
|
+
{ id: "dashboard", label: "Lake", vi: "Mặt hồ", icon: "dashboard" },
|
|
76
|
+
{ id: "missions", label: "Missions", vi: "Nhiệm vụ", icon: "missions" },
|
|
77
|
+
{ id: "chat", label: "Conversation", vi: "Trò chuyện", icon: "chat" },
|
|
78
|
+
{ id: "agents", label: "Agents", vi: "Tác nhân", icon: "agents" },
|
|
79
|
+
{ id: "memory", label: "Memory Garden", vi: "Vườn ký ức", icon: "memory" },
|
|
80
|
+
{ id: "skills", label: "Skills", vi: "Kỹ năng", icon: "skills" },
|
|
81
|
+
{ id: "providers", label: "Providers", vi: "Nhà cung cấp", icon: "providers" },
|
|
82
|
+
{ id: "settings", label: "Settings", vi: "Cài đặt", icon: "settings" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
function Sidebar({ page, onNav }) {
|
|
86
|
+
const D = window.YANA;
|
|
87
|
+
const [account, setAccount] = useState(null);
|
|
88
|
+
const [open, setOpen] = useState(false);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
fetch("/api/auth/status")
|
|
91
|
+
.then((r) => r.json())
|
|
92
|
+
.then((d) => setAccount(d.username || null))
|
|
93
|
+
.catch(() => {});
|
|
94
|
+
}, []);
|
|
95
|
+
const nav = (id) => { onNav(id); setOpen(false); };
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
<button className="glass-strong yana-menu-btn" aria-label={L("Open menu", "Mở menu")}
|
|
99
|
+
aria-expanded={open} onClick={() => setOpen(true)}>
|
|
100
|
+
{Icons.menu(18)}
|
|
101
|
+
</button>
|
|
102
|
+
<div className={"yana-backdrop" + (open ? " show" : "")} onClick={() => setOpen(false)} aria-hidden="true"></div>
|
|
103
|
+
<nav className={"glass yana-sidebar" + (open ? " open" : "")} style={{
|
|
104
|
+
borderRadius: "var(--r-lg)",
|
|
105
|
+
display: "flex", flexDirection: "column",
|
|
106
|
+
padding: "calc(14px * var(--sp))", gap: 4,
|
|
107
|
+
}}>
|
|
108
|
+
<div style={{ marginBottom: "calc(14px * var(--sp))" }}><Wordmark /></div>
|
|
109
|
+
|
|
110
|
+
{NAV.map((n) => {
|
|
111
|
+
const active = page === n.id;
|
|
112
|
+
return (
|
|
113
|
+
<button key={n.id} onClick={() => nav(n.id)} style={{
|
|
114
|
+
display: "flex", alignItems: "center", gap: 11,
|
|
115
|
+
padding: "calc(8px * var(--sp)) 11px", borderRadius: "var(--r-sm)",
|
|
116
|
+
border: "none", cursor: "pointer", width: "100%", textAlign: "left",
|
|
117
|
+
fontSize: 13.5, fontWeight: active ? 500 : 400,
|
|
118
|
+
color: active ? "var(--primary)" : "var(--ink-2)",
|
|
119
|
+
background: active ? "var(--primary-soft)" : "transparent",
|
|
120
|
+
transition: "background .15s, color .15s",
|
|
121
|
+
}}
|
|
122
|
+
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "rgba(var(--surface-rgb), .5)"; }}
|
|
123
|
+
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }}>
|
|
124
|
+
{Icons[n.icon](17)}
|
|
125
|
+
<span>{L(n.label, n.vi)}</span>
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
|
|
130
|
+
<div style={{ flex: 1 }}></div>
|
|
131
|
+
|
|
132
|
+
<div style={{
|
|
133
|
+
borderRadius: "var(--r-md)", padding: "11px 12px",
|
|
134
|
+
background: "var(--primary-soft)",
|
|
135
|
+
display: "flex", flexDirection: "column", gap: 7,
|
|
136
|
+
}}>
|
|
137
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
138
|
+
<span style={{ color: "var(--primary)" }}>{Icons.safety(16)}</span>
|
|
139
|
+
<span style={{ fontSize: 12.5, fontWeight: 500, color: "var(--primary)" }}>YAMTAM Core</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div style={{ fontSize: 12, color: "var(--ink-2)" }}>{D.stats.agents} {L("agents supervised", "tác nhân được giám sát")}</div>
|
|
142
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
143
|
+
<span className="dot on"></span>
|
|
144
|
+
<span style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{L("All gates active", "Mọi cổng an toàn đang bật")}</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 4px 2px" }}>
|
|
149
|
+
<div style={{
|
|
150
|
+
width: 28, height: 28, borderRadius: "50%", flex: "none",
|
|
151
|
+
background: "linear-gradient(145deg, var(--gold), color-mix(in oklab, var(--gold) 55%, white))",
|
|
152
|
+
color: "white", display: "grid", placeItems: "center", fontSize: 12, fontWeight: 600,
|
|
153
|
+
}}>{(account || "Y").trim().charAt(0).toUpperCase()}</div>
|
|
154
|
+
<div style={{ lineHeight: 1.2, flex: 1, minWidth: 0 }}>
|
|
155
|
+
<div title={account || ""} style={{
|
|
156
|
+
fontSize: 12.5, fontWeight: 500,
|
|
157
|
+
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
|
158
|
+
}}>{account || "Yana"}</div>
|
|
159
|
+
<div style={{ fontSize: 11, color: "var(--ink-3)" }}>{L("Account", "Tài khoản")} · YAMTAM</div>
|
|
160
|
+
</div>
|
|
161
|
+
<button onClick={signOut} title={L("Sign out", "Đăng xuất")} aria-label={L("Sign out", "Đăng xuất")} style={{
|
|
162
|
+
background: "none", border: "none", cursor: "pointer", padding: 4,
|
|
163
|
+
color: "var(--ink-3)", display: "inline-flex", borderRadius: 8,
|
|
164
|
+
}}>
|
|
165
|
+
<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor"
|
|
166
|
+
strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
167
|
+
<path d="M12.5 6.5V4.5a1.5 1.5 0 0 0-1.5-1.5H5A1.5 1.5 0 0 0 3.5 4.5v11A1.5 1.5 0 0 0 5 17h6a1.5 1.5 0 0 0 1.5-1.5v-2M8.5 10H17m0 0-2.5-2.5M17 10l-2.5 2.5" />
|
|
168
|
+
</svg>
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</nav>
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ---------- Page scaffolding ---------- */
|
|
177
|
+
function PageHeader({ title, sub, children }) {
|
|
178
|
+
return (
|
|
179
|
+
<header style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 16, marginBottom: "var(--gap)" }}>
|
|
180
|
+
<div>
|
|
181
|
+
<h1 className="h-display" style={{ margin: 0, fontSize: 26 }}>{title}</h1>
|
|
182
|
+
{sub && <p style={{ margin: "3px 0 0", color: "var(--ink-2)", fontSize: 13.5 }}>{sub}</p>}
|
|
183
|
+
</div>
|
|
184
|
+
{children}
|
|
185
|
+
</header>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function Card({ title, aside, children, style, className }) {
|
|
190
|
+
return (
|
|
191
|
+
<section className={"glass " + (className || "")} style={{ borderRadius: "var(--r-lg)", padding: "var(--pad-card)", ...style }}>
|
|
192
|
+
{(title || aside) && (
|
|
193
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
|
|
194
|
+
{title && <h2 className="label-xs" style={{ margin: 0 }}>{title}</h2>}
|
|
195
|
+
{aside}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
{children}
|
|
199
|
+
</section>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Object.assign(window, { Icons, YanaMark, Wordmark, Sidebar, PageHeader, Card, NAV });
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Yana AI — Dashboard (AI Control Center)
|
|
2
|
+
// All numbers are real: /api/status (MANIFEST), /api/dashboard (L1 memory +
|
|
3
|
+
// audit log + uptime), /api/usage (per-provider stats), YanaVault (keys).
|
|
4
|
+
function StatTile({ label, value, sub, accent }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "var(--pad-card)", display: "flex", flexDirection: "column", gap: 4 }}>
|
|
7
|
+
<span className="label-xs">{label}</span>
|
|
8
|
+
<span className="num-lg">{value}</span>
|
|
9
|
+
<span style={{ fontSize: 12.5, color: accent ? "var(--primary)" : "var(--ink-3)" }}>{sub}</span>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ModelRow({ m }) {
|
|
15
|
+
return (
|
|
16
|
+
<div style={{ display: "grid", gridTemplateColumns: "16px 1fr 110px 56px", alignItems: "center", gap: 12, padding: "calc(8px * var(--sp)) 0" }}>
|
|
17
|
+
<span className={"dot " + (m.status === "active" ? "on" : "idle")}></span>
|
|
18
|
+
<div style={{ lineHeight: 1.3, minWidth: 0 }}>
|
|
19
|
+
<div style={{ fontSize: 13.5, fontWeight: 500 }}>{m.name} <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>{m.model}</span></div>
|
|
20
|
+
<div style={{ fontSize: 12, color: "var(--ink-3)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.role}</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="bar"><i style={{ width: m.load + "%" }}></i></div>
|
|
23
|
+
<span style={{ fontSize: 12, color: "var(--ink-2)", textAlign: "right" }}>{m.latency}</span>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function MissionRowMini({ m, onOpen }) {
|
|
29
|
+
return (
|
|
30
|
+
<button onClick={onOpen} style={{
|
|
31
|
+
display: "grid", gridTemplateColumns: "1fr 90px 48px", alignItems: "center", gap: 12,
|
|
32
|
+
padding: "calc(8px * var(--sp)) 0", width: "100%", textAlign: "left",
|
|
33
|
+
background: "none", border: "none", cursor: "pointer", color: "inherit",
|
|
34
|
+
}}>
|
|
35
|
+
<div style={{ lineHeight: 1.3, minWidth: 0 }}>
|
|
36
|
+
<div style={{ fontSize: 13.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.name}</div>
|
|
37
|
+
<div style={{ fontSize: 12, color: "var(--ink-3)" }}>{m.owner} · {m.status}</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="bar"><i style={{ width: m.progress + "%" }}></i></div>
|
|
40
|
+
<span style={{ fontSize: 12, color: "var(--ink-2)", textAlign: "right" }}>{m.progress}%</span>
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function EmptyHint({ text }) {
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "calc(8px * var(--sp)) 0" }}>
|
|
48
|
+
<span className="dot idle" style={{ flex: "none" }}></span>
|
|
49
|
+
<span style={{ fontSize: 13, color: "var(--ink-3)", lineHeight: 1.45 }}>{text}</span>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fmtAgo(iso) {
|
|
55
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
56
|
+
if (!isFinite(ms) || ms < 0) return "—";
|
|
57
|
+
const mins = Math.floor(ms / 60000);
|
|
58
|
+
if (mins < 60) return mins + L(" min ago", " phút trước");
|
|
59
|
+
const hours = Math.floor(mins / 60);
|
|
60
|
+
if (hours < 24) return hours + L(" h ago", " giờ trước");
|
|
61
|
+
return Math.floor(hours / 24) + L(" days ago", " ngày trước");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fmtUptime(s) {
|
|
65
|
+
if (s < 3600) return Math.floor(s / 60) + L(" min", " phút");
|
|
66
|
+
if (s < 86400) return (s / 3600).toFixed(1) + L(" h", " giờ");
|
|
67
|
+
return (s / 86400).toFixed(1) + L(" days", " ngày");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ---------- Local time + weather (browser timezone = wherever you are) ----- */
|
|
71
|
+
function greetingFor(hour, name) {
|
|
72
|
+
const who = name ? ", " + name : "";
|
|
73
|
+
if (hour >= 5 && hour < 12) return L("Good morning" + who, "Chào buổi sáng" + who);
|
|
74
|
+
if (hour >= 12 && hour < 18) return L("Good afternoon" + who, "Chào buổi chiều" + who);
|
|
75
|
+
if (hour >= 18 && hour < 22) return L("Good evening" + who, "Chào buổi tối" + who);
|
|
76
|
+
return L("Up late" + who, "Khuya rồi" + who);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// WMO weather codes → emoji + label (open-meteo current.weather_code)
|
|
80
|
+
function describeWeather(code) {
|
|
81
|
+
if (code === 0) return ["☀️", L("Clear", "Quang đãng")];
|
|
82
|
+
if (code <= 2) return ["⛅", L("Partly cloudy", "Ít mây")];
|
|
83
|
+
if (code === 3) return ["☁️", L("Overcast", "Nhiều mây")];
|
|
84
|
+
if (code === 45 || code === 48) return ["🌫️", L("Fog", "Sương mù")];
|
|
85
|
+
if (code <= 57) return ["🌦️", L("Drizzle", "Mưa phùn")];
|
|
86
|
+
if (code <= 67) return ["🌧️", L("Rain", "Mưa")];
|
|
87
|
+
if (code <= 77) return ["🌨️", L("Snow", "Tuyết")];
|
|
88
|
+
if (code <= 82) return ["🌧️", L("Showers", "Mưa rào")];
|
|
89
|
+
if (code <= 86) return ["🌨️", L("Snow showers", "Mưa tuyết")];
|
|
90
|
+
return ["⛈️", L("Thunderstorm", "Dông bão")];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function useLocalWeather() {
|
|
94
|
+
const [weather, setWeather] = React.useState(null);
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
if (!navigator.geolocation) return;
|
|
97
|
+
navigator.geolocation.getCurrentPosition(
|
|
98
|
+
(pos) => {
|
|
99
|
+
const { latitude, longitude } = pos.coords;
|
|
100
|
+
fetch("https://api.open-meteo.com/v1/forecast?latitude=" + latitude.toFixed(3) +
|
|
101
|
+
"&longitude=" + longitude.toFixed(3) + "¤t=temperature_2m,weather_code&timezone=auto")
|
|
102
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
103
|
+
.then((d) => { if (d && d.current) setWeather(d.current); })
|
|
104
|
+
.catch(() => {});
|
|
105
|
+
},
|
|
106
|
+
() => {}, // permission denied → no widget, no nagging
|
|
107
|
+
{ timeout: 10000, maximumAge: 30 * 60 * 1000 }
|
|
108
|
+
);
|
|
109
|
+
}, []);
|
|
110
|
+
return weather;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---------- The heart of Yana: the mission composer ---------- */
|
|
114
|
+
function MissionComposer({ onNav, missionCount }) {
|
|
115
|
+
const D = window.YANA;
|
|
116
|
+
const [v, setV] = React.useState("");
|
|
117
|
+
const [account, setAccount] = React.useState(null);
|
|
118
|
+
const [now, setNow] = React.useState(() => new Date());
|
|
119
|
+
const weather = useLocalWeather();
|
|
120
|
+
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
fetch("/api/auth/status")
|
|
123
|
+
.then((r) => r.json())
|
|
124
|
+
.then((d) => setAccount(d.username || null))
|
|
125
|
+
.catch(() => {});
|
|
126
|
+
const id = setInterval(() => setNow(new Date()), 30000);
|
|
127
|
+
return () => clearInterval(id);
|
|
128
|
+
}, []);
|
|
129
|
+
const suggestions = [
|
|
130
|
+
["Ship v0.9 safely", "Phát hành v0.9 an toàn"],
|
|
131
|
+
["Summarize what changed overnight", "Tóm tắt thay đổi qua đêm"],
|
|
132
|
+
["Prune stale memories", "Dọn ký ức cũ"],
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
async function begin(text) {
|
|
136
|
+
const goal = (text || v).trim();
|
|
137
|
+
if (!goal) return;
|
|
138
|
+
try {
|
|
139
|
+
const r = await fetch("/api/missions", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify({ name: goal }),
|
|
143
|
+
});
|
|
144
|
+
if (r.ok) {
|
|
145
|
+
const { mission } = await r.json();
|
|
146
|
+
D._openMission = mission.id;
|
|
147
|
+
}
|
|
148
|
+
} catch (_) {}
|
|
149
|
+
onNav("missions");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const connectedN = D.providers.filter((p) => YanaVault.hasKey(p.id)).length;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div style={{ maxWidth: 660, margin: "0 auto", padding: "calc(34px * var(--sp)) 0 calc(40px * var(--sp))", textAlign: "center" }}>
|
|
156
|
+
<h1 className="h-display" style={{ margin: "0 0 18px", fontSize: 30 }}>{greetingFor(now.getHours(), account)}</h1>
|
|
157
|
+
<div className="glass-strong" style={{ borderRadius: 18, padding: "10px 10px 10px 20px", display: "flex", alignItems: "center", gap: 12, textAlign: "left" }}>
|
|
158
|
+
<input
|
|
159
|
+
value={v}
|
|
160
|
+
onChange={(e) => setV(e.target.value)}
|
|
161
|
+
onKeyDown={(e) => { if (e.key === "Enter") begin(); }}
|
|
162
|
+
placeholder={L("What do you want to accomplish today?", "Hôm nay bạn muốn hoàn thành điều gì?")}
|
|
163
|
+
style={{ flex: 1, border: "none", outline: "none", background: "transparent", fontSize: 15.5, fontFamily: "inherit", color: "var(--ink)" }}
|
|
164
|
+
/>
|
|
165
|
+
<button onClick={() => begin()} style={{
|
|
166
|
+
display: "flex", alignItems: "center", gap: 7, padding: "9px 17px", borderRadius: 13,
|
|
167
|
+
border: "none", cursor: "pointer", background: "var(--primary)", color: "white",
|
|
168
|
+
fontSize: 13.5, fontWeight: 500, flex: "none",
|
|
169
|
+
boxShadow: "0 4px 14px color-mix(in oklab, var(--primary) 32%, transparent)",
|
|
170
|
+
}}>{Icons.spark(15)} {L("New Mission", "Nhiệm vụ mới")}</button>
|
|
171
|
+
</div>
|
|
172
|
+
<div style={{ display: "flex", gap: 7, justifyContent: "center", flexWrap: "wrap", marginTop: 13 }}>
|
|
173
|
+
{suggestions.map(([en, vi]) => (
|
|
174
|
+
<button key={en} onClick={() => begin(en)} className="chip neutral" style={{ cursor: "pointer", fontSize: 12 }}>{L(en, vi)}</button>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 9, marginTop: 22, fontSize: 12.5, color: "var(--ink-3)", flexWrap: "wrap" }}>
|
|
178
|
+
<span className="dot on pulse"></span>
|
|
179
|
+
<span>{L("Lake status:", "Trạng thái hồ:")} <b style={{ fontWeight: 500, color: "var(--ink-2)" }}>{L("Calm", "Tĩnh lặng")}</b></span>
|
|
180
|
+
<span style={{ opacity: .5 }}>·</span>
|
|
181
|
+
<span>{now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</span>
|
|
182
|
+
{weather && <span style={{ opacity: .5 }}>·</span>}
|
|
183
|
+
{weather && (
|
|
184
|
+
<span title={describeWeather(weather.weather_code)[1]}>
|
|
185
|
+
{describeWeather(weather.weather_code)[0]} {Math.round(weather.temperature_2m)}°C
|
|
186
|
+
</span>
|
|
187
|
+
)}
|
|
188
|
+
<span style={{ opacity: .5 }}>·</span>
|
|
189
|
+
<span>{connectedN} {L("providers connected", "nhà cung cấp đã kết nối")}</span>
|
|
190
|
+
<span style={{ opacity: .5 }}>·</span>
|
|
191
|
+
<span>{missionCount} {L("missions running", "nhiệm vụ đang chạy")}</span>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function Dashboard({ t, onNav }) {
|
|
198
|
+
const D = window.YANA;
|
|
199
|
+
const [dash, setDash] = React.useState(null);
|
|
200
|
+
const [usage, setUsage] = React.useState(null);
|
|
201
|
+
const [missions, setMissions] = React.useState([]);
|
|
202
|
+
|
|
203
|
+
React.useEffect(() => {
|
|
204
|
+
fetch("/api/dashboard").then((r) => (r.ok ? r.json() : null)).then((d) => { if (d) setDash(d); }).catch(() => {});
|
|
205
|
+
fetch("/api/usage").then((r) => (r.ok ? r.json() : null)).then((d) => { if (d) setUsage(d.usage); }).catch(() => {});
|
|
206
|
+
fetch("/api/missions").then((r) => (r.ok ? r.json() : null)).then((d) => { if (d) setMissions(d.missions); }).catch(() => {});
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
const connected = D.providers.filter((p) => YanaVault.hasKey(p.id));
|
|
210
|
+
const totalTok = connected.reduce((s, p) => s + ((usage && usage[p.id] && usage[p.id].est_tokens) || 0), 0);
|
|
211
|
+
const liveModels = connected.map((p) => {
|
|
212
|
+
const u = usage && usage[p.id];
|
|
213
|
+
return {
|
|
214
|
+
id: p.id, name: p.name, model: p.models[0], role: p.role,
|
|
215
|
+
status: u && u.requests > 0 ? "active" : "idle",
|
|
216
|
+
load: totalTok > 0 && u ? Math.round((u.est_tokens / totalTok) * 100) : 0,
|
|
217
|
+
latency: u && u.avg_latency_ms ? (u.avg_latency_ms / 1000).toFixed(1) + "s" : "—",
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
const mem = dash && dash.memories;
|
|
221
|
+
const safety = dash && dash.safety;
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div data-screen-label="Lake">
|
|
225
|
+
<MissionComposer onNav={onNav} missionCount={missions.filter((m) => m.status !== "done").length} />
|
|
226
|
+
|
|
227
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "var(--gap)", marginBottom: "var(--gap)" }}>
|
|
228
|
+
<StatTile label={L("Agents", "Tác nhân")} value={D.stats.agents || "—"} sub={L("in catalog", "trong danh mục")} accent />
|
|
229
|
+
<StatTile label={L("Skills", "Kỹ năng")} value={(D.stats.skills || 0).toLocaleString()} sub={L("indexed & callable", "đã lập chỉ mục")} />
|
|
230
|
+
<StatTile label={L("Missions", "Nhiệm vụ")} value={missions.filter((m) => m.status !== "done").length} sub={L("in motion", "đang diễn ra")} />
|
|
231
|
+
<StatTile label={L("Memories", "Ký ức")} value={mem ? mem.total : "—"} sub={mem ? "+" + mem.today + L(" today", " hôm nay") : L("L1 atomic facts", "L1 atomic facts")} />
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div style={{ display: "grid", gridTemplateColumns: "1.25fr 1fr", gap: "var(--gap)" }}>
|
|
235
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
|
|
236
|
+
<Card title={L("Active AI Models", "Mô hình AI đang hoạt động")} aside={<span className="chip neutral">{connected.length} {L("providers", "nhà cung cấp")}</span>}>
|
|
237
|
+
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
238
|
+
{liveModels.length
|
|
239
|
+
? liveModels.map((m) => <ModelRow key={m.id} m={m} />)
|
|
240
|
+
: <EmptyHint text={L("No providers connected — add an API key in Providers.", "Chưa kết nối nhà cung cấp — thêm API key ở mục Nhà cung cấp.")} />}
|
|
241
|
+
</div>
|
|
242
|
+
</Card>
|
|
243
|
+
|
|
244
|
+
{t.showMissions && (
|
|
245
|
+
<Card title={L("Missions", "Nhiệm vụ")} aside={
|
|
246
|
+
<button onClick={() => onNav("missions")} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--primary)", fontSize: 12.5, fontWeight: 500, display: "flex", alignItems: "center", gap: 2 }}>
|
|
247
|
+
{L("Mission Center", "Trung tâm nhiệm vụ")} {Icons.chevron(13)}
|
|
248
|
+
</button>
|
|
249
|
+
}>
|
|
250
|
+
{missions.length
|
|
251
|
+
? missions.slice(0, 4).map((m) => <MissionRowMini key={m.id} m={m} onOpen={() => { window.YANA._openMission = m.id; onNav("missions"); }} />)
|
|
252
|
+
: <EmptyHint text={L("No missions yet — start one above.", "Chưa có nhiệm vụ — bắt đầu một nhiệm vụ ở trên.")} />}
|
|
253
|
+
</Card>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
|
|
258
|
+
{t.showAgents && (
|
|
259
|
+
<Card title={L("Running Agents", "Tác nhân đang chạy")} aside={
|
|
260
|
+
<button onClick={() => onNav("agents")} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--primary)", fontSize: 12.5, fontWeight: 500, display: "flex", alignItems: "center", gap: 2 }}>
|
|
261
|
+
{L("Agent Space", "Không gian tác nhân")} {Icons.chevron(13)}
|
|
262
|
+
</button>
|
|
263
|
+
}>
|
|
264
|
+
<EmptyHint text={L("No agents running — agents start when a mission dispatches.", "Chưa có tác nhân nào chạy — tác nhân khởi động khi nhiệm vụ được giao.")} />
|
|
265
|
+
</Card>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{t.showMemory && (
|
|
269
|
+
<Card title={L("Memory Garden", "Vườn ký ức")} aside={<span className="chip pink">{Icons.memory(13)} {mem ? "+" + mem.today : "—"} {L("today", "hôm nay")}</span>}>
|
|
270
|
+
{mem && mem.recent.length
|
|
271
|
+
? mem.recent.map((m, i) => (
|
|
272
|
+
<div key={i} style={{ padding: "calc(7px * var(--sp)) 0", display: "flex", gap: 10, alignItems: "baseline" }}>
|
|
273
|
+
<span className="chip neutral" style={{ flex: "none", fontSize: 11 }}>{m.kind}</span>
|
|
274
|
+
<span style={{ fontSize: 13, color: "var(--ink-2)", lineHeight: 1.45 }}>{m.text}</span>
|
|
275
|
+
</div>
|
|
276
|
+
))
|
|
277
|
+
: <EmptyHint text={L("No memories yet.", "Chưa có ký ức nào.")} />}
|
|
278
|
+
</Card>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{t.showSystem && (
|
|
282
|
+
<Card title={L("System Health", "Sức khỏe hệ thống")}>
|
|
283
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
|
|
284
|
+
{[
|
|
285
|
+
[L("Audit events", "Sự kiện audit"), safety ? safety.events_today + L(" today", " hôm nay") : "—"],
|
|
286
|
+
[L("Blocked actions", "Hành động bị chặn"), safety ? String(safety.blocked_today) : "—"],
|
|
287
|
+
[L("Last incident", "Sự cố gần nhất"), safety ? (safety.last_incident ? fmtAgo(safety.last_incident) : L("None recorded", "Chưa ghi nhận")) : "—"],
|
|
288
|
+
[L("Server uptime", "Uptime máy chủ"), dash ? fmtUptime(dash.uptime_s) : "—"],
|
|
289
|
+
].map(([k, v]) => (
|
|
290
|
+
<div key={k} style={{ lineHeight: 1.35 }}>
|
|
291
|
+
<div style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{k}</div>
|
|
292
|
+
<div style={{ fontSize: 13, fontWeight: 500 }}>{v}</div>
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</Card>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
window.Dashboard = Dashboard;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
7
|
+
<title>Yana AI</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
|
|
9
|
+
<link href="https://fonts.bunny.net/css?family=be-vietnam-pro:300,400,500,600,700&display=swap" rel="stylesheet" />
|
|
10
|
+
<link rel="stylesheet" href="themes.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
|
15
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
|
16
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
17
|
+
<!-- Plain JS first — sets window.YANA before any JSX runs -->
|
|
18
|
+
<script src="/shared/crypto-store.js"></script>
|
|
19
|
+
<script src="/shared/data.js"></script>
|
|
20
|
+
<!-- JSX components in dependency order -->
|
|
21
|
+
<script type="text/babel" src="/shared/tweaks-panel.jsx"></script>
|
|
22
|
+
<script type="text/babel" src="components.jsx"></script>
|
|
23
|
+
<script type="text/babel" src="dashboard.jsx"></script>
|
|
24
|
+
<script type="text/babel" src="chat.jsx"></script>
|
|
25
|
+
<script type="text/babel" src="spaces.jsx"></script>
|
|
26
|
+
<script type="text/babel" src="system.jsx"></script>
|
|
27
|
+
<script type="text/babel" src="app.jsx"></script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|