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,248 @@
1
+ // Yana AI — Agent Space + Mission Center
2
+ // Agent catalog is real: GET /api/agents reads core/agents/*.md frontmatter.
3
+ function AgentCard({ a }) {
4
+ return (
5
+ <div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "var(--pad-card)", display: "flex", flexDirection: "column", gap: 10 }}>
6
+ <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
7
+ <div style={{
8
+ width: 38, height: 38, borderRadius: 13, flex: "none", display: "grid", placeItems: "center",
9
+ fontSize: 15, fontWeight: 500, color: "var(--primary)",
10
+ background: "var(--primary-soft)", boxShadow: "inset 0 1px 0 rgba(255,255,255,.5)",
11
+ }}>{a.name[0].toUpperCase()}</div>
12
+ <div style={{ lineHeight: 1.25, minWidth: 0 }}>
13
+ <div style={{ fontSize: 14.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.name}</div>
14
+ <div style={{ fontSize: 12, color: "var(--ink-3)" }}>{a.category}</div>
15
+ </div>
16
+ </div>
17
+ <div style={{ fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5 }}>
18
+ {a.description || L("No description.", "Chưa có mô tả.")}
19
+ </div>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ function AgentSpace() {
25
+ const [data, setData] = React.useState(null);
26
+ const [filter, setFilter] = React.useState("all");
27
+
28
+ React.useEffect(() => {
29
+ fetch("/api/agents")
30
+ .then((r) => (r.ok ? r.json() : null))
31
+ .then((d) => { if (d) setData(d); })
32
+ .catch(() => {});
33
+ }, []);
34
+
35
+ const agents = data ? data.agents : [];
36
+ const categories = ["all", ...Array.from(new Set(agents.map((a) => a.category)))];
37
+ const visible = filter === "all" ? agents : agents.filter((a) => a.category === filter);
38
+
39
+ return (
40
+ <div data-screen-label="Agent Space">
41
+ <PageHeader
42
+ title={L("Agent Space", "Không gian tác nhân")}
43
+ sub={data
44
+ ? data.total + L(" agents in catalog · none running — agents start when a mission dispatches", " tác nhân trong danh mục · chưa có tác nhân nào chạy — khởi động khi nhiệm vụ được giao")
45
+ : L("Loading agent catalog…", "Đang tải danh mục tác nhân…")}>
46
+ <select value={filter} onChange={(e) => setFilter(e.target.value)} style={{
47
+ padding: "7px 12px", borderRadius: 99, border: "1px solid var(--border-strong)",
48
+ background: "transparent", color: "var(--ink-2)", fontSize: 12.5, fontFamily: "inherit", cursor: "pointer",
49
+ }}>
50
+ {categories.map((c) => <option key={c} value={c}>{c === "all" ? L("All categories", "Tất cả danh mục") : c}</option>)}
51
+ </select>
52
+ </PageHeader>
53
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))", gap: "var(--gap)" }}>
54
+ {visible.map((a) => <AgentCard key={a.category + "/" + a.name} a={a} />)}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ /* ---------- Mission Center ---------- */
61
+ const TASK_STATE = {
62
+ done: { label: () => L("Done", "Xong"), color: "var(--good)" },
63
+ active: { label: () => L("Active", "Đang chạy"), color: "var(--primary)" },
64
+ queued: { label: () => L("Queued", "Đang chờ"), color: "var(--ink-3)" },
65
+ };
66
+
67
+ // Clicking a task cycles its state: queued → active → done → queued
68
+ const NEXT_STATE = { queued: "active", active: "done", done: "queued" };
69
+
70
+ function MissionCard({ m, open, onToggle, onUpdate, onDelete, onPlan, planning }) {
71
+ function cycleTask(i) {
72
+ const tasks = m.tasks.map((tk, j) => j === i ? { ...tk, state: NEXT_STATE[tk.state] } : tk);
73
+ onUpdate({ id: m.id, tasks });
74
+ }
75
+ const statusColor = m.status === "done" ? "var(--good)" : m.status === "active" ? "var(--primary)" : "var(--gold)";
76
+ return (
77
+ <div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "var(--pad-card)" }}>
78
+ <button onClick={onToggle} style={{
79
+ display: "grid", gridTemplateColumns: "1fr 130px 52px 18px", alignItems: "center", gap: 14,
80
+ width: "100%", background: "none", border: "none", cursor: "pointer", textAlign: "left", color: "inherit", padding: 0,
81
+ }}>
82
+ <div style={{ lineHeight: 1.3, minWidth: 0 }}>
83
+ <div style={{ fontSize: 14.5, fontWeight: 500 }}>{m.name}</div>
84
+ <div style={{ fontSize: 12, color: "var(--ink-3)" }}>
85
+ {m.owner} · <span style={{ color: statusColor }}>{m.status}</span>
86
+ {m.route && <span> · {L("route:", "tuyến:")} {m.route}</span>}
87
+ {m.skill && <span> · {m.skill}</span>}
88
+ </div>
89
+ </div>
90
+ <div className="bar"><i style={{ width: m.progress + "%" }}></i></div>
91
+ <span style={{ fontSize: 12.5, color: "var(--ink-2)", textAlign: "right" }}>{m.progress}%</span>
92
+ <span style={{ color: "var(--ink-3)", transform: open ? "rotate(90deg)" : "none", transition: "transform .18s", display: "inline-flex" }}>{Icons.chevron(14)}</span>
93
+ </button>
94
+ {open && (
95
+ <div style={{ marginTop: 13, paddingTop: 11, borderTop: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 2 }}>
96
+ {m.tasks.map((tk, i) => {
97
+ const st = TASK_STATE[tk.state];
98
+ return (
99
+ <button key={i} onClick={() => cycleTask(i)}
100
+ title={L("Click to advance state", "Nhấn để chuyển trạng thái")}
101
+ style={{
102
+ display: "grid", gridTemplateColumns: "16px 1fr 110px 64px", alignItems: "center", gap: 11,
103
+ padding: "6px 0", fontSize: 13, background: "none", border: "none",
104
+ cursor: "pointer", textAlign: "left", color: "inherit", width: "100%",
105
+ }}>
106
+ <span style={{ color: st.color, display: "inline-flex" }}>
107
+ {tk.state === "done" ? Icons.check(14) : tk.state === "active" ? Icons.spark(14) : Icons.clock(14)}
108
+ </span>
109
+ <span style={{ color: tk.state === "done" ? "var(--ink-3)" : "var(--ink)" }}>{tk.name}</span>
110
+ <span style={{ fontSize: 12, color: "var(--ink-3)" }}>{tk.agent}</span>
111
+ <span style={{ fontSize: 11.5, fontWeight: 500, color: st.color, textAlign: "right" }}>{st.label()}</span>
112
+ </button>
113
+ );
114
+ })}
115
+ <div style={{ display: "flex", gap: 8, marginTop: 10 }}>
116
+ <button onClick={() => onPlan(m)} disabled={planning} style={{
117
+ display: "flex", alignItems: "center", gap: 6, padding: "6px 13px", borderRadius: 99,
118
+ border: "none", cursor: planning ? "wait" : "pointer", fontSize: 12, fontWeight: 500,
119
+ background: "var(--primary-soft)", color: "var(--primary)",
120
+ }}>{Icons.spark(13)} {planning ? L("Planning…", "Đang lập kế hoạch…") : L("Plan with Yana", "Yana lập kế hoạch")}</button>
121
+ <button onClick={() => onDelete(m)} style={{
122
+ padding: "6px 13px", borderRadius: 99, border: "none", cursor: "pointer", fontSize: 12,
123
+ background: "rgba(var(--shadow-rgb), .07)", color: "var(--ink-3)",
124
+ }}>{L("Delete", "Xóa")}</button>
125
+ </div>
126
+ </div>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ // "Plan with Yana": ask the connected provider for a 3-7 step task breakdown
133
+ // (JSON), then PATCH the mission. Reuses the /api/chat streaming pipeline.
134
+ async function planMission(m) {
135
+ const cfg = window.getProviderConfig();
136
+ if (!cfg.apiKey) { alert(L("Add a provider API key first (Providers page).", "Thêm API key ở mục Nhà cung cấp trước.")); return null; }
137
+
138
+ const prompt =
139
+ `Break this mission into 3-7 concrete tasks: "${m.name}".\n` +
140
+ `Reply with ONLY a JSON array, no prose: ` +
141
+ `[{"name":"<task>","agent":"<one of: Navigator, Builder, Reviewer, Researcher>"}]`;
142
+
143
+ const res = await fetch("/api/chat", {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ task: prompt, apiKey: cfg.apiKey, provider: cfg.provider }),
147
+ });
148
+ if (!res.ok || !res.body) return null;
149
+
150
+ // Drain the SSE stream into one string
151
+ const reader = res.body.getReader();
152
+ const dec = new TextDecoder();
153
+ let raw = "";
154
+ while (true) {
155
+ const { done, value } = await reader.read();
156
+ if (done) break;
157
+ for (const line of dec.decode(value, { stream: true }).split("\n")) {
158
+ if (!line.startsWith("data: ")) continue;
159
+ const payload = line.slice(6).trim();
160
+ if (payload === "[DONE]") continue;
161
+ try { const o = JSON.parse(payload); if (o.text) raw += o.text; } catch (_) {}
162
+ }
163
+ }
164
+ const match = raw.match(/\[[\s\S]*\]/);
165
+ if (!match) return null;
166
+ try {
167
+ return JSON.parse(match[0])
168
+ .filter((t) => t && t.name)
169
+ .map((t) => ({ name: String(t.name), agent: String(t.agent || "Navigator"), state: "queued" }));
170
+ } catch (_) { return null; }
171
+ }
172
+
173
+ function MissionCenter() {
174
+ const [missions, setMissions] = React.useState(null);
175
+ const [open, setOpen] = React.useState(() => window.YANA._openMission || null);
176
+ const [planningId, setPlanningId] = React.useState(null);
177
+
178
+ const reload = React.useCallback(() => {
179
+ fetch("/api/missions")
180
+ .then((r) => (r.ok ? r.json() : null))
181
+ .then((d) => { if (d) setMissions(d.missions); })
182
+ .catch(() => {});
183
+ }, []);
184
+ React.useEffect(reload, []);
185
+
186
+ async function update(patch) {
187
+ const r = await fetch("/api/missions/update", {
188
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch),
189
+ });
190
+ if (r.ok) reload();
191
+ }
192
+
193
+ async function create() {
194
+ const name = window.prompt(L("What should this mission accomplish?", "Nhiệm vụ này cần hoàn thành điều gì?"));
195
+ if (!name || !name.trim()) return;
196
+ const r = await fetch("/api/missions", {
197
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim() }),
198
+ });
199
+ if (r.ok) { const { mission } = await r.json(); setOpen(mission.id); reload(); }
200
+ }
201
+
202
+ async function remove(m) {
203
+ if (!window.confirm(L("Delete mission “" + m.name + "”?", "Xóa nhiệm vụ “" + m.name + "”?"))) return;
204
+ await fetch("/api/missions/delete", {
205
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: m.id }),
206
+ });
207
+ reload();
208
+ }
209
+
210
+ async function plan(m) {
211
+ setPlanningId(m.id);
212
+ try {
213
+ const tasks = await planMission(m);
214
+ if (tasks && tasks.length) await update({ id: m.id, tasks, status: "active" });
215
+ else alert(L("Could not get a plan — check the provider key.", "Không lấy được kế hoạch — kiểm tra API key."));
216
+ } finally { setPlanningId(null); }
217
+ }
218
+
219
+ return (
220
+ <div data-screen-label="Mission Center">
221
+ <PageHeader title={L("Mission Center", "Trung tâm nhiệm vụ")} sub={L("Multi-agent work, visible end to end — progress, owners, dependencies.", "Công việc đa tác nhân, nhìn thấy từ đầu đến cuối — tiến độ, người phụ trách, phụ thuộc.")}>
222
+ <button onClick={create} style={{
223
+ display: "flex", alignItems: "center", gap: 7, padding: "8px 15px", borderRadius: 99,
224
+ border: "none", cursor: "pointer", background: "var(--primary)", color: "white",
225
+ fontSize: 13, fontWeight: 500, boxShadow: "0 4px 12px color-mix(in oklab, var(--primary) 30%, transparent)",
226
+ }}>{Icons.plus(15)} {L("New mission", "Nhiệm vụ mới")}</button>
227
+ </PageHeader>
228
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--gap)", maxWidth: 860 }}>
229
+ {missions && missions.length === 0 && (
230
+ <div style={{ color: "var(--ink-3)", fontSize: 13 }}>
231
+ {L("No missions yet — create one above or from the Lake.", "Chưa có nhiệm vụ — tạo ở trên hoặc từ Mặt hồ.")}
232
+ </div>
233
+ )}
234
+ {(missions || []).map((m) => (
235
+ <MissionCard key={m.id} m={m}
236
+ open={open === m.id}
237
+ onToggle={() => setOpen(open === m.id ? null : m.id)}
238
+ onUpdate={update}
239
+ onDelete={remove}
240
+ onPlan={plan}
241
+ planning={planningId === m.id} />
242
+ ))}
243
+ </div>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ Object.assign(window, { AgentSpace, MissionCenter });