privateboard 0.1.0 → 0.1.2
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/dist/cli.js +2060 -183
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.js +3 -3
- package/public/agent-profile.css +5 -4
- package/public/agent-profile.js +18 -2
- package/public/app.js +513 -26
- package/public/avatar-skill.js +6 -9
- package/public/home.html +1750 -0
- package/public/{prototype-dashboard.html → index.html} +129 -116
- package/public/onboarding.js +4 -4
- package/public/quote-cta.css +225 -0
- package/public/quote-cta.js +355 -0
- package/public/report/spines/a16z-thesis.css +33 -1
- package/public/report/spines/anthropic-essay.css +54 -1
- package/public/report/spines/boardroom-dark.css +18 -2
- package/public/report/spines/gartner-note.css +47 -0
- package/public/report/spines/mckinsey-deck.css +38 -1
- package/public/report/spines/openai-paper.css +37 -1
- package/public/report.html +361 -6
- package/public/room-settings.css +6 -4
- package/public/room-settings.js +8 -5
- package/public/user-settings.css +18 -0
- package/public/user-settings.js +31 -2
package/public/report.html
CHANGED
|
@@ -60,7 +60,18 @@
|
|
|
60
60
|
-webkit-print-color-adjust: exact !important;
|
|
61
61
|
box-shadow: none !important;
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
|
|
64
|
+
/* Edge-to-edge backgrounds in PDF · zero @page margin so the
|
|
65
|
+
body's paper colour fills every pixel of the page. The visible
|
|
66
|
+
gutter is recreated via body padding so existing per-section
|
|
67
|
+
paddings (cover / chapters / methodology) keep working
|
|
68
|
+
unchanged. Without this, A4 PDFs of light-paper spines
|
|
69
|
+
(anthropic-essay #F4F0E8 / a16z-thesis #F7F3E8 / cream-printed
|
|
70
|
+
boardroom-dark #FAF7F0) showed a jarring white perimeter
|
|
71
|
+
outside a tan content rectangle — the user's "why is there
|
|
72
|
+
white around my yellow report" complaint. */
|
|
73
|
+
@page { size: A4; margin: 0; }
|
|
74
|
+
body { padding: 14mm 12mm !important; }
|
|
64
75
|
|
|
65
76
|
/* Strip browser chrome from the PDF. */
|
|
66
77
|
.top-rule,
|
|
@@ -72,9 +83,16 @@
|
|
|
72
83
|
display: none !important;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
/* Page-frame
|
|
76
|
-
|
|
86
|
+
/* Page-frame background · matches body's printed colour per spine
|
|
87
|
+
so any PDF engine that paints html beyond body's box (some
|
|
88
|
+
legacy print pipelines) stays coherent. Modern Chrome with
|
|
89
|
+
@page margin: 0 fills the page from body alone, but this is
|
|
90
|
+
belt-and-braces. Default white; per-spine `:has()` selectors
|
|
91
|
+
below override for the light-paper spines. */
|
|
77
92
|
html { background: #FFFFFF !important; }
|
|
93
|
+
html:has(body[data-spine="anthropic-essay"]) { background: #F4F0E8 !important; }
|
|
94
|
+
html:has(body[data-spine="a16z-thesis"]) { background: #F7F3E8 !important; }
|
|
95
|
+
html:has(body[data-spine="boardroom-dark"]) { background: #FAF7F0 !important; }
|
|
78
96
|
|
|
79
97
|
/* Container resets so .doc fills the printable area instead of
|
|
80
98
|
being clipped at its 880px on-screen max-width. */
|
|
@@ -199,14 +217,112 @@
|
|
|
199
217
|
.body section.section-the-bet > ol > li,
|
|
200
218
|
.body section.section-new-questions li.nq-item,
|
|
201
219
|
.body pre.mermaid,
|
|
202
|
-
.body table.md-table
|
|
220
|
+
.body table.md-table,
|
|
221
|
+
/* Tier A additions · the units that were still getting sliced
|
|
222
|
+
between pages despite the legacy ruleset. Each one is a small
|
|
223
|
+
atomic visual block that deserves to land whole. The strip
|
|
224
|
+
parent IS in this list (intro + 3-5 cards typically fits on
|
|
225
|
+
one page); the grid itself is NOT — under Tier B the grid is
|
|
226
|
+
block-flowed in print, so individual `.metric-card` atoms are
|
|
227
|
+
what we actually want unbreakable. Listing the grid here would
|
|
228
|
+
force ALL cards onto the same page even when they overflow,
|
|
229
|
+
leaving large dead space at page bottoms. */
|
|
230
|
+
.body .metric-strip, /* intro + grid as one unit */
|
|
231
|
+
.body blockquote, /* convergence / SPA / pull-quotes */
|
|
232
|
+
.body ol > li, .body ul > li { /* tighter than the per-section li rules above */
|
|
203
233
|
break-inside: avoid;
|
|
204
234
|
page-break-inside: avoid;
|
|
205
235
|
}
|
|
236
|
+
/* Strip intro stays glued to the first card · lone "Three numbers
|
|
237
|
+
worth pricing in" titles wandering to a previous page bottom
|
|
238
|
+
look broken even when individual cards are intact. */
|
|
239
|
+
.body .metric-strip-intro {
|
|
240
|
+
break-after: avoid;
|
|
241
|
+
page-break-after: avoid;
|
|
242
|
+
}
|
|
243
|
+
/* Glue rules · keep heading + first content together. CSS only
|
|
244
|
+
has break-after: avoid, which says "no break right after this
|
|
245
|
+
element" — so applying it to the first content element after a
|
|
246
|
+
heading also makes that pair atomic. We pair them with the
|
|
247
|
+
heading rule below for redundancy. */
|
|
206
248
|
.body h2, .body h3, .body h4 {
|
|
207
249
|
break-after: avoid;
|
|
208
250
|
page-break-after: avoid;
|
|
209
251
|
}
|
|
252
|
+
/* Don't let a heading land alone in the page footer with its
|
|
253
|
+
paragraph / table on the next page. The adjacent-sibling
|
|
254
|
+
selector matches the FIRST element after the heading and
|
|
255
|
+
refuses to break before it · effectively keeps "heading +
|
|
256
|
+
opener" as one unit. */
|
|
257
|
+
.body section > h2 + p,
|
|
258
|
+
.body section > h2 + blockquote,
|
|
259
|
+
.body section > h2 + table.md-table,
|
|
260
|
+
.body section > h2 + .metric-strip,
|
|
261
|
+
.body section > h3 + p,
|
|
262
|
+
.body section > h3 + blockquote,
|
|
263
|
+
.body section > h3 + table.md-table {
|
|
264
|
+
break-before: avoid;
|
|
265
|
+
page-break-before: avoid;
|
|
266
|
+
}
|
|
267
|
+
/* Paragraph-level orphan / widow control · a paragraph isn't
|
|
268
|
+
allowed to leave only 1-2 lines on either side of a page
|
|
269
|
+
break. Cheapest typographic discipline; massive effect on the
|
|
270
|
+
feel of multi-page reports. */
|
|
271
|
+
.body { orphans: 3; widows: 3; }
|
|
272
|
+
|
|
273
|
+
/* ─── Tier B · CSS Grid → flex-wrap in print ────────────────
|
|
274
|
+
Chrome's PDF engine notoriously ignores `break-inside: avoid`
|
|
275
|
+
on direct children of a `display: grid` container — but it
|
|
276
|
+
DOES honour the same hint on flex children. The earlier fix
|
|
277
|
+
collapsed both the Headline-Findings pillar grid and the
|
|
278
|
+
metric-strip card grid to `display: block` for safety, which
|
|
279
|
+
worked but stacked everything vertically — the user reported
|
|
280
|
+
the PDF then looks nothing like the on-screen side-by-side
|
|
281
|
+
layout. Switching to flex-wrap keeps the side-by-side cards
|
|
282
|
+
visible in print AND lets the per-card `break-inside: avoid`
|
|
283
|
+
(defined later in this @media block for `.metric-card`) hold,
|
|
284
|
+
so a single card never gets sliced across pages. The on-screen
|
|
285
|
+
grid is untouched.
|
|
286
|
+
|
|
287
|
+
`flex: 1 1 180px` mirrors the screen `minmax(180px, 1fr)` —
|
|
288
|
+
each card gets at least 180px and shares any extra evenly. */
|
|
289
|
+
.body .pillars-grid,
|
|
290
|
+
.body .metric-strip-grid {
|
|
291
|
+
display: flex !important;
|
|
292
|
+
flex-wrap: wrap !important;
|
|
293
|
+
gap: 10px !important;
|
|
294
|
+
align-items: stretch !important;
|
|
295
|
+
}
|
|
296
|
+
.body .pillars-grid > .pillar,
|
|
297
|
+
.body .metric-strip-grid > .metric-card {
|
|
298
|
+
flex: 1 1 180px !important;
|
|
299
|
+
max-width: 100% !important;
|
|
300
|
+
margin-bottom: 0 !important;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Glue chapter-num to its h2 · the "Section 0X" kicker is a
|
|
304
|
+
sibling element inserted before the heading (not nested), so
|
|
305
|
+
the legacy heading-only break-after: avoid doesn't cover it.
|
|
306
|
+
Without this, the kicker can land alone in a page footer with
|
|
307
|
+
its h2 floating to the next page top — visually orphaned. */
|
|
308
|
+
.body .chapter-num {
|
|
309
|
+
break-after: avoid;
|
|
310
|
+
page-break-after: avoid;
|
|
311
|
+
}
|
|
312
|
+
/* First-child glue · the first item inside a grid (now block-
|
|
313
|
+
flowed) shouldn't be allowed to break away from the section's
|
|
314
|
+
h2 above. Combined with the h2's break-after: avoid this
|
|
315
|
+
locks the heading + opener pair across page boundaries. */
|
|
316
|
+
.body .pillars-grid > .pillar:first-child,
|
|
317
|
+
.body .metric-strip-grid > .metric-card:first-child,
|
|
318
|
+
.body section.section-recommendations ol > li:first-child,
|
|
319
|
+
.body section.section-considerations ol > li:first-child,
|
|
320
|
+
.body section.section-the-bet ol > li:first-child,
|
|
321
|
+
.body section.section-new-questions ol > li:first-child,
|
|
322
|
+
.body section.section-big-ideas ol > li:first-child {
|
|
323
|
+
break-before: avoid;
|
|
324
|
+
page-break-before: avoid;
|
|
325
|
+
}
|
|
210
326
|
.body section.section-methodology {
|
|
211
327
|
break-inside: avoid;
|
|
212
328
|
page-break-inside: avoid;
|
|
@@ -246,6 +362,128 @@
|
|
|
246
362
|
.body section.section-big-ideas + h2.section-methodology {
|
|
247
363
|
border-top: none;
|
|
248
364
|
}
|
|
365
|
+
|
|
366
|
+
/* ─── Drop the table's heavy top rule when it's the first content
|
|
367
|
+
of a section ───────────────────────────────────────────────────
|
|
368
|
+
Pattern hit hard by Risks-to-Manage / Pre-Mortem (and any other
|
|
369
|
+
table-first section): the chapter-num kicker above the H2
|
|
370
|
+
already carries an underline (its border-bottom hairline), and
|
|
371
|
+
the table immediately below the H2 carries its own 1.5px
|
|
372
|
+
border-top. With only the H2 title between them, those two
|
|
373
|
+
parallel rules read as a doubled frame around the heading. The
|
|
374
|
+
table's row separators keep its internal structure clear without
|
|
375
|
+
the top rule. Spine-agnostic — applied via the inline-loaded
|
|
376
|
+
stylesheet which beats per-spine selectors. */
|
|
377
|
+
.body section > table.md-table:first-child {
|
|
378
|
+
border-top: none;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ─── Metric-strip · spine-agnostic baseline ─────────────────────
|
|
382
|
+
A row of KPI cards generated by the ```metric-strip fenced block.
|
|
383
|
+
Per-spine CSS (public/report/spines/*.css) overrides surfaces +
|
|
384
|
+
typography for visual personality; this baseline is what every
|
|
385
|
+
spine inherits before its overrides land — keeps the strip
|
|
386
|
+
readable even for spines that haven't shipped a treatment yet.
|
|
387
|
+
|
|
388
|
+
Discipline:
|
|
389
|
+
· No `border-left` as a callout (project rule · feedback memo).
|
|
390
|
+
· Card boundaries are surface + spacing, not double parallel
|
|
391
|
+
borders. The grid sits on the section background; cards lift
|
|
392
|
+
via a tiny background tint.
|
|
393
|
+
· Metric value is the eye-catch · big tabular numerals.
|
|
394
|
+
· Trend glyph is small and doesn't compete with the value. */
|
|
395
|
+
.body .metric-strip {
|
|
396
|
+
margin: 18px 0 22px;
|
|
397
|
+
}
|
|
398
|
+
.body .metric-strip-intro {
|
|
399
|
+
font-family: var(--mono);
|
|
400
|
+
font-size: 11px;
|
|
401
|
+
letter-spacing: 0.06em;
|
|
402
|
+
text-transform: uppercase;
|
|
403
|
+
color: var(--text-soft, #8E8B83);
|
|
404
|
+
margin: 0 0 10px;
|
|
405
|
+
}
|
|
406
|
+
.body .metric-strip-grid {
|
|
407
|
+
display: grid;
|
|
408
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
409
|
+
gap: 10px;
|
|
410
|
+
align-items: stretch;
|
|
411
|
+
}
|
|
412
|
+
.body .metric-card {
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
gap: 6px;
|
|
416
|
+
padding: 14px 16px;
|
|
417
|
+
background: var(--panel-2, rgba(255, 255, 255, 0.025));
|
|
418
|
+
border: 0.5px solid var(--line-bright, rgba(255, 255, 255, 0.08));
|
|
419
|
+
min-height: 92px;
|
|
420
|
+
}
|
|
421
|
+
.body .metric-label {
|
|
422
|
+
font-family: var(--mono);
|
|
423
|
+
font-size: 10px;
|
|
424
|
+
letter-spacing: 0.1em;
|
|
425
|
+
text-transform: uppercase;
|
|
426
|
+
color: var(--text-soft, #8E8B83);
|
|
427
|
+
line-height: 1.3;
|
|
428
|
+
}
|
|
429
|
+
.body .metric-value-row {
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: baseline;
|
|
432
|
+
gap: 8px;
|
|
433
|
+
line-height: 1;
|
|
434
|
+
}
|
|
435
|
+
.body .metric-value {
|
|
436
|
+
font-family: "SF Mono", "JetBrains Mono", "Menlo", "Helvetica Neue",
|
|
437
|
+
"Songti SC", monospace;
|
|
438
|
+
font-size: 26px;
|
|
439
|
+
font-weight: 700;
|
|
440
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
441
|
+
color: var(--text, #C8C5BE);
|
|
442
|
+
letter-spacing: -0.01em;
|
|
443
|
+
}
|
|
444
|
+
.body .metric-trend {
|
|
445
|
+
font-family: "SF Mono", "JetBrains Mono", monospace;
|
|
446
|
+
font-size: 14px;
|
|
447
|
+
line-height: 1;
|
|
448
|
+
color: var(--text-dim, #5C5A52);
|
|
449
|
+
}
|
|
450
|
+
.body .metric-trend[data-trend="up"] { color: var(--em, var(--lime, #6FB572)); }
|
|
451
|
+
.body .metric-trend[data-trend="down"] { color: var(--red, #B5706A); }
|
|
452
|
+
.body .metric-trend[data-trend="flat"] { color: var(--text-dim, #5C5A52); }
|
|
453
|
+
.body .metric-qualifier {
|
|
454
|
+
font-family: var(--sans);
|
|
455
|
+
font-size: 11.5px;
|
|
456
|
+
line-height: 1.4;
|
|
457
|
+
color: var(--text-soft, #8E8B83);
|
|
458
|
+
letter-spacing: -0.003em;
|
|
459
|
+
}
|
|
460
|
+
.body .metric-attribution {
|
|
461
|
+
font-family: var(--mono);
|
|
462
|
+
font-size: 9.5px;
|
|
463
|
+
letter-spacing: 0.06em;
|
|
464
|
+
text-transform: uppercase;
|
|
465
|
+
color: var(--text-faint, #5C5A4D);
|
|
466
|
+
margin-top: 2px;
|
|
467
|
+
}
|
|
468
|
+
/* Print · keep cards intact across page breaks. */
|
|
469
|
+
@media print {
|
|
470
|
+
.body .metric-card { break-inside: avoid; page-break-inside: avoid; }
|
|
471
|
+
}
|
|
472
|
+
/* Defensive · malformed strips render as a quiet placeholder. */
|
|
473
|
+
.body .metric-strip-error {
|
|
474
|
+
padding: 12px 14px;
|
|
475
|
+
background: var(--panel-3, rgba(255, 255, 255, 0.04));
|
|
476
|
+
color: var(--text-dim, #5C5A52);
|
|
477
|
+
font-family: var(--mono);
|
|
478
|
+
font-size: 11px;
|
|
479
|
+
letter-spacing: 0.06em;
|
|
480
|
+
}
|
|
481
|
+
.body .metric-strip-error::before {
|
|
482
|
+
content: "// metric-strip · malformed";
|
|
483
|
+
display: block;
|
|
484
|
+
margin-bottom: 6px;
|
|
485
|
+
color: var(--red, #B5706A);
|
|
486
|
+
}
|
|
249
487
|
</style>
|
|
250
488
|
</head>
|
|
251
489
|
<body>
|
|
@@ -388,6 +626,66 @@
|
|
|
388
626
|
}).join("\n");
|
|
389
627
|
}
|
|
390
628
|
|
|
629
|
+
/** Parse a fenced ```metric-strip block body (strict JSON) and emit
|
|
630
|
+
* the dashboard card grid. Mirrors how ```mermaid is handled — the
|
|
631
|
+
* block is pulled out at the placeholder layer before block parsing,
|
|
632
|
+
* so the structured HTML survives the renderer's escape pass.
|
|
633
|
+
* Defensive: bad JSON / missing fields render as a quiet fallback
|
|
634
|
+
* block so a malformed strip doesn't break the rest of the report. */
|
|
635
|
+
function renderMetricStrip(body) {
|
|
636
|
+
let parsed;
|
|
637
|
+
try { parsed = JSON.parse(body); } catch (e) {
|
|
638
|
+
return `<div class="metric-strip metric-strip-error" data-err="parse">${escape(body)}</div>`;
|
|
639
|
+
}
|
|
640
|
+
if (!parsed || typeof parsed !== "object") {
|
|
641
|
+
return `<div class="metric-strip metric-strip-error" data-err="shape"></div>`;
|
|
642
|
+
}
|
|
643
|
+
const intro = typeof parsed.intro === "string" ? parsed.intro.trim() : "";
|
|
644
|
+
const cardsRaw = Array.isArray(parsed.cards) ? parsed.cards : [];
|
|
645
|
+
const cards = [];
|
|
646
|
+
for (const c of cardsRaw) {
|
|
647
|
+
if (!c || typeof c !== "object") continue;
|
|
648
|
+
const label = typeof c.label === "string" ? c.label.trim() : "";
|
|
649
|
+
const value = typeof c.value === "string" ? c.value.trim() : "";
|
|
650
|
+
if (!label || !value) continue;
|
|
651
|
+
const qualifier = typeof c.qualifier === "string" ? c.qualifier.trim() : "";
|
|
652
|
+
const attribution = typeof c.attribution === "string" ? c.attribution.trim() : "";
|
|
653
|
+
const trend =
|
|
654
|
+
c.trend === "up" || c.trend === "down" || c.trend === "flat" ? c.trend : "";
|
|
655
|
+
cards.push({ label, value, qualifier, attribution, trend });
|
|
656
|
+
if (cards.length >= 5) break;
|
|
657
|
+
}
|
|
658
|
+
if (cards.length === 0) {
|
|
659
|
+
return `<div class="metric-strip metric-strip-error" data-err="empty"></div>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Trend → arrow glyph (rendered in a small badge on the card).
|
|
663
|
+
const trendGlyph = (t) => (t === "up" ? "↑" : t === "down" ? "↓" : t === "flat" ? "→" : "");
|
|
664
|
+
|
|
665
|
+
const introHtml = intro
|
|
666
|
+
? `<p class="metric-strip-intro">${escape(intro)}</p>`
|
|
667
|
+
: "";
|
|
668
|
+
const cardsHtml = cards.map((c) => {
|
|
669
|
+
const trendAttr = c.trend ? ` data-trend="${escape(c.trend)}"` : "";
|
|
670
|
+
const trendBadge = c.trend
|
|
671
|
+
? `<span class="metric-trend" data-trend="${escape(c.trend)}" aria-label="${escape(c.trend)}">${trendGlyph(c.trend)}</span>`
|
|
672
|
+
: "";
|
|
673
|
+
const qualifier = c.qualifier ? `<div class="metric-qualifier">${escape(c.qualifier)}</div>` : "";
|
|
674
|
+
const attribution = c.attribution ? `<div class="metric-attribution">${escape(c.attribution)}</div>` : "";
|
|
675
|
+
return `<div class="metric-card"${trendAttr}>` +
|
|
676
|
+
`<div class="metric-label">${escape(c.label)}</div>` +
|
|
677
|
+
`<div class="metric-value-row"><span class="metric-value">${escape(c.value)}</span>${trendBadge}</div>` +
|
|
678
|
+
qualifier +
|
|
679
|
+
attribution +
|
|
680
|
+
`</div>`;
|
|
681
|
+
}).join("");
|
|
682
|
+
|
|
683
|
+
return `<div class="metric-strip" data-cards="${cards.length}">` +
|
|
684
|
+
introHtml +
|
|
685
|
+
`<div class="metric-strip-grid">${cardsHtml}</div>` +
|
|
686
|
+
`</div>`;
|
|
687
|
+
}
|
|
688
|
+
|
|
391
689
|
// Slightly richer markdown than the in-room renderer — supports h1–h4,
|
|
392
690
|
// bullets, ordered lists, paragraphs, blockquotes, fenced code blocks
|
|
393
691
|
// (incl. ```mermaid for diagrams), and pipe tables.
|
|
@@ -400,7 +698,11 @@
|
|
|
400
698
|
const lines = md.split("\n");
|
|
401
699
|
let i = 0;
|
|
402
700
|
while (i < lines.length) {
|
|
403
|
-
|
|
701
|
+
// Fence info-string allows dashes too (e.g. ```metric-strip).
|
|
702
|
+
// Stock `\w*` excludes `-`, so multi-word lang tags fell
|
|
703
|
+
// through and rendered as raw text · the brief writer emits
|
|
704
|
+
// ```metric-strip / ```chart-block style fences regularly.
|
|
705
|
+
const fence = /^```([\w-]*)\s*$/.exec(lines[i]);
|
|
404
706
|
if (fence) {
|
|
405
707
|
const lang = (fence[1] || "").toLowerCase();
|
|
406
708
|
const start = i + 1;
|
|
@@ -412,6 +714,8 @@
|
|
|
412
714
|
const idx = placeholders.length;
|
|
413
715
|
if (lang === "mermaid") {
|
|
414
716
|
placeholders.push(`<pre class="mermaid">${escape(sanitizeMermaid(body))}</pre>`);
|
|
717
|
+
} else if (lang === "metric-strip") {
|
|
718
|
+
placeholders.push(renderMetricStrip(body));
|
|
415
719
|
} else {
|
|
416
720
|
placeholders.push(
|
|
417
721
|
`<pre class="codeblock${lang ? ` lang-${escape(lang)}` : ""}"><code>${escape(body)}</code></pre>`,
|
|
@@ -564,6 +868,11 @@
|
|
|
564
868
|
{ re: /^critical\s*assumptions?|^load[-\s]bearing\s*assumptions?|^承重假设|^关键假设/i, cls: "section-critical-assumptions" },
|
|
565
869
|
{ re: /^scenario\s*tree|^scenarios?$|^情景树|^情景分析|^命名情景/i, cls: "section-scenario-tree" },
|
|
566
870
|
{ re: /^leading\s*indicators?|^watch[-\s]list|^先行指标|^监测指标|^监控信号/i, cls: "section-leading-indicators" },
|
|
871
|
+
// Dashboard-style KPI strip · catches the house-style label
|
|
872
|
+
// variants (By the Numbers / The Underwrite / Three Numbers Worth
|
|
873
|
+
// Pricing In / Strategic Indicators / Quantitative Reads / 用数字说话 /
|
|
874
|
+
// 关键指标 / 指标看板 / 实证锚点 / 战略指标 / etc.) plus the literal default.
|
|
875
|
+
{ re: /^by\s*the\s*numbers|^the\s*underwrite|^(three|key|strategic|quantitative)\s*(numbers?|metrics?|indicators?|reads?|anchors?)|^numbers?\s*(that|worth)|^the\s*numbers$|^a\s+few\s+numbers|^what\s+the\s+(numbers|data|math)|^why\s+the\s+math|^the\s*diagnostic\s*at\s*a\s*glance|^indicator\s*dashboard|^empirical\s*anchors|^metric\s*strip|^用数字说话|^支撑数据|^值得定价的|^数字$|^数字告诉我们|^几个浮上来的数字|^数据怎么说|^为什么数算得过来|^战略指标|^关键指标|^诊断速览|^驱动判断的数字|^定量结果|^实证锚点|^指标看板|^定量判读/i, cls: "section-metric-strip" },
|
|
567
876
|
{ re: /^recommendations?/i, cls: "section-recommendations" },
|
|
568
877
|
{ re: /^the\s*bet$|^the\s*bet[\s·:]/i, cls: "section-the-bet" },
|
|
569
878
|
{ re: /^considerations?/i, cls: "section-considerations" },
|
|
@@ -1334,13 +1643,29 @@
|
|
|
1334
1643
|
// expose. The chart title is hidden because the markdown
|
|
1335
1644
|
// already renders an H3 caption directly above each chart —
|
|
1336
1645
|
// showing it twice is the single ugliest mermaid default.
|
|
1646
|
+
// Hide every mermaid chart's built-in <text> title — the markdown
|
|
1647
|
+
// already prints an H3 caption right above each diagram, and
|
|
1648
|
+
// showing it twice is mermaid's single ugliest default. Apply
|
|
1649
|
+
// across quadrant / xychart / pie / timeline.
|
|
1337
1650
|
const themeCSS = `
|
|
1338
1651
|
g.quadrant-chart text { font-family: ${t.fontFamily}; }
|
|
1339
1652
|
g.quadrant-point > circle, .quadrant-point circle {
|
|
1340
1653
|
stroke: ${t.vars.background};
|
|
1341
1654
|
stroke-width: 1.5px;
|
|
1342
1655
|
}
|
|
1343
|
-
text.quadrant-title
|
|
1656
|
+
text.quadrant-title,
|
|
1657
|
+
.pieTitleText,
|
|
1658
|
+
.xy-chart .title-text,
|
|
1659
|
+
.timeline .title-text,
|
|
1660
|
+
.timeline-title { display: none !important; }
|
|
1661
|
+
/* xychart bars: thin stroke against the background so adjacent
|
|
1662
|
+
bars don't merge visually on dense datasets. */
|
|
1663
|
+
.xy-chart .bar { stroke: ${t.vars.background}; stroke-width: 1px; }
|
|
1664
|
+
/* Pie slice text: legibility on the dark / light spines. */
|
|
1665
|
+
.pieCircle { stroke: ${t.vars.background}; stroke-width: 1.5px; }
|
|
1666
|
+
text.slice, .pieLegendText { font-family: ${t.fontFamily}; }
|
|
1667
|
+
/* Timeline · period/label fonts inherit the spine. */
|
|
1668
|
+
.timeline text { font-family: ${t.fontFamily}; }
|
|
1344
1669
|
`;
|
|
1345
1670
|
mermaid.initialize({
|
|
1346
1671
|
startOnLoad: false,
|
|
@@ -1397,6 +1722,36 @@
|
|
|
1397
1722
|
quadrantExternalBorderStrokeFill: t.vars.border,
|
|
1398
1723
|
},
|
|
1399
1724
|
flowchart: { useMaxWidth: true, htmlLabels: true },
|
|
1725
|
+
// ── xychart-beta · bar charts ──────────────────────────
|
|
1726
|
+
// Tight margins, label hidden (caption above), and a single
|
|
1727
|
+
// colour palette derived from the spine's pointFill so
|
|
1728
|
+
// every bar in a chart shares a coherent voice. Mermaid
|
|
1729
|
+
// 11+ accepts the `xyChart` key (older releases tolerated
|
|
1730
|
+
// `xychart` — keep BOTH so the config isn't fragile across
|
|
1731
|
+
// CDN bumps).
|
|
1732
|
+
xyChart: {
|
|
1733
|
+
width: 720,
|
|
1734
|
+
height: 360,
|
|
1735
|
+
titlePadding: 0,
|
|
1736
|
+
titleFontSize: 0,
|
|
1737
|
+
showTitle: false,
|
|
1738
|
+
showDataLabel: true,
|
|
1739
|
+
plotReservedSpacePercent: 50,
|
|
1740
|
+
xAxis: { labelFontSize: 12, titleFontSize: 0, showTitle: false },
|
|
1741
|
+
yAxis: { labelFontSize: 12, titleFontSize: 13 },
|
|
1742
|
+
},
|
|
1743
|
+
// Pie · legend on the right, slice labels disabled so wide
|
|
1744
|
+
// labels don't push the chart off-screen. The legend
|
|
1745
|
+
// already carries label + value (showData: true above).
|
|
1746
|
+
pie: {
|
|
1747
|
+
textPosition: 0.7,
|
|
1748
|
+
useMaxWidth: true,
|
|
1749
|
+
},
|
|
1750
|
+
// Timeline · cards-on-rail; title suppressed (caption above).
|
|
1751
|
+
timeline: {
|
|
1752
|
+
padding: 16,
|
|
1753
|
+
useMaxWidth: true,
|
|
1754
|
+
},
|
|
1400
1755
|
});
|
|
1401
1756
|
await mermaid.run({ querySelector: ".mermaid" });
|
|
1402
1757
|
} catch (e) {
|
package/public/room-settings.css
CHANGED
|
@@ -183,13 +183,15 @@
|
|
|
183
183
|
.rs-config-row { grid-template-columns: 1fr; gap: 6px; }
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/* Tone + intensity chip rows · explicit grid columns (
|
|
186
|
+
/* Tone + intensity chip rows · explicit grid columns (5 / 3) so the
|
|
187
187
|
chips always sit in a single row at content width. flex-wrap was
|
|
188
188
|
the prior approach but allowed chips to fold onto a second line on
|
|
189
|
-
tight viewports — the user wants them locked to one row.
|
|
189
|
+
tight viewports — the user wants them locked to one row. Tone count
|
|
190
|
+
bumped from 4 → 5 when `research` and `critique` were added; if
|
|
191
|
+
you add more modes, bump this number too. */
|
|
190
192
|
.rs-mode-grid {
|
|
191
193
|
display: grid;
|
|
192
|
-
grid-template-columns: repeat(
|
|
194
|
+
grid-template-columns: repeat(5, max-content);
|
|
193
195
|
gap: 3px;
|
|
194
196
|
}
|
|
195
197
|
.rs-intensity-chips {
|
|
@@ -439,7 +441,7 @@
|
|
|
439
441
|
/* Section label · matches the new composer's mono micro-type kicker.
|
|
440
442
|
No leading lime bullet — the inline count chip carries enough
|
|
441
443
|
visual weight, and dropping the bullet lets the labels sit flush
|
|
442
|
-
left like the rest of the
|
|
444
|
+
left like the rest of the app's section heads. */
|
|
443
445
|
.rs-label {
|
|
444
446
|
font-size: 9px;
|
|
445
447
|
font-weight: 700;
|
package/public/room-settings.js
CHANGED
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
"Co-creator. Directors stand with you and push the idea outward — yes-and a contribution, name a concrete adjacent variant (\"what if we instead…\"), borrow pieces from another director's turn into new combinations. May end with one curious question, never a defense-demanding one.",
|
|
17
17
|
constructive:
|
|
18
18
|
"Sympathetic interrogator. They want you to win, but only via the strongest version. Each turn picks ONE load-bearing assumption and proposes the candidate stronger version that would stand. Disagreement is allowed, but every objection comes packaged with a forward path.",
|
|
19
|
+
research:
|
|
20
|
+
"Collaborative inquiry. The room mines the materials in front of it (your brief, web-search results, prior turns) for what's actually there. Each turn must cite a specific source piece, label it OBSERVATION / INFERENCE / SPECULATION, then extract the insight your lens makes salient. Defaults web search ON when a Brave key is configured.",
|
|
19
21
|
debate:
|
|
20
22
|
"Peer reviewer. Each turn opens by steelmanning your strongest claim (\"the strongest read of your point is…\") and only then attacks THAT version — naming a specific risk, demanding evidence, exposing the trade-off you're hiding. Sharp but professional. Skipping the steelman is a protocol violation.",
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
+
critique:
|
|
24
|
+
"Review board. The room audits a finished deliverable systematically — each turn names the dimension being audited (logic / evidence / scope / risk / etc.), surfaces 2–3 specific flaws labelled BLOCKER · MAJOR · MINOR, points at the load-bearing piece, and indicates the direction a fix would lie. At least one BLOCKER or MAJOR per turn is mandatory.",
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
/** Intensity tooltips · what each pick does to the directors' default
|
|
@@ -146,8 +148,8 @@
|
|
|
146
148
|
const NAMES = {};
|
|
147
149
|
|
|
148
150
|
// Baseline state — synced from window.app.currentRoom each time the
|
|
149
|
-
// overlay opens. The fallback values keep the
|
|
150
|
-
//
|
|
151
|
+
// overlay opens. The fallback values keep the page usable in standalone
|
|
152
|
+
// preview (where window.app is absent).
|
|
151
153
|
const ROOM_STATE = {
|
|
152
154
|
title: "the minimum viable structure of a data flywheel",
|
|
153
155
|
topic: "I want to build an AI assistant for enterprise HR teams — automated resume screening + interview guides. Does this idea hold up under three-director scrutiny?",
|
|
@@ -201,8 +203,9 @@
|
|
|
201
203
|
const MODES = [
|
|
202
204
|
{ v: "brainstorm", label: "Brainstorm", desc: "yes-and" },
|
|
203
205
|
{ v: "constructive", label: "Constructive", desc: "push & sharpen" },
|
|
206
|
+
{ v: "research", label: "Research", desc: "mine the material" },
|
|
204
207
|
{ v: "debate", label: "Debate", desc: "find holes" },
|
|
205
|
-
{ v: "
|
|
208
|
+
{ v: "critique", label: "Critique", desc: "audit the deliverable" }
|
|
206
209
|
];
|
|
207
210
|
|
|
208
211
|
|
package/public/user-settings.css
CHANGED
|
@@ -656,6 +656,24 @@
|
|
|
656
656
|
}
|
|
657
657
|
.us-foot .us-done:hover { border-color: var(--lime, #6FB572); color: var(--lime, #6FB572); }
|
|
658
658
|
|
|
659
|
+
/* Right-side cluster · website link + Done button. */
|
|
660
|
+
.us-foot-right {
|
|
661
|
+
display: flex;
|
|
662
|
+
align-items: center;
|
|
663
|
+
gap: 14px;
|
|
664
|
+
}
|
|
665
|
+
.us-foot .us-website {
|
|
666
|
+
font-family: var(--mono);
|
|
667
|
+
font-size: 9.5px;
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
letter-spacing: 0.1em;
|
|
670
|
+
text-transform: uppercase;
|
|
671
|
+
color: var(--text-soft, #8E8B83);
|
|
672
|
+
text-decoration: none;
|
|
673
|
+
transition: color 0.12s;
|
|
674
|
+
}
|
|
675
|
+
.us-foot .us-website:hover { color: var(--lime, #6FB572); }
|
|
676
|
+
|
|
659
677
|
@media (max-width: 600px) {
|
|
660
678
|
.user-settings-overlay { padding: 12px; }
|
|
661
679
|
.us-head { padding-left: 14px; padding-right: 14px; }
|
package/public/user-settings.js
CHANGED
|
@@ -903,7 +903,10 @@
|
|
|
903
903
|
|
|
904
904
|
<footer class="us-foot">
|
|
905
905
|
<span class="saved">changes save automatically</span>
|
|
906
|
-
<
|
|
906
|
+
<div class="us-foot-right">
|
|
907
|
+
<a class="us-website" href="/home.html" target="_blank" rel="noopener">website ↗</a>
|
|
908
|
+
<button type="button" class="us-done">[ Done ]</button>
|
|
909
|
+
</div>
|
|
907
910
|
</footer>
|
|
908
911
|
|
|
909
912
|
</div>
|
|
@@ -971,7 +974,6 @@
|
|
|
971
974
|
if (typeof window.app.renderUserBlock === "function") window.app.renderUserBlock();
|
|
972
975
|
} else {
|
|
973
976
|
document.querySelectorAll(".sidebar-foot .user-name").forEach((el) => { el.textContent = (u.name || "Kay").toUpperCase(); });
|
|
974
|
-
document.querySelectorAll(".sidebar-foot .user-menu .name").forEach((el) => { el.textContent = u.name || "Kay"; });
|
|
975
977
|
}
|
|
976
978
|
}
|
|
977
979
|
|
|
@@ -1124,6 +1126,26 @@
|
|
|
1124
1126
|
// wireKeysSection again.
|
|
1125
1127
|
fetchKeyMeta().then(() => {
|
|
1126
1128
|
if (currentSection !== "keys") return;
|
|
1129
|
+
|
|
1130
|
+
// After-onboarding sync · the bootstrap fetchKeyMeta ran before
|
|
1131
|
+
// the user wrote their first key (during onboarding). When the
|
|
1132
|
+
// user opens settings without a page refresh, _keysMeta was
|
|
1133
|
+
// empty at first render, so activeProviders was derived without
|
|
1134
|
+
// the just-configured provider — and the keys tab paints with
|
|
1135
|
+
// no row for it (e.g. "no OpenRouter section visible until
|
|
1136
|
+
// refresh"). Detect that drift here and rebuild the section
|
|
1137
|
+
// when a configured provider is missing its row. Inline pill
|
|
1138
|
+
// refresh below handles the simpler case where the row already
|
|
1139
|
+
// exists and only its `● configured` state needs flipping.
|
|
1140
|
+
const missingActive = LLM_PROVIDER_IDS.filter(
|
|
1141
|
+
(id) => _keysMeta[id] && _keysMeta[id].configured && !activeProviders.includes(id),
|
|
1142
|
+
);
|
|
1143
|
+
if (missingActive.length > 0) {
|
|
1144
|
+
activeProviders = null;
|
|
1145
|
+
rerenderKeysSection();
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1127
1149
|
paneEl.querySelectorAll(".us-key-row").forEach((row) => {
|
|
1128
1150
|
const provider = row.dataset.provider;
|
|
1129
1151
|
const meta = _keysMeta[provider];
|
|
@@ -1179,6 +1201,13 @@
|
|
|
1179
1201
|
if (window.app && typeof window.app.refreshKeys === "function") {
|
|
1180
1202
|
window.app.refreshKeys();
|
|
1181
1203
|
}
|
|
1204
|
+
// If an agent profile is open, its skill rows have data-key-
|
|
1205
|
+
// configured cached from first paint. Re-fetch so the web-search
|
|
1206
|
+
// toggle no longer shows the "configure key" prompt after the
|
|
1207
|
+
// user added the Brave key here.
|
|
1208
|
+
if (typeof window.refreshAgentProfileSkills === "function") {
|
|
1209
|
+
window.refreshAgentProfileSkills();
|
|
1210
|
+
}
|
|
1182
1211
|
}
|
|
1183
1212
|
|
|
1184
1213
|
function init() {
|