privateboard 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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/cli.js +10502 -0
- package/dist/cli.js.map +1 -0
- package/package.json +63 -0
- package/public/adjourn-overlay.css +253 -0
- package/public/agent-overlay.css +444 -0
- package/public/agent-overlay.js +604 -0
- package/public/agent-profile.css +3230 -0
- package/public/agent-profile.js +3329 -0
- package/public/app.js +6629 -0
- package/public/auto-hide-scroll.js +90 -0
- package/public/avatar-skill.js +793 -0
- package/public/avatars/chair.svg +98 -0
- package/public/avatars/first-principles.svg +122 -0
- package/public/avatars/long-horizon.svg +147 -0
- package/public/avatars/open_ai.png +0 -0
- package/public/avatars/phenomenologist.svg +130 -0
- package/public/avatars/socrates.svg +187 -0
- package/public/avatars/user-empathy.svg +117 -0
- package/public/avatars/value-investor.svg +117 -0
- package/public/favicon.svg +10 -0
- package/public/fonts/agent-Italic.woff2 +0 -0
- package/public/fonts/human-sans.woff2 +0 -0
- package/public/icons.css +103 -0
- package/public/models-cache.js +57 -0
- package/public/new-agent.css +1359 -0
- package/public/new-agent.js +675 -0
- package/public/onboarding.css +628 -0
- package/public/onboarding.js +782 -0
- package/public/prototype-dashboard.html +7596 -0
- package/public/report/spines/a16z-thesis.css +1055 -0
- package/public/report/spines/anthropic-essay.css +556 -0
- package/public/report/spines/boardroom-dark.css +1082 -0
- package/public/report/spines/gartner-note.css +538 -0
- package/public/report/spines/mckinsey-deck.css +523 -0
- package/public/report/spines/openai-paper.css +516 -0
- package/public/report.html +1417 -0
- package/public/room-settings.css +895 -0
- package/public/room-settings.js +1039 -0
- package/public/themes.css +338 -0
- package/public/user-settings.css +1236 -0
- package/public/user-settings.js +1291 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════
|
|
2
|
+
NEW AGENT OVERLAY
|
|
3
|
+
═══════════════════════════════════════════
|
|
4
|
+
Public API: window.openNewAgent()
|
|
5
|
+
Triggered by clicks on [data-new-agent] (the sidebar's
|
|
6
|
+
`New agent` button in the Agents tab).
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
const MODEL_GROUPS = [
|
|
10
|
+
{ provider: "anthropic", models: [
|
|
11
|
+
{ v: "sonnet-4-6", name: "Sonnet 4.6", deck: "balanced · default" },
|
|
12
|
+
{ v: "opus-4-7", name: "Opus 4.7", deck: "deep reasoning" },
|
|
13
|
+
{ v: "haiku-4-5", name: "Haiku 4.5", deck: "fast · low-cost" }
|
|
14
|
+
]},
|
|
15
|
+
{ provider: "openai", models: [
|
|
16
|
+
{ v: "gpt-5-5", name: "GPT-5.5", deck: "flagship · 1M ctx" },
|
|
17
|
+
{ v: "gpt-5-4", name: "GPT-5.4", deck: "general · 1M ctx" },
|
|
18
|
+
{ v: "gpt-5-4-mini", name: "GPT-5.4 Mini", deck: "fast · 400k ctx" }
|
|
19
|
+
]},
|
|
20
|
+
{ provider: "google", models: [
|
|
21
|
+
{ v: "gemini-3-1", name: "Gemini 3.1 Pro", deck: "flagship · 1M ctx" },
|
|
22
|
+
{ v: "gemini-3-flash", name: "Gemini 3 Flash", deck: "frontier flash · 1M ctx" },
|
|
23
|
+
{ v: "gemini-3-1-flash", name: "Gemini 3.1 Flash Lite", deck: "fast · 1M ctx" }
|
|
24
|
+
]},
|
|
25
|
+
{ provider: "xai", models: [
|
|
26
|
+
{ v: "grok-4-3", name: "Grok 4.3", deck: "flagship · 1M ctx" },
|
|
27
|
+
{ v: "grok-4-1-fast", name: "Grok 4.1 Fast", deck: "fast · 256k ctx" }
|
|
28
|
+
]}
|
|
29
|
+
];
|
|
30
|
+
const ALL_MODELS = MODEL_GROUPS.flatMap((g) => g.models);
|
|
31
|
+
|
|
32
|
+
/* ─── Skill catalog ─────────────────────────────────
|
|
33
|
+
Installable abilities — slot-grid analog of an RPG
|
|
34
|
+
equipment ring. v1 is visual only (the LLM adapter
|
|
35
|
+
doesn't yet wire tool-use), but the UI shows the
|
|
36
|
+
vocabulary so users can shape the director's
|
|
37
|
+
intended capability surface. */
|
|
38
|
+
const SKILL_CATALOG = [
|
|
39
|
+
{ v: "search", icon: "⌕", name: "Web Search", deck: "real-time fetch" },
|
|
40
|
+
{ v: "pdf", icon: "▤", name: "PDF Parse", deck: "extract from PDFs" },
|
|
41
|
+
{ v: "shell", icon: "⌨", name: "Shell", deck: "execute commands" },
|
|
42
|
+
{ v: "browser", icon: "◍", name: "Browser", deck: "navigate the web" },
|
|
43
|
+
{ v: "code", icon: "▶", name: "Code Exec", deck: "run python / node" },
|
|
44
|
+
{ v: "tables", icon: "▦", name: "Tables", deck: "csv · xlsx" },
|
|
45
|
+
{ v: "memory", icon: "✎", name: "Memory", deck: "long-term notes" },
|
|
46
|
+
{ v: "urls", icon: "↗", name: "URL Fetch", deck: "grab pages" },
|
|
47
|
+
];
|
|
48
|
+
const SKILL_SLOTS = 8;
|
|
49
|
+
|
|
50
|
+
function escape(s) {
|
|
51
|
+
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
52
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
53
|
+
}[c]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ───── 8-bit avatar generator ─────
|
|
57
|
+
Pure function: same seed → same avatar.
|
|
58
|
+
placeholder=true returns a neutral grey silhouette.
|
|
59
|
+
*/
|
|
60
|
+
const PALETTES = {
|
|
61
|
+
hair: ["#7B4F2A","#3A2418","#D4A347","#5A3D8F","#B53D3D","#1F1F1F","#8B7355","#A05A2C","#2D5532","#C46A2C"],
|
|
62
|
+
skin: ["#F4C99B","#E8B589","#D9A077","#C68863","#A86B47","#8B5A3C"],
|
|
63
|
+
shirt: ["#6FB572","#6A9B97","#B5706A","#B59E6A","#9B7BB5","#5470A8","#7B5A8A","#C46A2C"],
|
|
64
|
+
eye: ["#1A1A1A","#3D2817","#1F3A5E","#2D2618"]
|
|
65
|
+
};
|
|
66
|
+
const FACE_MASK = [
|
|
67
|
+
"........XXXXXXXX........",
|
|
68
|
+
"......XXXXXXXXXXXX......",
|
|
69
|
+
".....XXXXXXXXXXXXXX.....",
|
|
70
|
+
"....XXXXXXXXXXXXXXXX....",
|
|
71
|
+
"....XXXXXXXXXXXXXXXX....",
|
|
72
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
73
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
74
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
75
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
76
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
77
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
78
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
79
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
80
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
81
|
+
"...XXXXXXXXXXXXXXXXXX...",
|
|
82
|
+
"....XXXXXXXXXXXXXXXX....",
|
|
83
|
+
"....XXXXXXXXXXXXXXXX....",
|
|
84
|
+
".....XXXXXXXXXXXXXX.....",
|
|
85
|
+
"......XXXXXXXXXXXX......"
|
|
86
|
+
];
|
|
87
|
+
function makeRng(seed) {
|
|
88
|
+
let h = 2166136261 >>> 0;
|
|
89
|
+
const s = String(seed || "agent");
|
|
90
|
+
for (let i = 0; i < s.length; i++) {
|
|
91
|
+
h = (h ^ s.charCodeAt(i)) >>> 0;
|
|
92
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
93
|
+
}
|
|
94
|
+
let st = h || 1;
|
|
95
|
+
return () => {
|
|
96
|
+
st = (Math.imul(st, 1664525) + 1013904223) >>> 0;
|
|
97
|
+
return st / 4294967296;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function pickFrom(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
|
|
101
|
+
function shortHash(seed) {
|
|
102
|
+
const rng = makeRng(seed);
|
|
103
|
+
return [0,0,0,0].map(() => Math.floor(rng() * 16).toString(16)).join("");
|
|
104
|
+
}
|
|
105
|
+
// Avatar generation delegates to the shared AvatarSkill
|
|
106
|
+
// (see public/avatar-skill.js). One source of truth for the
|
|
107
|
+
// 8-bit pixel-art look used here, in user settings, and anywhere
|
|
108
|
+
// else that wants a director-style avatar.
|
|
109
|
+
function generateAvatar(seed, opts) {
|
|
110
|
+
return window.AvatarSkill.generate(seed, opts);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function modalHTML() {
|
|
114
|
+
// Slimmed form (per user brief): only name, bio, avatar, and
|
|
115
|
+
// instruction. Rules/skills/model/knowledge live in the agent
|
|
116
|
+
// profile after creation. Chrome aligns with the global overlay
|
|
117
|
+
// pattern (mirrors .convene-modal): backdrop blur, corner
|
|
118
|
+
// brackets, classification + head + body + foot.
|
|
119
|
+
return `
|
|
120
|
+
<div class="new-agent-overlay" id="new-agent-overlay" role="dialog" aria-modal="true" aria-hidden="true">
|
|
121
|
+
<div class="new-agent-modal" role="document">
|
|
122
|
+
<div class="na-classification">
|
|
123
|
+
<span><span class="dot">●</span> directors · new</span>
|
|
124
|
+
<span class="right">// shape the role</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<header class="na-head">
|
|
128
|
+
<div>
|
|
129
|
+
<div class="na-step-num">// new <span class="hl">director</span> · manual setup</div>
|
|
130
|
+
<div class="na-step-title">shape the role</div>
|
|
131
|
+
</div>
|
|
132
|
+
<button type="button" class="na-close" aria-label="Close">✕</button>
|
|
133
|
+
</header>
|
|
134
|
+
|
|
135
|
+
<div class="na-body">
|
|
136
|
+
<div class="na-stack">
|
|
137
|
+
|
|
138
|
+
<div class="na-avatar-block">
|
|
139
|
+
<div class="na-portrait">
|
|
140
|
+
<div data-na-avatar class="na-avatar-frame"></div>
|
|
141
|
+
</div>
|
|
142
|
+
<button type="button" class="na-avatar-regen" data-na-regen>
|
|
143
|
+
<span class="na-avatar-regen-mark">◆</span>
|
|
144
|
+
<span class="na-avatar-regen-label">generate 8-bit avatar</span>
|
|
145
|
+
</button>
|
|
146
|
+
<div class="na-avatar-vibe" data-na-vibe></div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="na-fields">
|
|
150
|
+
|
|
151
|
+
<div class="na-field">
|
|
152
|
+
<label class="na-field-label">
|
|
153
|
+
<span>Name</span>
|
|
154
|
+
<span class="na-field-meta"><span class="na-name-count">0</span>/32</span>
|
|
155
|
+
</label>
|
|
156
|
+
<div class="na-input-wrap na-name-wrap">
|
|
157
|
+
<input type="text" class="na-name-input" placeholder="Aurelia · The Long-Cycle Strategist" maxlength="32">
|
|
158
|
+
</div>
|
|
159
|
+
<div class="na-field-hint">handle: <span class="na-handle-preview">/new_agent</span></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="na-field">
|
|
163
|
+
<label class="na-field-label">
|
|
164
|
+
<span>Intro</span>
|
|
165
|
+
<span class="na-field-meta"><span class="na-desc-count">0</span>/280</span>
|
|
166
|
+
</label>
|
|
167
|
+
<div class="na-textarea-wrap intro">
|
|
168
|
+
<textarea class="na-desc-input" placeholder="One or two sentences · how this director shows up in a room. Reads everything on a hundred-year scale. Knows which patterns repeat and which never do." maxlength="280"></textarea>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="na-field-hint">becomes their public bio</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="na-field">
|
|
174
|
+
<label class="na-field-label">
|
|
175
|
+
<span>Instruction</span>
|
|
176
|
+
<span class="na-field-meta"><span class="na-instr-count">0</span> chars · markdown</span>
|
|
177
|
+
</label>
|
|
178
|
+
<div class="na-textarea-wrap tall">
|
|
179
|
+
<textarea class="na-instr-input" spellcheck="false" placeholder="### Role
|
|
180
|
+
You are __, the room's __. Your job is to ___.
|
|
181
|
+
|
|
182
|
+
### Voice
|
|
183
|
+
Demand ___. Don't ___. Cite ___ when ___.
|
|
184
|
+
|
|
185
|
+
### Boundaries
|
|
186
|
+
When the room ___, raise an objection."></textarea>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="na-field-hint">applies to every room they join · skills, rules and model are configured later in the profile</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<footer class="na-foot">
|
|
196
|
+
<div class="na-foot-meta">configure skills · rules · model after creation</div>
|
|
197
|
+
<div class="na-foot-actions">
|
|
198
|
+
<button type="button" class="na-cancel">cancel</button>
|
|
199
|
+
<button type="button" class="na-create" disabled>
|
|
200
|
+
<span class="na-create-mark">◆</span>
|
|
201
|
+
<span>create director</span>
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</footer>
|
|
205
|
+
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let overlay, modal;
|
|
212
|
+
// Synthetic knowledge state (visual only — no real upload).
|
|
213
|
+
let knowState = [];
|
|
214
|
+
// Rules state — array of strings, max 5. Visual-only in v1; not yet
|
|
215
|
+
// submitted to the backend.
|
|
216
|
+
let rulesState = [];
|
|
217
|
+
const RULES_MAX = 5;
|
|
218
|
+
// Skills state — array of skill ids (e.g. "search","pdf"). Visual
|
|
219
|
+
// only in v1.
|
|
220
|
+
let skillsState = [];
|
|
221
|
+
// Avatar state: placeholder until user clicks regenerate.
|
|
222
|
+
let avatarState = { placeholder: true, seed: null, roll: 0 };
|
|
223
|
+
|
|
224
|
+
/* ─── Rules ───────────────────────────── */
|
|
225
|
+
function renderRules() {
|
|
226
|
+
const list = modal && modal.querySelector("[data-na-rules]");
|
|
227
|
+
const addBtn = modal && modal.querySelector("[data-na-rule-add]");
|
|
228
|
+
if (!list) return;
|
|
229
|
+
if (rulesState.length === 0) {
|
|
230
|
+
list.innerHTML = `<li class="na-rule-empty">no rules yet · directors will follow only their instruction</li>`;
|
|
231
|
+
} else {
|
|
232
|
+
list.innerHTML = rulesState.map((body, i) => `
|
|
233
|
+
<li class="na-rule" data-rule-idx="${i}">
|
|
234
|
+
<span class="na-rule-num">${i + 1}</span>
|
|
235
|
+
<input type="text" class="na-rule-input" placeholder="never preface · cite the load-bearing claim with **bold** · ..." maxlength="120" value="${escape(body)}">
|
|
236
|
+
<button type="button" class="na-rule-rm" data-na-rule-rm="${i}" title="Remove">✕</button>
|
|
237
|
+
</li>
|
|
238
|
+
`).join("");
|
|
239
|
+
}
|
|
240
|
+
if (addBtn) {
|
|
241
|
+
const atCap = rulesState.length >= RULES_MAX;
|
|
242
|
+
addBtn.disabled = atCap;
|
|
243
|
+
addBtn.classList.toggle("at-cap", atCap);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function addRule() {
|
|
247
|
+
if (rulesState.length >= RULES_MAX) return;
|
|
248
|
+
rulesState.push("");
|
|
249
|
+
renderRules();
|
|
250
|
+
// Focus the freshly-added input.
|
|
251
|
+
const inputs = modal.querySelectorAll(".na-rule-input");
|
|
252
|
+
const last = inputs[inputs.length - 1];
|
|
253
|
+
if (last) last.focus();
|
|
254
|
+
}
|
|
255
|
+
function removeRule(idx) {
|
|
256
|
+
if (idx < 0 || idx >= rulesState.length) return;
|
|
257
|
+
rulesState.splice(idx, 1);
|
|
258
|
+
renderRules();
|
|
259
|
+
}
|
|
260
|
+
function setRule(idx, body) {
|
|
261
|
+
if (idx < 0 || idx >= rulesState.length) return;
|
|
262
|
+
rulesState[idx] = body;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ─── Skills ──────────────────────────── */
|
|
266
|
+
function renderSkills() {
|
|
267
|
+
const grid = modal && modal.querySelector("[data-na-skill-grid]");
|
|
268
|
+
const countEl = modal && modal.querySelector(".na-skill-count");
|
|
269
|
+
if (!grid) return;
|
|
270
|
+
const slots = [];
|
|
271
|
+
for (let i = 0; i < SKILL_SLOTS; i++) {
|
|
272
|
+
const v = skillsState[i];
|
|
273
|
+
const s = v ? SKILL_CATALOG.find((x) => x.v === v) : null;
|
|
274
|
+
if (s) {
|
|
275
|
+
slots.push(`
|
|
276
|
+
<button type="button" class="na-skill-slot filled" data-na-skill-slot="${i}" title="${escape(s.name)} · click to remove">
|
|
277
|
+
<span class="na-skill-icon">${escape(s.icon)}</span>
|
|
278
|
+
<span class="na-skill-name">${escape(s.name)}</span>
|
|
279
|
+
</button>
|
|
280
|
+
`);
|
|
281
|
+
} else {
|
|
282
|
+
slots.push(`
|
|
283
|
+
<button type="button" class="na-skill-slot empty" data-na-skill-slot="${i}" title="Install ability">
|
|
284
|
+
<span class="na-skill-icon">+</span>
|
|
285
|
+
<span class="na-skill-name">empty</span>
|
|
286
|
+
</button>
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
grid.innerHTML = slots.join("");
|
|
291
|
+
if (countEl) countEl.textContent = String(skillsState.length);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Open a tiny inline picker beneath the slot — shows abilities the
|
|
295
|
+
* director doesn't already have. Click one to install it into the
|
|
296
|
+
* given slot (or the next empty if slot is null). */
|
|
297
|
+
function openSkillPicker(anchor, targetSlot) {
|
|
298
|
+
closeSkillPicker();
|
|
299
|
+
const installed = new Set(skillsState);
|
|
300
|
+
const available = SKILL_CATALOG.filter((s) => !installed.has(s.v));
|
|
301
|
+
if (available.length === 0) return;
|
|
302
|
+
|
|
303
|
+
const pop = document.createElement("div");
|
|
304
|
+
pop.className = "na-skill-picker";
|
|
305
|
+
pop.id = "na-skill-picker";
|
|
306
|
+
pop.innerHTML = available.map((s) => `
|
|
307
|
+
<button type="button" class="na-skill-pick" data-skill-pick="${escape(s.v)}">
|
|
308
|
+
<span class="na-skill-pick-icon">${escape(s.icon)}</span>
|
|
309
|
+
<span class="na-skill-pick-body">
|
|
310
|
+
<span class="na-skill-pick-name">${escape(s.name)}</span>
|
|
311
|
+
<span class="na-skill-pick-deck">${escape(s.deck)}</span>
|
|
312
|
+
</span>
|
|
313
|
+
</button>
|
|
314
|
+
`).join("");
|
|
315
|
+
document.body.appendChild(pop);
|
|
316
|
+
|
|
317
|
+
// Position below the anchor button.
|
|
318
|
+
const r = anchor.getBoundingClientRect();
|
|
319
|
+
const margin = 6;
|
|
320
|
+
pop.style.left = Math.max(margin, Math.min(r.left, window.innerWidth - 260 - margin)) + "px";
|
|
321
|
+
pop.style.top = (r.bottom + 4) + "px";
|
|
322
|
+
|
|
323
|
+
// Stash target slot index for the install handler.
|
|
324
|
+
pop.dataset.targetSlot = String(targetSlot ?? "");
|
|
325
|
+
|
|
326
|
+
// Dismiss on outside click.
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
const off = (e) => {
|
|
329
|
+
if (e.target.closest("#na-skill-picker")) return;
|
|
330
|
+
closeSkillPicker();
|
|
331
|
+
document.removeEventListener("click", off, true);
|
|
332
|
+
};
|
|
333
|
+
document.addEventListener("click", off, true);
|
|
334
|
+
}, 0);
|
|
335
|
+
}
|
|
336
|
+
function closeSkillPicker() {
|
|
337
|
+
const pop = document.getElementById("na-skill-picker");
|
|
338
|
+
if (pop) pop.remove();
|
|
339
|
+
}
|
|
340
|
+
function installSkill(slotIdx, skillV) {
|
|
341
|
+
if (!SKILL_CATALOG.some((s) => s.v === skillV)) return;
|
|
342
|
+
if (skillsState.includes(skillV)) return;
|
|
343
|
+
// If slotIdx is provided and that slot is empty, place there.
|
|
344
|
+
// Otherwise append to first available position.
|
|
345
|
+
if (slotIdx !== null && slotIdx >= 0 && slotIdx < SKILL_SLOTS && !skillsState[slotIdx]) {
|
|
346
|
+
skillsState[slotIdx] = skillV;
|
|
347
|
+
} else {
|
|
348
|
+
for (let i = 0; i < SKILL_SLOTS; i++) {
|
|
349
|
+
if (!skillsState[i]) { skillsState[i] = skillV; break; }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
renderSkills();
|
|
353
|
+
}
|
|
354
|
+
function uninstallSkill(slotIdx) {
|
|
355
|
+
if (slotIdx < 0 || slotIdx >= SKILL_SLOTS) return;
|
|
356
|
+
skillsState[slotIdx] = undefined;
|
|
357
|
+
// Compact array so empty slots are at the end visually.
|
|
358
|
+
skillsState = skillsState.filter(Boolean);
|
|
359
|
+
renderSkills();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getProviderStatus(provider) {
|
|
363
|
+
const keys = (typeof window.boardroomKeys === "function" ? window.boardroomKeys() : {}) || {};
|
|
364
|
+
if (keys[provider]) return { label: "direct", cls: "direct" };
|
|
365
|
+
if (keys.openrouter) return { label: "via openrouter", cls: "via" };
|
|
366
|
+
return { label: "no key", cls: "none" };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function refreshProviderStatus() {
|
|
370
|
+
if (!modal) return;
|
|
371
|
+
modal.querySelectorAll(".na-model-grp[data-provider]").forEach((grp) => {
|
|
372
|
+
const provider = grp.dataset.provider;
|
|
373
|
+
const s = getProviderStatus(provider);
|
|
374
|
+
const badge = grp.querySelector(".na-model-grp-status");
|
|
375
|
+
if (!badge) return;
|
|
376
|
+
badge.textContent = "· " + s.label;
|
|
377
|
+
badge.classList.remove("direct", "via", "none");
|
|
378
|
+
badge.classList.add(s.cls);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function open() {
|
|
383
|
+
if (!overlay) return;
|
|
384
|
+
// Reset form to a clean slate every time.
|
|
385
|
+
modal.querySelector(".na-name-input").value = "";
|
|
386
|
+
modal.querySelector(".na-desc-input").value = "";
|
|
387
|
+
modal.querySelector(".na-instr-input").value = "";
|
|
388
|
+
avatarState = { placeholder: true, seed: null, roll: 0 };
|
|
389
|
+
paintAvatar();
|
|
390
|
+
refreshAll();
|
|
391
|
+
|
|
392
|
+
overlay.classList.add("open");
|
|
393
|
+
overlay.setAttribute("aria-hidden", "false");
|
|
394
|
+
document.body.style.overflow = "hidden";
|
|
395
|
+
setTimeout(() => modal.querySelector(".na-name-input").focus(), 80);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function close() {
|
|
399
|
+
if (!overlay) return;
|
|
400
|
+
overlay.classList.remove("open");
|
|
401
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
402
|
+
document.body.style.overflow = "";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function slugify(s) {
|
|
406
|
+
return String(s)
|
|
407
|
+
.trim()
|
|
408
|
+
.toLowerCase()
|
|
409
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
410
|
+
.replace(/^_+|_+$/g, "")
|
|
411
|
+
.slice(0, 14) || "new_agent";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function activeModelInfo() {
|
|
415
|
+
const c = modal.querySelector(".na-model-opt.active");
|
|
416
|
+
const fallback = { name: "—", provider: "—", deck: "—" };
|
|
417
|
+
if (!c) return fallback;
|
|
418
|
+
const v = c.dataset.model;
|
|
419
|
+
const m = ALL_MODELS.find((x) => x.v === v);
|
|
420
|
+
if (!m) return fallback;
|
|
421
|
+
let prov = "—";
|
|
422
|
+
for (const g of MODEL_GROUPS) {
|
|
423
|
+
if (g.models.some((x) => x.v === v)) { prov = g.provider; break; }
|
|
424
|
+
}
|
|
425
|
+
return { name: m.name, provider: prov, deck: m.deck };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function paintAvatar() {
|
|
429
|
+
const frame = modal.querySelector("[data-na-avatar]");
|
|
430
|
+
const seedEl = modal.querySelector("[data-na-seed]");
|
|
431
|
+
const rollEl = modal.querySelector("[data-na-roll]");
|
|
432
|
+
if (!frame) return;
|
|
433
|
+
if (avatarState.placeholder) {
|
|
434
|
+
frame.classList.add("placeholder");
|
|
435
|
+
frame.innerHTML = generateAvatar("__placeholder__", { placeholder: true });
|
|
436
|
+
if (seedEl) seedEl.textContent = "—";
|
|
437
|
+
if (rollEl) rollEl.textContent = "";
|
|
438
|
+
} else {
|
|
439
|
+
frame.classList.remove("placeholder");
|
|
440
|
+
const seedKey = avatarState.seed + "::" + avatarState.roll;
|
|
441
|
+
frame.innerHTML = generateAvatar(seedKey);
|
|
442
|
+
if (seedEl) seedEl.textContent = shortHash(avatarState.seed);
|
|
443
|
+
if (rollEl) rollEl.textContent = " · #" + avatarState.roll;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function positionDropdown() {
|
|
448
|
+
if (!modal) return;
|
|
449
|
+
const select = modal.querySelector("[data-na-model-select]");
|
|
450
|
+
const trigger = modal.querySelector("[data-na-trigger]");
|
|
451
|
+
const dropdown = modal.querySelector("[data-na-dropdown]");
|
|
452
|
+
if (!select || !trigger || !dropdown) return;
|
|
453
|
+
if (select.dataset.open !== "true") return;
|
|
454
|
+
|
|
455
|
+
const rect = trigger.getBoundingClientRect();
|
|
456
|
+
const vh = window.innerHeight;
|
|
457
|
+
const vw = window.innerWidth;
|
|
458
|
+
const margin = 8;
|
|
459
|
+
const gap = 4;
|
|
460
|
+
const spaceBelow = vh - rect.bottom - margin;
|
|
461
|
+
const spaceAbove = rect.top - margin;
|
|
462
|
+
|
|
463
|
+
// Reset previous run
|
|
464
|
+
dropdown.style.top = "";
|
|
465
|
+
dropdown.style.bottom = "";
|
|
466
|
+
dropdown.style.maxHeight = "";
|
|
467
|
+
|
|
468
|
+
// Width matches the trigger; clamp to viewport horizontally
|
|
469
|
+
const left = Math.max(margin, Math.min(rect.left, vw - rect.width - margin));
|
|
470
|
+
dropdown.style.left = left + "px";
|
|
471
|
+
dropdown.style.width = rect.width + "px";
|
|
472
|
+
|
|
473
|
+
const minPreferred = 220;
|
|
474
|
+
const flipUp = spaceBelow < minPreferred && spaceAbove > spaceBelow;
|
|
475
|
+
|
|
476
|
+
if (flipUp) {
|
|
477
|
+
dropdown.style.bottom = (vh - rect.top + gap) + "px";
|
|
478
|
+
dropdown.style.maxHeight = Math.max(140, spaceAbove - gap) + "px";
|
|
479
|
+
select.dataset.flip = "up";
|
|
480
|
+
} else {
|
|
481
|
+
dropdown.style.top = (rect.bottom + gap) + "px";
|
|
482
|
+
dropdown.style.maxHeight = Math.max(140, spaceBelow - gap) + "px";
|
|
483
|
+
select.dataset.flip = "down";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Regenerate the avatar by asking the LLM for a "vibe seed" derived
|
|
488
|
+
* from the director's name + bio, then painting the SVG via the
|
|
489
|
+
* shared AvatarSkill. Falls back to a local random seed if the
|
|
490
|
+
* endpoint errors (no key, network, etc.) so the button always
|
|
491
|
+
* produces a fresh face. */
|
|
492
|
+
async function regenerateAvatar() {
|
|
493
|
+
const name = modal.querySelector(".na-name-input").value.trim();
|
|
494
|
+
const desc = modal.querySelector(".na-desc-input").value.trim();
|
|
495
|
+
const btn = modal.querySelector("[data-na-regen]");
|
|
496
|
+
const labelEl = btn?.querySelector(".na-avatar-regen-label");
|
|
497
|
+
const vibeEl = modal.querySelector("[data-na-vibe]");
|
|
498
|
+
|
|
499
|
+
avatarState.placeholder = false;
|
|
500
|
+
avatarState.roll = (avatarState.roll || 0) + 1;
|
|
501
|
+
|
|
502
|
+
// Without a name, just produce a random seed locally — no point
|
|
503
|
+
// burning an LLM call on an empty form.
|
|
504
|
+
if (!name) {
|
|
505
|
+
avatarState.seed = (window.AvatarSkill?.randomSeed?.() || ("anon|" + Date.now()));
|
|
506
|
+
if (vibeEl) vibeEl.textContent = "";
|
|
507
|
+
paintAvatar();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (btn) btn.disabled = true;
|
|
512
|
+
const originalLabel = labelEl?.textContent || "generate 8-bit avatar";
|
|
513
|
+
if (labelEl) labelEl.textContent = "thinking…";
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const res = await fetch("/api/avatar/generate", {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: { "content-type": "application/json" },
|
|
519
|
+
body: JSON.stringify({ name, bio: desc }),
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) throw new Error("avatar gen failed");
|
|
522
|
+
const j = await res.json();
|
|
523
|
+
avatarState.seed = j.seed + "::" + avatarState.roll;
|
|
524
|
+
if (vibeEl) vibeEl.textContent = j.vibe || "";
|
|
525
|
+
} catch (_) {
|
|
526
|
+
avatarState.seed = (name + "|" + desc + "|" + avatarState.roll);
|
|
527
|
+
if (vibeEl) vibeEl.textContent = "";
|
|
528
|
+
} finally {
|
|
529
|
+
if (btn) btn.disabled = false;
|
|
530
|
+
if (labelEl) labelEl.textContent = originalLabel;
|
|
531
|
+
paintAvatar();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function refreshAll() {
|
|
536
|
+
const name = modal.querySelector(".na-name-input").value;
|
|
537
|
+
const desc = modal.querySelector(".na-desc-input").value;
|
|
538
|
+
const instr = modal.querySelector(".na-instr-input").value;
|
|
539
|
+
|
|
540
|
+
const handle = "/" + slugify(name);
|
|
541
|
+
modal.querySelector(".na-handle-preview").textContent = handle;
|
|
542
|
+
modal.querySelector(".na-name-count").textContent = name.length;
|
|
543
|
+
modal.querySelector(".na-desc-count").textContent = desc.length;
|
|
544
|
+
modal.querySelector(".na-instr-count").textContent = instr.length;
|
|
545
|
+
|
|
546
|
+
// Create button enabled when name + bio are present.
|
|
547
|
+
const create = modal.querySelector(".na-create");
|
|
548
|
+
const ready = name.trim().length >= 2 && desc.trim().length >= 8;
|
|
549
|
+
create.disabled = !ready;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function init() {
|
|
553
|
+
if (document.getElementById("new-agent-overlay")) return;
|
|
554
|
+
const wrap = document.createElement("div");
|
|
555
|
+
wrap.innerHTML = modalHTML().trim();
|
|
556
|
+
document.body.appendChild(wrap.firstChild);
|
|
557
|
+
|
|
558
|
+
overlay = document.getElementById("new-agent-overlay");
|
|
559
|
+
modal = overlay.querySelector(".new-agent-modal");
|
|
560
|
+
|
|
561
|
+
// Close
|
|
562
|
+
modal.querySelector(".na-close").addEventListener("click", close);
|
|
563
|
+
modal.querySelector(".na-cancel").addEventListener("click", close);
|
|
564
|
+
overlay.addEventListener("click", (e) => {
|
|
565
|
+
if (e.target === overlay) close();
|
|
566
|
+
});
|
|
567
|
+
document.addEventListener("keydown", (e) => {
|
|
568
|
+
if (e.key === "Escape" && overlay.classList.contains("open")) {
|
|
569
|
+
e.stopImmediatePropagation();
|
|
570
|
+
close();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Triggers anywhere
|
|
575
|
+
document.addEventListener("click", (e) => {
|
|
576
|
+
if (e.target.closest("[data-new-agent]")) {
|
|
577
|
+
e.preventDefault();
|
|
578
|
+
open();
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Live updates
|
|
583
|
+
modal.querySelector(".na-name-input").addEventListener("input", refreshAll);
|
|
584
|
+
modal.querySelector(".na-desc-input").addEventListener("input", refreshAll);
|
|
585
|
+
modal.querySelector(".na-instr-input").addEventListener("input", refreshAll);
|
|
586
|
+
|
|
587
|
+
// Avatar regenerate
|
|
588
|
+
modal.querySelector("[data-na-regen]").addEventListener("click", (e) => {
|
|
589
|
+
e.preventDefault();
|
|
590
|
+
regenerateAvatar();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Initial paint of placeholder
|
|
594
|
+
paintAvatar();
|
|
595
|
+
|
|
596
|
+
// Create — POST to /api/agents and refresh the sidebar's agents list.
|
|
597
|
+
modal.querySelector(".na-create").addEventListener("click", async () => {
|
|
598
|
+
const create = modal.querySelector(".na-create");
|
|
599
|
+
if (create.disabled) return;
|
|
600
|
+
|
|
601
|
+
const name = modal.querySelector(".na-name-input").value.trim();
|
|
602
|
+
const bio = modal.querySelector(".na-desc-input").value.trim();
|
|
603
|
+
const instruction = modal.querySelector(".na-instr-input").value.trim();
|
|
604
|
+
// Default model · resolved from the user's current key set via
|
|
605
|
+
// the shared /api/models cache (`defaultModelV` field). Without
|
|
606
|
+
// this, new agents were always born with `opus-4-7` even when
|
|
607
|
+
// the user only had a direct OpenAI key — the agent would then
|
|
608
|
+
// hit `NoKeyError` on every turn until the user manually
|
|
609
|
+
// changed its model. The cache may not have loaded yet (very
|
|
610
|
+
// first interaction); we fall back to `opus-4-7` only as a
|
|
611
|
+
// last resort and leave the runtime resolver to fix it up.
|
|
612
|
+
let modelV = "opus-4-7";
|
|
613
|
+
const cache = (typeof window.boardroomModels === "function") ? window.boardroomModels() : null;
|
|
614
|
+
if (cache && typeof cache.defaultModelV === "string" && cache.defaultModelV) {
|
|
615
|
+
modelV = cache.defaultModelV;
|
|
616
|
+
} else if (cache && Array.isArray(cache.reachable) && cache.reachable.length > 0) {
|
|
617
|
+
modelV = cache.reachable[0].modelV;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Avatar → data URL. If the user never clicked "regenerate", we
|
|
621
|
+
// build one off the form values now so the agent has a real face.
|
|
622
|
+
let avatarSeed = avatarState.seed;
|
|
623
|
+
let avatarRoll = avatarState.roll || 1;
|
|
624
|
+
if (avatarState.placeholder) {
|
|
625
|
+
avatarSeed = (name + "|" + bio) || "anon";
|
|
626
|
+
avatarRoll = 1;
|
|
627
|
+
}
|
|
628
|
+
const svg = generateAvatar(avatarSeed + "::" + avatarRoll);
|
|
629
|
+
const avatarPath = "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
630
|
+
|
|
631
|
+
// Lock the button while the request is in flight.
|
|
632
|
+
const orig = create.textContent;
|
|
633
|
+
create.disabled = true;
|
|
634
|
+
create.textContent = "[ creating… ]";
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const res = await fetch("/api/agents", {
|
|
638
|
+
method: "POST",
|
|
639
|
+
headers: { "content-type": "application/json" },
|
|
640
|
+
body: JSON.stringify({ name, bio, instruction, modelV, avatarPath }),
|
|
641
|
+
});
|
|
642
|
+
if (!res.ok) {
|
|
643
|
+
const err = await res.json().catch(() => ({}));
|
|
644
|
+
throw new Error(err.error || res.statusText);
|
|
645
|
+
}
|
|
646
|
+
const created = await res.json();
|
|
647
|
+
// Refresh app.agents so the sidebar + agentsById register the
|
|
648
|
+
// new director immediately. Falls back gracefully if app isn't
|
|
649
|
+
// booted (shouldn't happen in normal flows).
|
|
650
|
+
if (window.app && typeof window.app.refreshAgents === "function") {
|
|
651
|
+
await window.app.refreshAgents();
|
|
652
|
+
}
|
|
653
|
+
// Hand the new agent's id to anyone watching for the event.
|
|
654
|
+
try {
|
|
655
|
+
window.dispatchEvent(new CustomEvent("boardroom:agent-created", { detail: created }));
|
|
656
|
+
} catch (_) { /* */ }
|
|
657
|
+
close();
|
|
658
|
+
} catch (e) {
|
|
659
|
+
alert("Couldn't create the director: " + (e && e.message ? e.message : e));
|
|
660
|
+
create.disabled = false;
|
|
661
|
+
create.textContent = orig;
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Public API
|
|
667
|
+
window.openNewAgent = function () { if (!overlay) init(); open(); };
|
|
668
|
+
window.closeNewAgent = close;
|
|
669
|
+
|
|
670
|
+
if (document.readyState === "loading") {
|
|
671
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
672
|
+
} else {
|
|
673
|
+
init();
|
|
674
|
+
}
|
|
675
|
+
})();
|