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,782 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════
|
|
2
|
+
ONBOARDING · first-run wizard
|
|
3
|
+
═══════════════════════════════════════════
|
|
4
|
+
Boots before app.init: detects first-run state, blocks the dashboard
|
|
5
|
+
until the user has a name + a configured key. After completion, marks
|
|
6
|
+
localStorage["boardroom.onboarded"] so subsequent boots skip.
|
|
7
|
+
|
|
8
|
+
Steps:
|
|
9
|
+
0 welcome / name
|
|
10
|
+
1 theme
|
|
11
|
+
2 api key (OpenRouter, the simplest path)
|
|
12
|
+
3 done · choose to seed a demo room or convene fresh
|
|
13
|
+
|
|
14
|
+
Each step persists immediately (PUT /api/prefs · PUT /api/keys/openrouter).
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
const ONBOARDED_KEY = "boardroom.onboarded";
|
|
18
|
+
|
|
19
|
+
const THEMES = [
|
|
20
|
+
{ slug: "regent", name: "Regent", desc: "warm gold on dark · default",
|
|
21
|
+
swatches: ["#0A0A0A","#131312","#C9A46B","#9A7B40","#A57843","#B5706A","#6A9B97","#C8C5BE"] },
|
|
22
|
+
{ slug: "eastwood", name: "Eastwood", desc: "calm forest green",
|
|
23
|
+
swatches: ["#0A0A0A","#131312","#6FB572","#427A48","#B59560","#B5706A","#6A9B97","#C8C5BE"] },
|
|
24
|
+
{ slug: "atrium", name: "Atrium", desc: "warm paper · daylight",
|
|
25
|
+
swatches: ["#FBFBF7","#F4F2EC","#2E7D32","#1B5E20","#A86C2A","#A8403D","#2E7D7A","#1F1E1A"] },
|
|
26
|
+
{ slug: "pinterest", name: "Pinterest", desc: "clean white · red accent",
|
|
27
|
+
swatches: ["#FFFFFF","#FAFAFA","#E60023","#AD081B","#F4A100","#E60023","#2E7D7A","#111111"] },
|
|
28
|
+
{ slug: "apple", name: "Apple", desc: "pure white · system blue",
|
|
29
|
+
swatches: ["#FFFFFF","#F5F5F7","#0071E3","#0051A8","#FF9500","#FF3B30","#5AC8FA","#1D1D1F"] },
|
|
30
|
+
{ slug: "alanpeabody", name: "Alan Peabody", desc: "cool blue · git-green accents",
|
|
31
|
+
swatches: ["#0E1419","#131A21","#6BAFE0","#3F7AAA","#C8A463","#D67373","#6FB5A8","#C8D0DA"] },
|
|
32
|
+
{ slug: "amuse", name: "Amuse", desc: "magenta + cyan · playful",
|
|
33
|
+
swatches: ["#1A0E14","#21121A","#D67BC0","#9C4884","#DCBE5D","#E07F84","#6FBFC2","#DECBD2"] },
|
|
34
|
+
{ slug: "jtriley", name: "JTriley", desc: "bright lime + yellow",
|
|
35
|
+
swatches: ["#0A0F0A","#131914","#B5DA40","#6E8E27","#F0CC4E","#D67762","#6FBE9A","#C8D6BE"] },
|
|
36
|
+
{ slug: "nebirhos", name: "Nebirhos", desc: "teal · warm orange",
|
|
37
|
+
swatches: ["#0A1414","#11201F","#5EB1A6","#357770","#DD9258","#D87060","#6FBEC2","#B8D4D0"] },
|
|
38
|
+
{ slug: "wedisagree", name: "We Disagree", desc: "argumentative orange",
|
|
39
|
+
swatches: ["#14110E","#1F1A14","#DD7B40","#A8521E","#E6B872","#E26060","#6FB28A","#D8CBBC"] }
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const DEMO_AGENTS = ["socrates", "first-principles", "value-investor"];
|
|
43
|
+
const NAMES = {
|
|
44
|
+
"socrates": "Socrates",
|
|
45
|
+
"first-principles": "First Principles",
|
|
46
|
+
"value-investor": "Value Investor",
|
|
47
|
+
"user-empathy": "User-Empathy",
|
|
48
|
+
"long-horizon": "Long Horizon",
|
|
49
|
+
"phenomenologist": "Phenomenologist",
|
|
50
|
+
};
|
|
51
|
+
// Recommended seed questions for first-run users. Concrete, current
|
|
52
|
+
// 2026-shaped scenarios — the kind of decision a builder/operator
|
|
53
|
+
// actually loses sleep over — so the boardroom's value (three lenses
|
|
54
|
+
// pressuring one decision) lands in the first turn. Each cast is
|
|
55
|
+
// hand-picked so the lenses fit the question.
|
|
56
|
+
const STARTER_QUESTIONS = [
|
|
57
|
+
{
|
|
58
|
+
tag: "// ai-startup",
|
|
59
|
+
text: "OpenAI and Anthropic keep launching everything — does my AI startup still have a real shot in 2026?",
|
|
60
|
+
hint: "skeptic + pattern hunter + causal reasoner stress-test the moat thesis",
|
|
61
|
+
tone: "debate",
|
|
62
|
+
intensity: "sharp",
|
|
63
|
+
briefStyle: "auto",
|
|
64
|
+
agents: ["socrates", "value-investor", "first-principles"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
tag: "// quit-tech",
|
|
68
|
+
text: "$300K saved, senior eng job at a Big Tech — quit now to build, or wait two more years?",
|
|
69
|
+
hint: "long-horizon vs. value-investor on a real fork in your career",
|
|
70
|
+
tone: "constructive",
|
|
71
|
+
intensity: "calm",
|
|
72
|
+
briefStyle: "auto",
|
|
73
|
+
agents: ["long-horizon", "value-investor", "socrates"],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
tag: "// pricing",
|
|
77
|
+
text: "$49/mo B2B SaaS with sticky enterprise users — are we leaving 5–10x on the table by not raising to $499?",
|
|
78
|
+
hint: "is your unit economics actually consumer-priced for an enterprise problem?",
|
|
79
|
+
tone: "debate",
|
|
80
|
+
intensity: "sharp",
|
|
81
|
+
briefStyle: "auto",
|
|
82
|
+
agents: ["value-investor", "user-empathy", "socrates"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
tag: "// pivot",
|
|
86
|
+
text: "Six months of runway, real users but flat MRR — pivot the product, hold the line, or shut it down?",
|
|
87
|
+
hint: "no-mercy room: force the load-bearing claim into the open",
|
|
88
|
+
tone: "no-mercy",
|
|
89
|
+
intensity: "brutal",
|
|
90
|
+
briefStyle: "auto",
|
|
91
|
+
agents: ["socrates", "first-principles", "long-horizon"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
tag: "// agent-stack",
|
|
95
|
+
text: "Cursor + Claude Code + ChatGPT — am I overpaying for overlap, or is this the right mix for 2026?",
|
|
96
|
+
hint: "first-principles + user-empathy figure out what each tool actually buys you",
|
|
97
|
+
tone: "constructive",
|
|
98
|
+
intensity: "sharp",
|
|
99
|
+
briefStyle: "auto",
|
|
100
|
+
agents: ["first-principles", "user-empathy", "value-investor"],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
// Make available for app.js's empty-state.
|
|
104
|
+
try { window.BOARDROOM_STARTERS = STARTER_QUESTIONS; } catch (e) {}
|
|
105
|
+
|
|
106
|
+
function escape(s) {
|
|
107
|
+
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
108
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
109
|
+
}[c]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Provider catalogue ─────────────────────────────────
|
|
113
|
+
// Model providers shown on step 2. OpenRouter leads — it's the
|
|
114
|
+
// universal router that unlocks every model from a single key, so
|
|
115
|
+
// it's the lowest-friction first stop for new users. Anthropic
|
|
116
|
+
// (Claude) is temporarily withheld; bring it back when the
|
|
117
|
+
// direct-Anthropic flow is ready.
|
|
118
|
+
// `slug` matches /api/keys/{slug} on the backend.
|
|
119
|
+
const KEY_PROVIDERS = [
|
|
120
|
+
{
|
|
121
|
+
slug: "openrouter",
|
|
122
|
+
label: "OpenRouter",
|
|
123
|
+
sub: "all-in-one router",
|
|
124
|
+
placeholder: "sk-or-v1-…",
|
|
125
|
+
help: "openrouter.ai/keys",
|
|
126
|
+
helpUrl: "https://openrouter.ai/keys",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
slug: "openai",
|
|
130
|
+
label: "ChatGPT",
|
|
131
|
+
sub: "OpenAI",
|
|
132
|
+
placeholder: "sk-…",
|
|
133
|
+
help: "platform.openai.com",
|
|
134
|
+
helpUrl: "https://platform.openai.com/api-keys",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
slug: "google",
|
|
138
|
+
label: "Gemini",
|
|
139
|
+
sub: "Google AI Studio",
|
|
140
|
+
placeholder: "AIza…",
|
|
141
|
+
help: "aistudio.google.com",
|
|
142
|
+
helpUrl: "https://aistudio.google.com/apikey",
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// ── State ──────────────────────────────────────────────
|
|
147
|
+
let currentStep = 0;
|
|
148
|
+
let prefsCache = { name: "", intro: "", theme: "regent" };
|
|
149
|
+
/** Per-provider configured flag · true when /api/keys reports the
|
|
150
|
+
* provider's row as configured. Drives the green dot on each tab
|
|
151
|
+
* and the Next-button enable state. */
|
|
152
|
+
let providerConfigured = {
|
|
153
|
+
openrouter: false,
|
|
154
|
+
openai: false,
|
|
155
|
+
google: false,
|
|
156
|
+
};
|
|
157
|
+
/** Currently-selected provider tab on step 2. Defaults to the first
|
|
158
|
+
* configured provider (sticky after partial completion) or to
|
|
159
|
+
* OpenRouter for fresh users — the lowest-friction starting point. */
|
|
160
|
+
let activeProvider = "openrouter";
|
|
161
|
+
let overlay = null;
|
|
162
|
+
|
|
163
|
+
/** True when ANY model provider has a key. Replaces the legacy
|
|
164
|
+
* single-provider flag; downstream callers that just need to know
|
|
165
|
+
* "can the user use the product?" use this. */
|
|
166
|
+
function anyKeyConfigured() {
|
|
167
|
+
return Object.values(providerConfigured).some(Boolean);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Persistence ────────────────────────────────────────
|
|
171
|
+
async function loadInitial() {
|
|
172
|
+
try {
|
|
173
|
+
const [prefsRes, keysRes] = await Promise.all([
|
|
174
|
+
fetch("/api/prefs"),
|
|
175
|
+
fetch("/api/keys"),
|
|
176
|
+
]);
|
|
177
|
+
if (prefsRes.ok) {
|
|
178
|
+
const p = await prefsRes.json();
|
|
179
|
+
prefsCache = { name: p.name === "You" ? "" : (p.name || ""), intro: p.intro || "", theme: p.theme || "regent" };
|
|
180
|
+
}
|
|
181
|
+
if (keysRes.ok) {
|
|
182
|
+
const k = await keysRes.json();
|
|
183
|
+
// Reset then patch by row · the API returns one row per
|
|
184
|
+
// configured provider, with the `provider` slug + `configured`
|
|
185
|
+
// boolean. Unknown providers (e.g. brave) get ignored.
|
|
186
|
+
for (const slug of Object.keys(providerConfigured)) {
|
|
187
|
+
providerConfigured[slug] = false;
|
|
188
|
+
}
|
|
189
|
+
for (const row of (k.keys || [])) {
|
|
190
|
+
if (row && typeof row.provider === "string" && row.provider in providerConfigured) {
|
|
191
|
+
providerConfigured[row.provider] = !!row.configured;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Sticky default · land on the first provider that's already
|
|
195
|
+
// configured so re-entering onboarding feels continuous.
|
|
196
|
+
const firstConfigured = KEY_PROVIDERS.find((p) => providerConfigured[p.slug]);
|
|
197
|
+
if (firstConfigured) activeProvider = firstConfigured.slug;
|
|
198
|
+
}
|
|
199
|
+
} catch (e) { /* keep defaults */ }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function shouldShow() {
|
|
203
|
+
// Server is authoritative · localStorage is just an optimization
|
|
204
|
+
// marker. If the server reports no key configured, we MUST show
|
|
205
|
+
// onboarding even when localStorage thinks we've onboarded — the
|
|
206
|
+
// most common reason for the mismatch is a DB wipe / fresh install
|
|
207
|
+
// on a browser that previously onboarded a different DB.
|
|
208
|
+
await loadInitial();
|
|
209
|
+
if (!anyKeyConfigured()) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
// Has at least one key. Mark localStorage so we skip the server
|
|
213
|
+
// roundtrip on subsequent boots that don't change key state.
|
|
214
|
+
try { localStorage.setItem(ONBOARDED_KEY, "true"); } catch (e) {}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function saveName(name) {
|
|
219
|
+
const v = (name || "").trim();
|
|
220
|
+
prefsCache.name = v;
|
|
221
|
+
try {
|
|
222
|
+
await fetch("/api/prefs", {
|
|
223
|
+
method: "PUT",
|
|
224
|
+
headers: { "content-type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ name: v || "You" }),
|
|
226
|
+
});
|
|
227
|
+
} catch (e) { /* */ }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function applyThemeImmediate(slug) {
|
|
231
|
+
const theme = slug || "regent";
|
|
232
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
233
|
+
try { localStorage.setItem("boardroom.theme", theme); } catch (e) {}
|
|
234
|
+
prefsCache.theme = theme;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function saveTheme(slug) {
|
|
238
|
+
applyThemeImmediate(slug);
|
|
239
|
+
try {
|
|
240
|
+
await fetch("/api/prefs", {
|
|
241
|
+
method: "PUT",
|
|
242
|
+
headers: { "content-type": "application/json" },
|
|
243
|
+
body: JSON.stringify({ theme: slug }),
|
|
244
|
+
});
|
|
245
|
+
} catch (e) { /* */ }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Save a key for a given provider. Backend endpoint is consistent:
|
|
249
|
+
* PUT /api/keys/{provider} with { key: "...", makeDefault: true }.
|
|
250
|
+
* The `makeDefault` flag tells the server "the user just picked
|
|
251
|
+
* this provider as their primary in onboarding" — it flips
|
|
252
|
+
* prefs.defaultModelV to the new provider's flagship and force-
|
|
253
|
+
* switches every existing agent to that primary, even ones that
|
|
254
|
+
* were still reachable on a different carrier. Without it, a
|
|
255
|
+
* user who had OpenRouter configured before and now picks Gemini
|
|
256
|
+
* in onboarding would see the chair stay on opus-4-7 (reachable
|
|
257
|
+
* via OR) instead of swinging to gemini-3-flash.
|
|
258
|
+
*
|
|
259
|
+
* Empty input doesn't fire a request — it's a no-op (DELETE flow
|
|
260
|
+
* lives in user-settings, not in onboarding). */
|
|
261
|
+
async function deleteProviderKey(provider) {
|
|
262
|
+
try {
|
|
263
|
+
await fetch("/api/keys/" + encodeURIComponent(provider), { method: "DELETE" });
|
|
264
|
+
} catch (e) { /* */ }
|
|
265
|
+
providerConfigured[provider] = false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function saveProviderKey(provider, value) {
|
|
269
|
+
const trimmed = (value || "").trim();
|
|
270
|
+
if (!trimmed) return false;
|
|
271
|
+
try {
|
|
272
|
+
const r = await fetch("/api/keys/" + encodeURIComponent(provider), {
|
|
273
|
+
method: "PUT",
|
|
274
|
+
headers: { "content-type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ key: trimmed, makeDefault: true }),
|
|
276
|
+
});
|
|
277
|
+
if (!r.ok) return false;
|
|
278
|
+
const data = await r.json();
|
|
279
|
+
const ok = !!data.configured;
|
|
280
|
+
providerConfigured[provider] = ok;
|
|
281
|
+
return ok;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Render ─────────────────────────────────────────────
|
|
288
|
+
function modalHTML() {
|
|
289
|
+
return `
|
|
290
|
+
<div class="onb-overlay" id="onb-overlay" role="dialog" aria-modal="true">
|
|
291
|
+
<div class="onb-modal" role="document">
|
|
292
|
+
<div class="onb-classification">
|
|
293
|
+
<span><span class="dot">●</span> first run · setup</span>
|
|
294
|
+
<span class="right">// local · ~30 seconds</span>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="onb-progress" data-onb-progress>
|
|
298
|
+
<div class="onb-dot active"></div>
|
|
299
|
+
<div class="onb-dot"></div>
|
|
300
|
+
<div class="onb-dot"></div>
|
|
301
|
+
<div class="onb-dot"></div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div class="onb-head" data-onb-head></div>
|
|
305
|
+
<div class="onb-body" data-onb-body></div>
|
|
306
|
+
|
|
307
|
+
<footer class="onb-foot">
|
|
308
|
+
<div class="onb-foot-left">
|
|
309
|
+
<button type="button" class="onb-btn" data-onb-back>[ ◂ Back ]</button>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="onb-foot-right" data-onb-actions></div>
|
|
312
|
+
</footer>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function renderStep() {
|
|
319
|
+
const head = overlay.querySelector("[data-onb-head]");
|
|
320
|
+
const body = overlay.querySelector("[data-onb-body]");
|
|
321
|
+
const back = overlay.querySelector("[data-onb-back]");
|
|
322
|
+
const actions = overlay.querySelector("[data-onb-actions]");
|
|
323
|
+
|
|
324
|
+
// Update progress dots
|
|
325
|
+
const dots = overlay.querySelectorAll(".onb-dot");
|
|
326
|
+
dots.forEach((d, i) => {
|
|
327
|
+
d.classList.toggle("active", i === currentStep);
|
|
328
|
+
d.classList.toggle("done", i < currentStep);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
back.style.visibility = currentStep === 0 ? "hidden" : "visible";
|
|
332
|
+
|
|
333
|
+
if (currentStep === 0) {
|
|
334
|
+
head.innerHTML = `
|
|
335
|
+
<div class="onb-tag">▸ welcome</div>
|
|
336
|
+
<div class="onb-title">A private boardroom for you.</div>
|
|
337
|
+
<div class="onb-deck">A board of stubborn advisors for the questions you take seriously. Not a chatbot. Three quick steps and you're in.</div>
|
|
338
|
+
`;
|
|
339
|
+
body.innerHTML = `
|
|
340
|
+
<div class="onb-field">
|
|
341
|
+
<div class="onb-field-label">What should the room call you?</div>
|
|
342
|
+
<div class="onb-input-wrap">
|
|
343
|
+
<input class="onb-input" data-onb-name maxlength="32" placeholder="e.g. Kay" value="${escape(prefsCache.name || "")}" autofocus>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="onb-field-hint">Used in the directors' system context. You can change it later in Preference.</div>
|
|
346
|
+
</div>
|
|
347
|
+
`;
|
|
348
|
+
actions.innerHTML = `<button type="button" class="onb-btn primary" data-onb-next>[ Next ▸ ]</button>`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
else if (currentStep === 1) {
|
|
352
|
+
head.innerHTML = `
|
|
353
|
+
<div class="onb-tag">▸ theme</div>
|
|
354
|
+
<div class="onb-title">Pick a palette.</div>
|
|
355
|
+
<div class="onb-deck">Applied instantly. You can change it any time in Preference → Theme.</div>
|
|
356
|
+
`;
|
|
357
|
+
const swatches = (cs) => cs.map((c) => `<span style="background:${c}"></span>`).join("");
|
|
358
|
+
body.innerHTML = `
|
|
359
|
+
<div class="onb-theme-grid">
|
|
360
|
+
${THEMES.map((t) => `
|
|
361
|
+
<a href="#" class="onb-theme${t.slug === prefsCache.theme ? " active" : ""}" data-onb-theme="${t.slug}">
|
|
362
|
+
<span class="onb-theme-swatch">${swatches(t.swatches)}</span>
|
|
363
|
+
<span>
|
|
364
|
+
<span class="onb-theme-name">${escape(t.name)}</span>
|
|
365
|
+
<div class="onb-theme-desc">${escape(t.desc)}</div>
|
|
366
|
+
</span>
|
|
367
|
+
<span></span>
|
|
368
|
+
</a>
|
|
369
|
+
`).join("")}
|
|
370
|
+
</div>
|
|
371
|
+
`;
|
|
372
|
+
actions.innerHTML = `<button type="button" class="onb-btn primary" data-onb-next>[ Next ▸ ]</button>`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
else if (currentStep === 2) {
|
|
376
|
+
const active = KEY_PROVIDERS.find((p) => p.slug === activeProvider) || KEY_PROVIDERS[0];
|
|
377
|
+
const tabs = KEY_PROVIDERS.map((p) => {
|
|
378
|
+
const isActive = p.slug === active.slug;
|
|
379
|
+
const isConfigured = providerConfigured[p.slug];
|
|
380
|
+
return `
|
|
381
|
+
<button type="button"
|
|
382
|
+
class="onb-key-tab${isActive ? " active" : ""}${isConfigured ? " configured" : ""}"
|
|
383
|
+
data-onb-provider="${escape(p.slug)}">
|
|
384
|
+
<span class="onb-key-tab-label">${escape(p.label)}</span>
|
|
385
|
+
<span class="onb-key-tab-sub">${escape(p.sub)}</span>
|
|
386
|
+
${isConfigured ? `<span class="onb-key-tab-dot" title="configured">●</span>` : ""}
|
|
387
|
+
</button>
|
|
388
|
+
`;
|
|
389
|
+
}).join("");
|
|
390
|
+
|
|
391
|
+
const inputValue = ""; // never echo back the saved key
|
|
392
|
+
const status = providerConfigured[active.slug]
|
|
393
|
+
? `<div class="onb-key-status ok">● ${escape(active.label)} key configured</div>`
|
|
394
|
+
: "";
|
|
395
|
+
|
|
396
|
+
head.innerHTML = `
|
|
397
|
+
<div class="onb-tag">▸ api key</div>
|
|
398
|
+
<div class="onb-title">Bring your own key.</div>
|
|
399
|
+
<div class="onb-deck">Boardroom runs against your model provider. Pick one below — any single key is enough to get started. Stored locally on this machine, never uploaded.</div>
|
|
400
|
+
`;
|
|
401
|
+
body.innerHTML = `
|
|
402
|
+
<div class="onb-key-tabs">${tabs}</div>
|
|
403
|
+
<div class="onb-field">
|
|
404
|
+
<div class="onb-field-label" data-onb-field-label>${escape(active.label)} API key</div>
|
|
405
|
+
<div class="onb-input-wrap">
|
|
406
|
+
<input class="onb-input" data-onb-key type="password" placeholder="${escape(active.placeholder)}" autocomplete="off" spellcheck="false" value="${escape(inputValue)}">
|
|
407
|
+
<button type="button" class="onb-input-reveal" data-onb-reveal aria-label="Show key" aria-pressed="false">show</button>
|
|
408
|
+
</div>
|
|
409
|
+
${status}
|
|
410
|
+
<div class="onb-field-hint">
|
|
411
|
+
Don't have one? <a href="${escape(active.helpUrl)}" target="_blank" rel="noopener" data-onb-help-link>Generate at ${escape(active.help)} →</a>
|
|
412
|
+
<br>
|
|
413
|
+
You can add or change provider keys later in Preferences.
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
`;
|
|
417
|
+
const enableNext = anyKeyConfigured();
|
|
418
|
+
actions.innerHTML = `<button type="button" class="onb-btn primary" data-onb-next ${enableNext ? "" : "disabled"}>[ Next ▸ ]</button>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
else if (currentStep === 3) {
|
|
422
|
+
head.innerHTML = `
|
|
423
|
+
<div class="onb-tag">▸ done · pick a starting question</div>
|
|
424
|
+
<div class="onb-title">${prefsCache.name ? escape(prefsCache.name) + ", y" : "Y"}our boardroom is open.</div>
|
|
425
|
+
<div class="onb-deck">Pick a starter to take one full journey: convene a room, watch three directors stress-test the question, then file the brief. Or convene your own from scratch.</div>
|
|
426
|
+
`;
|
|
427
|
+
const cards = STARTER_QUESTIONS.map((q, idx) => {
|
|
428
|
+
const cast = (q.agents || []).map((slug) => `
|
|
429
|
+
<span class="onb-starter-agent" title="${escape(NAMES[slug] || slug)}">
|
|
430
|
+
<img class="onb-starter-av" src="avatars/${escape(slug)}.svg" alt="${escape(NAMES[slug] || slug)}">
|
|
431
|
+
<span class="onb-starter-name">${escape(NAMES[slug] || slug)}</span>
|
|
432
|
+
</span>
|
|
433
|
+
`).join("");
|
|
434
|
+
return `
|
|
435
|
+
<div class="onb-starter" data-onb-action="starter" data-onb-starter-idx="${idx}">
|
|
436
|
+
<div class="onb-starter-tag">${escape(q.tag)}</div>
|
|
437
|
+
<div class="onb-starter-main">
|
|
438
|
+
<div class="onb-starter-text">${escape(q.text)}</div>
|
|
439
|
+
<div class="onb-starter-hint">${escape(q.hint)}</div>
|
|
440
|
+
<div class="onb-starter-meta">
|
|
441
|
+
<span class="meta-tag tag-tone"><span class="k">tone</span><span class="v">${escape(q.tone)}</span></span>
|
|
442
|
+
<span class="meta-tag tag-intensity"><span class="k">intensity</span><span class="v">${escape(q.intensity)}</span></span>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
<!-- data-no-agent-overlay · these avatars are decorative
|
|
446
|
+
cast indicators, not profile triggers. agent-overlay.js
|
|
447
|
+
honours this attribute on autotag + click. -->
|
|
448
|
+
<div class="onb-starter-cast" data-no-agent-overlay>${cast}</div>
|
|
449
|
+
<button type="button" class="onb-starter-start" data-onb-action="starter" data-onb-starter-idx="${idx}">
|
|
450
|
+
<span class="onb-starter-start-arrow">▶</span>
|
|
451
|
+
<span class="onb-starter-start-label">Start</span>
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
`;
|
|
455
|
+
}).join("");
|
|
456
|
+
body.innerHTML = `
|
|
457
|
+
<div class="onb-starters">${cards}</div>
|
|
458
|
+
<div class="onb-final-divider"><span>or</span></div>
|
|
459
|
+
<div class="onb-final">
|
|
460
|
+
<button type="button" class="onb-final-card" data-onb-action="convene">
|
|
461
|
+
<div class="onb-final-mark">▸ CONVENE</div>
|
|
462
|
+
<div class="onb-final-title">Convene your own</div>
|
|
463
|
+
<div class="onb-final-deck">Pick directors, write your own question, start the room.</div>
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
`;
|
|
467
|
+
actions.innerHTML = `<button type="button" class="onb-btn" data-onb-action="skip">[ I'll explore on my own ]</button>`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Actions ────────────────────────────────────────────
|
|
472
|
+
function next() {
|
|
473
|
+
if (currentStep === 0) {
|
|
474
|
+
const v = overlay.querySelector("[data-onb-name]").value;
|
|
475
|
+
void saveName(v);
|
|
476
|
+
} else if (currentStep === 2) {
|
|
477
|
+
// Gated · the user must have AT LEAST ONE provider key
|
|
478
|
+
// configured (any of OpenRouter / OpenAI / Google) before
|
|
479
|
+
// they can proceed to the starter screen.
|
|
480
|
+
if (!anyKeyConfigured()) return;
|
|
481
|
+
}
|
|
482
|
+
currentStep = Math.min(currentStep + 1, 3);
|
|
483
|
+
renderStep();
|
|
484
|
+
}
|
|
485
|
+
function back() {
|
|
486
|
+
currentStep = Math.max(currentStep - 1, 0);
|
|
487
|
+
renderStep();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Switch the active provider tab on step 2. Inline DOM update —
|
|
491
|
+
* NOT a full re-render — so the single input element survives the
|
|
492
|
+
* switch and whatever the user has typed stays in the field. The
|
|
493
|
+
* tabs above are just a label declaring what the input is for. */
|
|
494
|
+
function selectProvider(slug) {
|
|
495
|
+
if (!(slug in providerConfigured)) return;
|
|
496
|
+
if (activeProvider === slug) return;
|
|
497
|
+
activeProvider = slug;
|
|
498
|
+
|
|
499
|
+
const active = KEY_PROVIDERS.find((p) => p.slug === slug) || KEY_PROVIDERS[0];
|
|
500
|
+
|
|
501
|
+
overlay.querySelectorAll(".onb-key-tab").forEach((el) => {
|
|
502
|
+
el.classList.toggle("active", el.getAttribute("data-onb-provider") === slug);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const fieldLabel = overlay.querySelector("[data-onb-field-label]");
|
|
506
|
+
if (fieldLabel) fieldLabel.textContent = `${active.label} API key`;
|
|
507
|
+
|
|
508
|
+
const input = overlay.querySelector("[data-onb-key]");
|
|
509
|
+
if (input) input.setAttribute("placeholder", active.placeholder);
|
|
510
|
+
|
|
511
|
+
const helpLink = overlay.querySelector("[data-onb-help-link]");
|
|
512
|
+
if (helpLink) {
|
|
513
|
+
helpLink.setAttribute("href", active.helpUrl);
|
|
514
|
+
helpLink.textContent = `Generate at ${active.help} →`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Status pill reflects the now-active provider's saved state.
|
|
518
|
+
const wrap = input ? input.closest(".onb-field") : null;
|
|
519
|
+
const existing = wrap ? wrap.querySelector(".onb-key-status") : null;
|
|
520
|
+
if (existing) existing.remove();
|
|
521
|
+
if (providerConfigured[slug] && wrap) {
|
|
522
|
+
wrap.querySelector(".onb-input-wrap").insertAdjacentHTML(
|
|
523
|
+
"afterend",
|
|
524
|
+
`<div class="onb-key-status ok">● ${escape(active.label)} key configured</div>`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function trySaveKey(value) {
|
|
530
|
+
const provider = activeProvider;
|
|
531
|
+
const label = (KEY_PROVIDERS.find((p) => p.slug === provider) || {}).label || provider;
|
|
532
|
+
const status = overlay.querySelector(".onb-key-status");
|
|
533
|
+
const nextBtn = overlay.querySelector("[data-onb-next]");
|
|
534
|
+
if (status) status.outerHTML = `<div class="onb-key-status warn">○ checking…</div>`;
|
|
535
|
+
const ok = await saveProviderKey(provider, value);
|
|
536
|
+
const fresh = overlay.querySelector(".onb-key-status, [class^=onb-key-status]");
|
|
537
|
+
if (ok) {
|
|
538
|
+
// Single-provider invariant · the onboarding step has one input;
|
|
539
|
+
// the tabs above only declare which provider that input is for.
|
|
540
|
+
// After a successful save, retire any other provider keys so the
|
|
541
|
+
// user leaves onboarding with exactly one configured carrier.
|
|
542
|
+
for (const slug of Object.keys(providerConfigured)) {
|
|
543
|
+
if (slug === provider || !providerConfigured[slug]) continue;
|
|
544
|
+
await deleteProviderKey(slug);
|
|
545
|
+
const otherTab = overlay.querySelector(`.onb-key-tab[data-onb-provider="${slug}"]`);
|
|
546
|
+
if (otherTab) {
|
|
547
|
+
otherTab.classList.remove("configured");
|
|
548
|
+
otherTab.querySelector(".onb-key-tab-dot")?.remove();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (fresh) fresh.outerHTML = `<div class="onb-key-status ok">● ${escape(label)} key configured</div>`;
|
|
552
|
+
else {
|
|
553
|
+
const wrap = overlay.querySelector("[data-onb-key]")?.closest(".onb-field");
|
|
554
|
+
if (wrap) wrap.querySelector(".onb-input-wrap").insertAdjacentHTML(
|
|
555
|
+
"afterend",
|
|
556
|
+
`<div class="onb-key-status ok">● ${escape(label)} key configured</div>`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
// Update the matching tab's "configured" state inline rather
|
|
560
|
+
// than re-rendering the whole step (would steal input focus).
|
|
561
|
+
const tab = overlay.querySelector(`.onb-key-tab[data-onb-provider="${provider}"]`);
|
|
562
|
+
if (tab && !tab.classList.contains("configured")) {
|
|
563
|
+
tab.classList.add("configured");
|
|
564
|
+
tab.insertAdjacentHTML("beforeend", `<span class="onb-key-tab-dot" title="configured">●</span>`);
|
|
565
|
+
}
|
|
566
|
+
if (nextBtn) nextBtn.disabled = false;
|
|
567
|
+
} else {
|
|
568
|
+
if (fresh) fresh.outerHTML = `<div class="onb-key-status error">✗ not saved</div>`;
|
|
569
|
+
if (nextBtn) nextBtn.disabled = !anyKeyConfigured();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function complete(then) {
|
|
574
|
+
try { localStorage.setItem(ONBOARDED_KEY, "true"); } catch (e) {}
|
|
575
|
+
document.body.classList.remove("onb-locked");
|
|
576
|
+
overlay.classList.remove("open");
|
|
577
|
+
setTimeout(() => {
|
|
578
|
+
overlay.remove();
|
|
579
|
+
overlay = null;
|
|
580
|
+
}, 220);
|
|
581
|
+
// Sync every client-side cache the server just mutated. The PUT
|
|
582
|
+
// /api/keys/:provider call with `makeDefault:true` did three
|
|
583
|
+
// server-side writes: the key row, prefs.defaultModelV (flipped
|
|
584
|
+
// to the new carrier's primary), and every agent's modelV (via
|
|
585
|
+
// reconcile). Each of those has its own client cache · refresh
|
|
586
|
+
// them all so user-settings, sidebar, and pickers reflect the
|
|
587
|
+
// new state immediately, no full reload needed. Wait on all
|
|
588
|
+
// three before running the continuation so any post-onboarding
|
|
589
|
+
// action (createRoom, open convene) sees fresh state.
|
|
590
|
+
const continuation = typeof then === "function" ? then : null;
|
|
591
|
+
const refreshes = [];
|
|
592
|
+
if (window.app && typeof window.app.refreshKeys === "function") {
|
|
593
|
+
refreshes.push(Promise.resolve(window.app.refreshKeys()).catch(() => {}));
|
|
594
|
+
}
|
|
595
|
+
if (window.app && typeof window.app.refreshAgents === "function") {
|
|
596
|
+
refreshes.push(Promise.resolve(window.app.refreshAgents()).catch(() => {}));
|
|
597
|
+
}
|
|
598
|
+
if (typeof window.boardroomModelsRefresh === "function") {
|
|
599
|
+
refreshes.push(Promise.resolve(window.boardroomModelsRefresh()).catch(() => {}));
|
|
600
|
+
}
|
|
601
|
+
Promise.all(refreshes).finally(() => { if (continuation) continuation(); });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function createDemoRoom(spec) {
|
|
605
|
+
if (!window.app || typeof window.app.createRoom !== "function") {
|
|
606
|
+
// app hasn't booted yet — wait briefly then retry once.
|
|
607
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
608
|
+
}
|
|
609
|
+
// Accept either a full starter spec object (preferred), a plain subject
|
|
610
|
+
// string (legacy), or nothing (default to the first starter).
|
|
611
|
+
let s = spec;
|
|
612
|
+
if (!s) s = STARTER_QUESTIONS[0];
|
|
613
|
+
else if (typeof s === "string") {
|
|
614
|
+
const text = s.trim();
|
|
615
|
+
s = STARTER_QUESTIONS.find((q) => q.text === text) || { text };
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
await window.app.createRoom({
|
|
619
|
+
subject: s.text,
|
|
620
|
+
agentIds: s.agents && s.agents.length >= 2 ? s.agents : DEMO_AGENTS,
|
|
621
|
+
mode: s.tone || "constructive",
|
|
622
|
+
intensity: s.intensity || "sharp",
|
|
623
|
+
briefStyle: s.briefStyle || "auto",
|
|
624
|
+
});
|
|
625
|
+
} catch (e) {
|
|
626
|
+
alert("Couldn't create a starter room: " + (e && e.message ? e.message : e));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function openConveneAfter() {
|
|
631
|
+
setTimeout(() => {
|
|
632
|
+
// Convene-overlay was retired in favour of the inline composer.
|
|
633
|
+
// Fall back to closing the active room so the composer shows; if
|
|
634
|
+
// app.closeRoom isn't ready yet, no-op (the user is on the
|
|
635
|
+
// dashboard already and will see it on next interaction).
|
|
636
|
+
try {
|
|
637
|
+
if (window.app && typeof window.app.closeRoom === "function") {
|
|
638
|
+
window.app.closeRoom();
|
|
639
|
+
} else if (typeof window.openConveneOverlay === "function") {
|
|
640
|
+
window.openConveneOverlay();
|
|
641
|
+
}
|
|
642
|
+
} catch { /* ignore */ }
|
|
643
|
+
}, 250);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ── Wiring ─────────────────────────────────────────────
|
|
647
|
+
function wire() {
|
|
648
|
+
overlay.addEventListener("click", async (e) => {
|
|
649
|
+
const t = e.target;
|
|
650
|
+
|
|
651
|
+
if (t.closest("[data-onb-next]")) {
|
|
652
|
+
e.preventDefault();
|
|
653
|
+
next();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (t.closest("[data-onb-back]")) {
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
back();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const themeChoice = t.closest("[data-onb-theme]");
|
|
662
|
+
if (themeChoice) {
|
|
663
|
+
e.preventDefault();
|
|
664
|
+
const slug = themeChoice.getAttribute("data-onb-theme");
|
|
665
|
+
overlay.querySelectorAll(".onb-theme").forEach((el) => el.classList.remove("active"));
|
|
666
|
+
themeChoice.classList.add("active");
|
|
667
|
+
void saveTheme(slug);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const providerChoice = t.closest("[data-onb-provider]");
|
|
671
|
+
if (providerChoice) {
|
|
672
|
+
e.preventDefault();
|
|
673
|
+
const slug = providerChoice.getAttribute("data-onb-provider");
|
|
674
|
+
if (slug) selectProvider(slug);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const reveal = t.closest("[data-onb-reveal]");
|
|
678
|
+
if (reveal) {
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
const input = overlay.querySelector("[data-onb-key]");
|
|
681
|
+
if (!input) return;
|
|
682
|
+
const showing = input.getAttribute("type") === "text";
|
|
683
|
+
input.setAttribute("type", showing ? "password" : "text");
|
|
684
|
+
reveal.setAttribute("aria-pressed", showing ? "false" : "true");
|
|
685
|
+
reveal.setAttribute("aria-label", showing ? "Show key" : "Hide key");
|
|
686
|
+
reveal.textContent = showing ? "show" : "hide";
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const action = t.closest("[data-onb-action]");
|
|
690
|
+
if (action) {
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
e.stopPropagation();
|
|
693
|
+
const a = action.getAttribute("data-onb-action");
|
|
694
|
+
if (a === "starter") {
|
|
695
|
+
const idx = parseInt(action.getAttribute("data-onb-starter-idx") || "-1", 10);
|
|
696
|
+
const spec = Number.isFinite(idx) ? STARTER_QUESTIONS[idx] : null;
|
|
697
|
+
complete(() => { void createDemoRoom(spec); });
|
|
698
|
+
} else if (a === "demo") {
|
|
699
|
+
complete(() => { void createDemoRoom(); });
|
|
700
|
+
} else if (a === "convene") {
|
|
701
|
+
complete(() => openConveneAfter());
|
|
702
|
+
} else if (a === "skip") {
|
|
703
|
+
complete();
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Live updates for the API key field — debounce, validate, save.
|
|
710
|
+
overlay.addEventListener("input", (e) => {
|
|
711
|
+
const t = e.target;
|
|
712
|
+
if (t.matches("[data-onb-name]")) {
|
|
713
|
+
prefsCache.name = t.value;
|
|
714
|
+
} else if (t.matches("[data-onb-key]")) {
|
|
715
|
+
clearTimeout(t.__onbTimer);
|
|
716
|
+
t.__onbTimer = setTimeout(() => trySaveKey(t.value), 280);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Enter on name → next
|
|
721
|
+
overlay.addEventListener("keydown", (e) => {
|
|
722
|
+
if (e.key !== "Enter") return;
|
|
723
|
+
if (e.target.matches("[data-onb-name]")) {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
next();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function show() {
|
|
731
|
+
if (document.getElementById("onb-overlay")) return;
|
|
732
|
+
// Claim the dashboard sub-state · prototype-dashboard.html runs a
|
|
733
|
+
// restore tick (~2.5s of 250ms retries) that re-opens whatever
|
|
734
|
+
// agent profile the user last viewed once refreshAgents mounts the
|
|
735
|
+
// sidebar rows. Onboarding always lands the user on a fresh room
|
|
736
|
+
// (or the composer), so claim "rooms" + clear the saved agent id
|
|
737
|
+
// up-front. Without this, finishing onboarding fast enough races
|
|
738
|
+
// the tick and the user occasionally lands on a stale agent
|
|
739
|
+
// profile instead of the room they just convened.
|
|
740
|
+
try {
|
|
741
|
+
localStorage.setItem("boardroom.sidebar.tab", "rooms");
|
|
742
|
+
localStorage.setItem("boardroom.sidebar.agents", "new");
|
|
743
|
+
} catch (e) {}
|
|
744
|
+
const wrap = document.createElement("div");
|
|
745
|
+
wrap.innerHTML = modalHTML().trim();
|
|
746
|
+
document.body.appendChild(wrap.firstChild);
|
|
747
|
+
overlay = document.getElementById("onb-overlay");
|
|
748
|
+
overlay.classList.add("open");
|
|
749
|
+
document.body.classList.add("onb-locked");
|
|
750
|
+
renderStep();
|
|
751
|
+
wire();
|
|
752
|
+
|
|
753
|
+
// First-run: for the user-settings.js bootstrap, suppress ⚙ click
|
|
754
|
+
// while overlay is open by capture-phase guard.
|
|
755
|
+
document.addEventListener("click", guardSettingsTrigger, true);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function guardSettingsTrigger(e) {
|
|
759
|
+
if (!overlay || !overlay.classList.contains("open")) {
|
|
760
|
+
document.removeEventListener("click", guardSettingsTrigger, true);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (e.target.closest("[data-user-settings-trigger]")) {
|
|
764
|
+
e.stopPropagation();
|
|
765
|
+
e.preventDefault();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function init() {
|
|
770
|
+
if (await shouldShow()) {
|
|
771
|
+
show();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (document.readyState === "loading") {
|
|
776
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
777
|
+
} else {
|
|
778
|
+
init();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
window.boardroomShowOnboarding = show; // exposed for testing
|
|
782
|
+
})();
|