privateboard 0.1.7 → 0.1.8
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 +694 -16
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +194 -0
- package/public/app.js +426 -66
- package/public/bento.html +1396 -0
- package/public/home.html +389 -17
- package/public/index.html +22 -5
- package/public/magazine.html +1266 -0
- package/public/newspaper.html +1770 -0
- package/public/report.html +366 -52
- package/public/user-settings.css +94 -47
- package/public/user-settings.js +76 -42
package/public/app.js
CHANGED
|
@@ -912,11 +912,37 @@
|
|
|
912
912
|
}
|
|
913
913
|
} else if (kind === "brief-started") {
|
|
914
914
|
this.markBriefEvent();
|
|
915
|
+
// Mode determines BOTH the seeded stage map and which pip
|
|
916
|
+
// rail renderBriefStages picks. Bento, magazine, and
|
|
917
|
+
// newspaper modes each emit only 2 stage events (extract
|
|
918
|
+
// + write), so seeding the 7-stage layout for a
|
|
919
|
+
// structured-mode brief leaves the 5 middle pips forever
|
|
920
|
+
// pending — the rail then renders with a row of dead
|
|
921
|
+
// black pips between extract and write. Read mode from
|
|
922
|
+
// the payload (server includes it on brief-started for
|
|
923
|
+
// exactly this reason) and seed accordingly.
|
|
924
|
+
const isStructured = this.isStructuredBriefMode(payload.mode);
|
|
925
|
+
const briefMode = isStructured ? payload.mode : "research-note";
|
|
926
|
+
const seededStages = isStructured
|
|
927
|
+
? {
|
|
928
|
+
extract: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
929
|
+
write: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
930
|
+
}
|
|
931
|
+
: {
|
|
932
|
+
extract: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
933
|
+
compose: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
934
|
+
"scaffold-anchor": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
935
|
+
"scaffold-findings": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
936
|
+
"scaffold-cluster": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
937
|
+
"scaffold-actions": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
938
|
+
write: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
939
|
+
};
|
|
915
940
|
const newBrief = {
|
|
916
941
|
id: payload.briefId,
|
|
917
942
|
title: "Generating…",
|
|
918
943
|
bodyMd: "",
|
|
919
944
|
style: payload.style || "mckinsey",
|
|
945
|
+
mode: briefMode,
|
|
920
946
|
// Carry the supplement so the tab strip can render the
|
|
921
947
|
// in-progress brief with its supplement label ("xxx 视角")
|
|
922
948
|
// immediately, instead of waiting for brief-final to
|
|
@@ -931,21 +957,13 @@
|
|
|
931
957
|
language: payload.language === "zh" ? "zh" : "en",
|
|
932
958
|
pipelineStartedAt: Date.now(),
|
|
933
959
|
createdAt: Date.now(),
|
|
934
|
-
// Stage checklist · seeded
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
stages:
|
|
941
|
-
extract: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
942
|
-
compose: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
943
|
-
"scaffold-anchor": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
944
|
-
"scaffold-findings": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
945
|
-
"scaffold-cluster": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
946
|
-
"scaffold-actions": { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
947
|
-
write: { status: "pending", detail: "", progress: null, startedAt: null, etaSec: null },
|
|
948
|
-
},
|
|
960
|
+
// Stage checklist · seeded according to the brief mode (see
|
|
961
|
+
// briefMode resolution above). brief-stage events flip
|
|
962
|
+
// them active → done as the pipeline progresses.
|
|
963
|
+
// startedAt is captured when each stage first becomes
|
|
964
|
+
// active so the UI can display elapsed time alongside the
|
|
965
|
+
// ETA range.
|
|
966
|
+
stages: seededStages,
|
|
949
967
|
};
|
|
950
968
|
this.currentBrief = newBrief;
|
|
951
969
|
// Insert the in-progress brief into currentBriefs so the tab
|
|
@@ -1076,10 +1094,14 @@
|
|
|
1076
1094
|
}
|
|
1077
1095
|
}
|
|
1078
1096
|
}
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1097
|
+
// No typing-SFX during the brief writer's Stage 3 stream.
|
|
1098
|
+
// The write phase is a long, off-screen streaming pass (the
|
|
1099
|
+
// user is usually reading prior content or doing something
|
|
1100
|
+
// else while the report builds) — a continuous click track
|
|
1101
|
+
// for tens of seconds reads as background noise, not a
|
|
1102
|
+
// presence cue. Director / chair turns still tick because
|
|
1103
|
+
// those land in the active conversation the user is
|
|
1104
|
+
// following.
|
|
1083
1105
|
} else if (kind === "brief-final") {
|
|
1084
1106
|
this.markBriefEvent();
|
|
1085
1107
|
const target = this._briefById(payload.briefId);
|
|
@@ -1484,6 +1506,192 @@
|
|
|
1484
1506
|
return false;
|
|
1485
1507
|
},
|
|
1486
1508
|
|
|
1509
|
+
/** Has the brief got its body content yet? Mode-aware check ·
|
|
1510
|
+
* research-note briefs land their content in `bodyMd`; bento
|
|
1511
|
+
* briefs land theirs in `bodyJson` and leave bodyMd empty. The
|
|
1512
|
+
* card-rendering / placeholder-detection / generating-check
|
|
1513
|
+
* logic uses this everywhere so a bento brief doesn't stay
|
|
1514
|
+
* stuck in "generating…" state after the BentoScaffold has
|
|
1515
|
+
* actually been persisted.
|
|
1516
|
+
*
|
|
1517
|
+
* Without this helper, a successful bento generation followed
|
|
1518
|
+
* by a page refresh hid the View Report button forever (the
|
|
1519
|
+
* card thought it was still generating because bodyMd was empty
|
|
1520
|
+
* — which is the steady-state shape for bento, not an
|
|
1521
|
+
* in-flight signal). */
|
|
1522
|
+
/** True for any structured-output mode — bento, magazine,
|
|
1523
|
+
* newspaper — that persists to body_json instead of body_md and
|
|
1524
|
+
* runs the single-pass chair-LLM pipeline (extract + write only,
|
|
1525
|
+
* no Stage 2/3 scaffold/write). Centralised so future modes
|
|
1526
|
+
* touch one place. */
|
|
1527
|
+
isStructuredBriefMode(mode) {
|
|
1528
|
+
return mode === "bento" || mode === "magazine" || mode === "newspaper";
|
|
1529
|
+
},
|
|
1530
|
+
|
|
1531
|
+
briefHasBody(b) {
|
|
1532
|
+
if (!b) return false;
|
|
1533
|
+
if (this.isStructuredBriefMode(b.mode)) {
|
|
1534
|
+
// Bento + magazine populate bodyJson with the BentoScaffold.
|
|
1535
|
+
// An empty object isn't a body; check for at least a title
|
|
1536
|
+
// field.
|
|
1537
|
+
const j = b.bodyJson;
|
|
1538
|
+
return !!(j && typeof j === "object" && (j.title || j.milestones));
|
|
1539
|
+
}
|
|
1540
|
+
// Default research-note · body lives in markdown.
|
|
1541
|
+
return !!(b.bodyMd && b.bodyMd.trim());
|
|
1542
|
+
},
|
|
1543
|
+
|
|
1544
|
+
/** Build the right viewer URL for a brief based on its mode.
|
|
1545
|
+
* · 'research-note' (default · or unknown) → `/report.html?r=R&b=B`
|
|
1546
|
+
* · 'bento' → `/bento.html?b=B`
|
|
1547
|
+
* · 'magazine' → `/magazine.html?b=B`
|
|
1548
|
+
* · 'newspaper' → `/newspaper.html?b=B`
|
|
1549
|
+
*
|
|
1550
|
+
* Structured renderers don't need the room context · the brief
|
|
1551
|
+
* is self-contained. Used by the View Report button, the
|
|
1552
|
+
* brief-picker popover, the open-report link in the brief card,
|
|
1553
|
+
* and the All Reports page — one helper so a future renderer
|
|
1554
|
+
* route change touches one place. */
|
|
1555
|
+
briefViewerHref(b, roomId) {
|
|
1556
|
+
if (!b || !b.id) return null;
|
|
1557
|
+
const id = encodeURIComponent(b.id);
|
|
1558
|
+
if (b.mode === "bento") return `/bento.html?b=${id}`;
|
|
1559
|
+
if (b.mode === "magazine") return `/magazine.html?b=${id}`;
|
|
1560
|
+
if (b.mode === "newspaper") return `/newspaper.html?b=${id}`;
|
|
1561
|
+
const r = roomId ? encodeURIComponent(roomId) : "";
|
|
1562
|
+
return r ? `/report.html?r=${r}&b=${id}` : `/report.html?b=${id}`;
|
|
1563
|
+
},
|
|
1564
|
+
|
|
1565
|
+
/** Human-readable label for a brief's mode · used in the brief-card
|
|
1566
|
+
* banner so the user sees which renderer the report will use
|
|
1567
|
+
* before they open it.
|
|
1568
|
+
*
|
|
1569
|
+
* IMPORTANT · the user-facing label for `research-note` is
|
|
1570
|
+
* "Report", NOT "Research Note". The internal mode key stays as
|
|
1571
|
+
* `research-note` (storage column, API value, picker option id),
|
|
1572
|
+
* but every user-facing surface — the mode picker (`renderBrief-
|
|
1573
|
+
* ModePicker` line ~1662 in this file), the report viewer's
|
|
1574
|
+
* header, etc. — calls it "Report". Keep this map in sync with
|
|
1575
|
+
* the picker labels rather than inventing a fresh user-facing
|
|
1576
|
+
* vocabulary.
|
|
1577
|
+
*
|
|
1578
|
+
* Legacy rows without an explicit `mode` default to "Report" by
|
|
1579
|
+
* the same backfill the storage layer applies. */
|
|
1580
|
+
briefModeLabel(b) {
|
|
1581
|
+
const mode = (b && b.mode) || "research-note";
|
|
1582
|
+
switch (mode) {
|
|
1583
|
+
case "bento": return "Bento";
|
|
1584
|
+
case "magazine": return "Magazine";
|
|
1585
|
+
case "newspaper": return "Newspaper";
|
|
1586
|
+
default: return "Report";
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
|
|
1590
|
+
/** Render the report-mode picker · four icon-tile cards that let
|
|
1591
|
+
* the user pick between research-note, bento, magazine, and
|
|
1592
|
+
* newspaper. Each tile shows a custom monoline SVG that
|
|
1593
|
+
* visually mirrors that mode's layout — research-note as a
|
|
1594
|
+
* memo, bento as a 4-cell grid, magazine as a masthead +
|
|
1595
|
+
* numeral block + small cards, newspaper as a banner + 3-column
|
|
1596
|
+
* text grid. The native radio is visually hidden (kept in the
|
|
1597
|
+
* DOM for a11y / keyboard nav).
|
|
1598
|
+
*
|
|
1599
|
+
* Used by both the adjourn overlay (filing the report at
|
|
1600
|
+
* adjourn-time / generate-brief flow) AND the supplement
|
|
1601
|
+
* overlay (regenerating with an extra perspective). The picker
|
|
1602
|
+
* reads its selected option via `input[name="brief-mode"]:checked`
|
|
1603
|
+
* from inside whatever overlay it lives in · the click-to-toggle
|
|
1604
|
+
* handler scopes to the `.adjourn-mode-options` container so it
|
|
1605
|
+
* works for any overlay embedding the picker. */
|
|
1606
|
+
renderBriefModePicker(defaultMode) {
|
|
1607
|
+
const safe = this.isStructuredBriefMode(defaultMode) ? defaultMode : "research-note";
|
|
1608
|
+
// Icon SVGs · `currentColor` lets each tile pick up its hover /
|
|
1609
|
+
// selected accent from the parent `.adjourn-mode-icon { color }`
|
|
1610
|
+
// rule. Stroke hierarchy is deliberate · main outlines at 1.0,
|
|
1611
|
+
// a single emphasis rule at 1.2 (newspaper headline + magazine
|
|
1612
|
+
// masthead bar), body details at 0.8. Filled blocks soften to
|
|
1613
|
+
// currentColor at 0.5 opacity so the lime selected-state reads
|
|
1614
|
+
// as a tint rather than a slab. The 24-unit viewBox renders at
|
|
1615
|
+
// 36px on screen → 1.5× scale, so stroke-1.0 is a clean 1.5px.
|
|
1616
|
+
const ICONS = {
|
|
1617
|
+
"research-note": `
|
|
1618
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1619
|
+
<path d="M6 3h9l4 4v14H6z"/>
|
|
1620
|
+
<path d="M15 3v4h4"/>
|
|
1621
|
+
<line x1="9" y1="12" x2="16" y2="12"/>
|
|
1622
|
+
<line x1="9" y1="15" x2="16" y2="15"/>
|
|
1623
|
+
<line x1="9" y1="18" x2="14" y2="18"/>
|
|
1624
|
+
</svg>`,
|
|
1625
|
+
"bento": `
|
|
1626
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1627
|
+
<rect x="3" y="3" width="9" height="13" rx="1.5"/>
|
|
1628
|
+
<rect x="13" y="3" width="8" height="6" rx="1.5"/>
|
|
1629
|
+
<rect x="13" y="10" width="8" height="6" rx="1.5"/>
|
|
1630
|
+
<rect x="3" y="17" width="18" height="4" rx="1.5"/>
|
|
1631
|
+
</svg>`,
|
|
1632
|
+
"magazine": `
|
|
1633
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1634
|
+
<line x1="3" y1="4" x2="21" y2="4" stroke-width="1.2"/>
|
|
1635
|
+
<rect x="3" y="7" width="6" height="9" rx="1.2"/>
|
|
1636
|
+
<rect x="11" y="7" width="4.5" height="4" rx="1.2"/>
|
|
1637
|
+
<rect x="16.5" y="7" width="4.5" height="4" rx="1.2"/>
|
|
1638
|
+
<rect x="11" y="12" width="4.5" height="4" rx="1.2"/>
|
|
1639
|
+
<rect x="16.5" y="12" width="4.5" height="4" rx="1.2"/>
|
|
1640
|
+
<rect x="3" y="18" width="18" height="3" rx="1" fill="currentColor" fill-opacity="0.45" stroke="none"/>
|
|
1641
|
+
</svg>`,
|
|
1642
|
+
"newspaper": `
|
|
1643
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1644
|
+
<rect x="3" y="3" width="18" height="2.5" fill="currentColor" fill-opacity="0.85" stroke="none"/>
|
|
1645
|
+
<line x1="7" y1="8" x2="17" y2="8" stroke-width="1.2"/>
|
|
1646
|
+
<line x1="4" y1="11.5" x2="8" y2="11.5" stroke-width="0.8"/>
|
|
1647
|
+
<line x1="4" y1="13.5" x2="8" y2="13.5" stroke-width="0.8"/>
|
|
1648
|
+
<line x1="4" y1="15.5" x2="6.5" y2="15.5" stroke-width="0.8"/>
|
|
1649
|
+
<line x1="10" y1="11.5" x2="14" y2="11.5" stroke-width="0.8"/>
|
|
1650
|
+
<line x1="10" y1="13.5" x2="14" y2="13.5" stroke-width="0.8"/>
|
|
1651
|
+
<line x1="10" y1="15.5" x2="12.5" y2="15.5" stroke-width="0.8"/>
|
|
1652
|
+
<line x1="16" y1="11.5" x2="20" y2="11.5" stroke-width="0.8"/>
|
|
1653
|
+
<line x1="16" y1="13.5" x2="20" y2="13.5" stroke-width="0.8"/>
|
|
1654
|
+
<line x1="16" y1="15.5" x2="18.5" y2="15.5" stroke-width="0.8"/>
|
|
1655
|
+
<line x1="3" y1="18.5" x2="21" y2="18.5" stroke-width="0.9"/>
|
|
1656
|
+
</svg>`,
|
|
1657
|
+
};
|
|
1658
|
+
const opt = (value, title, deck, primary) => `
|
|
1659
|
+
<label class="adjourn-mode-option${primary ? " adjourn-mode-option-primary" : ""}${safe === value ? " on" : ""}">
|
|
1660
|
+
<input type="radio" name="brief-mode" value="${value}"${safe === value ? " checked" : ""}>
|
|
1661
|
+
<div class="adjourn-mode-icon">${ICONS[value] || ""}</div>
|
|
1662
|
+
<div class="adjourn-mode-body">
|
|
1663
|
+
<div class="adjourn-mode-title">${title}</div>
|
|
1664
|
+
<div class="adjourn-mode-deck">${deck}</div>
|
|
1665
|
+
</div>
|
|
1666
|
+
</label>`;
|
|
1667
|
+
return `
|
|
1668
|
+
<div class="adjourn-mode-picker" data-mode-picker>
|
|
1669
|
+
<div class="adjourn-mode-label">// report format</div>
|
|
1670
|
+
<div class="adjourn-mode-options adjourn-mode-options-4">
|
|
1671
|
+
${opt("research-note", "Report", "Long-form markdown · bottom line, findings, recommendations.", true)}
|
|
1672
|
+
${opt("bento", "Bento", "Single-page poster · 3 milestones + sidebar cards.")}
|
|
1673
|
+
${opt("magazine", "Magazine", "Editorial spread · cover line, 5 cards, dark closer.")}
|
|
1674
|
+
${opt("newspaper", "Newspaper", "Broadsheet · banner masthead, 3-column editorial.")}
|
|
1675
|
+
</div>
|
|
1676
|
+
</div>`;
|
|
1677
|
+
},
|
|
1678
|
+
|
|
1679
|
+
/** Read the user's last-picked report mode from localStorage so the
|
|
1680
|
+
* picker defaults to their previous choice. Falls back to
|
|
1681
|
+
* 'research-note' on first run / private mode. */
|
|
1682
|
+
lastBriefMode() {
|
|
1683
|
+
try {
|
|
1684
|
+
const v = localStorage.getItem("pb.briefMode");
|
|
1685
|
+
return this.isStructuredBriefMode(v) ? v : "research-note";
|
|
1686
|
+
} catch { return "research-note"; }
|
|
1687
|
+
},
|
|
1688
|
+
saveLastBriefMode(mode) {
|
|
1689
|
+
try {
|
|
1690
|
+
const safe = this.isStructuredBriefMode(mode) ? mode : "research-note";
|
|
1691
|
+
localStorage.setItem("pb.briefMode", safe);
|
|
1692
|
+
} catch { /* private-mode etc. — silently ignore */ }
|
|
1693
|
+
},
|
|
1694
|
+
|
|
1487
1695
|
/** Pure cache read · returns true when the local app.keys map
|
|
1488
1696
|
* reports any model provider as configured. Used by the gate
|
|
1489
1697
|
* and (via wrappers) anywhere downstream UI needs the answer
|
|
@@ -1647,6 +1855,9 @@
|
|
|
1647
1855
|
async adjournRoom(opts) {
|
|
1648
1856
|
if (!this.currentRoomId) return;
|
|
1649
1857
|
const skipBrief = !!(opts && opts.skipBrief);
|
|
1858
|
+
const mode = opts && this.isStructuredBriefMode(opts.mode)
|
|
1859
|
+
? opts.mode
|
|
1860
|
+
: "research-note";
|
|
1650
1861
|
// Pre-flight · adjourn-with-brief triggers the brief writer
|
|
1651
1862
|
// pipeline (3 LLM stages). When skipBrief=true the chair just
|
|
1652
1863
|
// posts the no-brief marker without LLM calls, so we let it
|
|
@@ -1657,7 +1868,7 @@
|
|
|
1657
1868
|
{
|
|
1658
1869
|
method: "POST",
|
|
1659
1870
|
headers: { "content-type": "application/json" },
|
|
1660
|
-
body: JSON.stringify(skipBrief ? { skipBrief: true } : {}),
|
|
1871
|
+
body: JSON.stringify(skipBrief ? { skipBrief: true } : { mode }),
|
|
1661
1872
|
},
|
|
1662
1873
|
);
|
|
1663
1874
|
if (!r.ok) {
|
|
@@ -1691,6 +1902,10 @@
|
|
|
1691
1902
|
const confirmTxt = isGen ? "[ Generate ]" : "[ Adjourn & file ]";
|
|
1692
1903
|
const subjectTxt = room.subject || room.name || "—";
|
|
1693
1904
|
const memberCount = (this.currentMembers || []).length;
|
|
1905
|
+
// Default the mode picker to whatever the user picked last. Bento
|
|
1906
|
+
// is opt-in · 'research-note' is the safe initial default for new
|
|
1907
|
+
// users / private-mode browsers (where localStorage is empty).
|
|
1908
|
+
const defaultMode = this.lastBriefMode();
|
|
1694
1909
|
const html = `
|
|
1695
1910
|
<div class="adjourn-overlay" id="adjourn-overlay" role="dialog" aria-modal="true" data-adjourn-mode="${this.escape(mode)}">
|
|
1696
1911
|
<div class="adjourn-backdrop" data-adjourn-close></div>
|
|
@@ -1711,9 +1926,12 @@
|
|
|
1711
1926
|
|
|
1712
1927
|
<div class="adjourn-body">
|
|
1713
1928
|
<div class="adjourn-summary">
|
|
1714
|
-
<div class="adjourn-summary-row">
|
|
1929
|
+
<div class="adjourn-summary-row adjourn-summary-row-subject">
|
|
1715
1930
|
<span class="adjourn-summary-key">// subject</span>
|
|
1716
|
-
<
|
|
1931
|
+
<div class="adjourn-summary-val adjourn-subject-wrap">
|
|
1932
|
+
<span class="adjourn-subject-text is-clamped" data-adjourn-subject>${this.escape(subjectTxt)}</span>
|
|
1933
|
+
<button type="button" class="adjourn-subject-toggle" data-adjourn-subject-toggle hidden>Show more</button>
|
|
1934
|
+
</div>
|
|
1717
1935
|
</div>
|
|
1718
1936
|
<div class="adjourn-summary-row">
|
|
1719
1937
|
<span class="adjourn-summary-key">// authors</span>
|
|
@@ -1725,10 +1943,12 @@
|
|
|
1725
1943
|
</div>
|
|
1726
1944
|
</div>
|
|
1727
1945
|
<p class="adjourn-summary-note">
|
|
1728
|
-
The chair compiles a
|
|
1729
|
-
|
|
1730
|
-
|
|
1946
|
+
The chair compiles a report from the room's transcript. Pick the
|
|
1947
|
+
format below — the room is marked adjourned and the report is
|
|
1948
|
+
filed in the chat once it's ready.
|
|
1731
1949
|
</p>
|
|
1950
|
+
|
|
1951
|
+
${this.renderBriefModePicker(defaultMode)}
|
|
1732
1952
|
</div>
|
|
1733
1953
|
|
|
1734
1954
|
<footer class="adjourn-foot">
|
|
@@ -1749,6 +1969,19 @@
|
|
|
1749
1969
|
wrap.innerHTML = html.trim();
|
|
1750
1970
|
document.body.appendChild(wrap.firstChild);
|
|
1751
1971
|
document.body.style.overflow = "hidden";
|
|
1972
|
+
// Subject clamp · the subject value is clamped to 3 lines by
|
|
1973
|
+
// default (a long room title shouldn't crowd the rest of the
|
|
1974
|
+
// overlay). After mount, measure whether the text actually
|
|
1975
|
+
// overflows the clamp — only then reveal the Show more / less
|
|
1976
|
+
// toggle. rAF lets the browser settle the initial layout so
|
|
1977
|
+
// scrollHeight is meaningful.
|
|
1978
|
+
requestAnimationFrame(() => {
|
|
1979
|
+
const subjEl = document.querySelector("[data-adjourn-subject]");
|
|
1980
|
+
const subjBtn = document.querySelector("[data-adjourn-subject-toggle]");
|
|
1981
|
+
if (subjEl && subjBtn && subjEl.scrollHeight > subjEl.clientHeight + 1) {
|
|
1982
|
+
subjBtn.hidden = false;
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1752
1985
|
// Esc closes the overlay. Listener auto-detaches on close so
|
|
1753
1986
|
// we don't accumulate handlers across opens.
|
|
1754
1987
|
this._adjournEsc = (ev) => {
|
|
@@ -1788,6 +2021,14 @@
|
|
|
1788
2021
|
confirm: "[ Regenerate ]",
|
|
1789
2022
|
confirmBusy: "[ Regenerating… ]",
|
|
1790
2023
|
};
|
|
2024
|
+
// Default the picker to the existing brief's mode · the user's
|
|
2025
|
+
// baseline assumption when regenerating with a supplement is "I
|
|
2026
|
+
// want the same kind of report, just with this extra angle." If
|
|
2027
|
+
// the parent brief's mode is missing for any reason, fall back
|
|
2028
|
+
// to the user's last picked mode (localStorage).
|
|
2029
|
+
const defaultMode = this.isStructuredBriefMode(this.currentBrief?.mode)
|
|
2030
|
+
? this.currentBrief.mode
|
|
2031
|
+
: this.lastBriefMode();
|
|
1791
2032
|
const html = `
|
|
1792
2033
|
<div class="supplement-overlay" id="supplement-overlay" role="dialog" aria-modal="true">
|
|
1793
2034
|
<div class="supplement-backdrop" data-supplement-close></div>
|
|
@@ -1806,6 +2047,7 @@
|
|
|
1806
2047
|
<div class="supplement-body">
|
|
1807
2048
|
<textarea class="supplement-input" data-supplement-input rows="6" placeholder="${this.escape(t.placeholder)}"></textarea>
|
|
1808
2049
|
<p class="supplement-hint">${this.escape(t.hint)}</p>
|
|
2050
|
+
${this.renderBriefModePicker(defaultMode)}
|
|
1809
2051
|
</div>
|
|
1810
2052
|
<footer class="supplement-foot">
|
|
1811
2053
|
<button type="button" class="supplement-cancel" data-supplement-close>${this.escape(t.cancel)}</button>
|
|
@@ -2455,6 +2697,17 @@
|
|
|
2455
2697
|
if (input) input.focus();
|
|
2456
2698
|
return;
|
|
2457
2699
|
}
|
|
2700
|
+
// Read the report-mode picker (research-note / bento). The
|
|
2701
|
+
// overlay defaults the picker to the existing brief's mode, so a
|
|
2702
|
+
// plain "regenerate with this perspective" keeps the same
|
|
2703
|
+
// format. Persisting the choice keeps the picker stable next
|
|
2704
|
+
// time. Server-side the same `/api/rooms/:id/brief` route reads
|
|
2705
|
+
// `mode` regardless of whether the call came from supplement or
|
|
2706
|
+
// generate-brief, so no backend change is needed.
|
|
2707
|
+
const briefModeInput = overlay.querySelector('input[name="brief-mode"]:checked');
|
|
2708
|
+
const v = briefModeInput && briefModeInput.value;
|
|
2709
|
+
const briefMode = this.isStructuredBriefMode(v) ? v : "research-note";
|
|
2710
|
+
this.saveLastBriefMode(briefMode);
|
|
2458
2711
|
// Frontend in-flight guard · without this, a slow server roundtrip
|
|
2459
2712
|
// gives the user time to click confirm twice (or to close+reopen
|
|
2460
2713
|
// the overlay and click again). Each click was firing its own POST,
|
|
@@ -2473,7 +2726,7 @@
|
|
|
2473
2726
|
{
|
|
2474
2727
|
method: "POST",
|
|
2475
2728
|
headers: { "content-type": "application/json" },
|
|
2476
|
-
body: JSON.stringify({ supplement: text }),
|
|
2729
|
+
body: JSON.stringify({ supplement: text, mode: briefMode }),
|
|
2477
2730
|
},
|
|
2478
2731
|
);
|
|
2479
2732
|
if (!r.ok) {
|
|
@@ -2504,7 +2757,7 @@
|
|
|
2504
2757
|
* whether streaming is actually in flight. */
|
|
2505
2758
|
isBriefPlaceholder(brief, room) {
|
|
2506
2759
|
if (!brief) return false;
|
|
2507
|
-
if (
|
|
2760
|
+
if (this.briefHasBody(brief)) return false;
|
|
2508
2761
|
if (!room) return true;
|
|
2509
2762
|
// Title matches the seed (room subject) → never got an updated title.
|
|
2510
2763
|
// Or title is the literal "Generating…" placeholder.
|
|
@@ -2621,7 +2874,7 @@
|
|
|
2621
2874
|
const target = (this.currentBriefs || []).find((b) => b.id === briefId);
|
|
2622
2875
|
// System UI · always English. Confirm + alert dialogs are app
|
|
2623
2876
|
// chrome and stay fixed-string regardless of brief language.
|
|
2624
|
-
const isStillGenerating = !!(target &&
|
|
2877
|
+
const isStillGenerating = !!(target && !this.briefHasBody(target));
|
|
2625
2878
|
const confirmText = isStillGenerating
|
|
2626
2879
|
? "This report is still generating. Deleting will stop the generation and remove the report. This can't be undone."
|
|
2627
2880
|
: (target?.supplement
|
|
@@ -2723,17 +2976,23 @@
|
|
|
2723
2976
|
const mode = overlay.getAttribute("data-adjourn-mode") || "adjourn";
|
|
2724
2977
|
const isGen = mode === "generate-brief";
|
|
2725
2978
|
const skipPicked = overlay.querySelector(".adjourn-skip-btn.picked") !== null;
|
|
2979
|
+
// Read the report-mode picker (research-note / bento). Persist the
|
|
2980
|
+
// choice so the picker defaults to the same option next time.
|
|
2981
|
+
const briefModeInput = overlay.querySelector('input[name="brief-mode"]:checked');
|
|
2982
|
+
const v = briefModeInput && briefModeInput.value;
|
|
2983
|
+
const briefMode = this.isStructuredBriefMode(v) ? v : "research-note";
|
|
2984
|
+
this.saveLastBriefMode(briefMode);
|
|
2726
2985
|
const btn = overlay.querySelector("[data-adjourn-confirm]");
|
|
2727
2986
|
const origLabel = isGen ? "[ Generate ]" : "[ Adjourn & file ]";
|
|
2728
2987
|
const busyLabel = isGen ? "[ Generating… ]" : "[ Adjourning… ]";
|
|
2729
2988
|
if (btn) { btn.disabled = true; btn.textContent = busyLabel; }
|
|
2730
2989
|
try {
|
|
2731
2990
|
if (isGen) {
|
|
2732
|
-
await this.generateBriefForAdjournedRoom();
|
|
2991
|
+
await this.generateBriefForAdjournedRoom(briefMode);
|
|
2733
2992
|
} else if (skipPicked) {
|
|
2734
2993
|
await this.adjournRoom({ skipBrief: true });
|
|
2735
2994
|
} else {
|
|
2736
|
-
await this.adjournRoom({});
|
|
2995
|
+
await this.adjournRoom({ mode: briefMode });
|
|
2737
2996
|
}
|
|
2738
2997
|
this.closeAdjournOverlay();
|
|
2739
2998
|
} catch (e) {
|
|
@@ -2746,8 +3005,9 @@
|
|
|
2746
3005
|
* whose user originally skipped the brief. Server emits the same
|
|
2747
3006
|
* brief-started / brief-token / brief-final SSE events as a normal
|
|
2748
3007
|
* adjourn, so the existing handlers in connectSSE handle the rest. */
|
|
2749
|
-
async generateBriefForAdjournedRoom() {
|
|
3008
|
+
async generateBriefForAdjournedRoom(mode) {
|
|
2750
3009
|
if (!this.currentRoomId) return;
|
|
3010
|
+
const briefMode = this.isStructuredBriefMode(mode) ? mode : "research-note";
|
|
2751
3011
|
// Pre-flight · the brief writer is a 3-stage LLM pipeline (per-
|
|
2752
3012
|
// director extract → composer → final write). All require a key.
|
|
2753
3013
|
if (!(await this.requireModelKey())) return;
|
|
@@ -2756,7 +3016,7 @@
|
|
|
2756
3016
|
{
|
|
2757
3017
|
method: "POST",
|
|
2758
3018
|
headers: { "content-type": "application/json" },
|
|
2759
|
-
body:
|
|
3019
|
+
body: JSON.stringify({ mode: briefMode }),
|
|
2760
3020
|
},
|
|
2761
3021
|
);
|
|
2762
3022
|
if (!r.ok) {
|
|
@@ -6610,7 +6870,7 @@
|
|
|
6610
6870
|
year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
6611
6871
|
})
|
|
6612
6872
|
: "";
|
|
6613
|
-
const href =
|
|
6873
|
+
const href = this.briefViewerHref(b, roomId);
|
|
6614
6874
|
return `
|
|
6615
6875
|
<a class="brief-picker-row" href="${this.escape(href)}" target="_blank" rel="noopener" data-brief-picker-row data-brief-id="${this.escape(b.id)}">
|
|
6616
6876
|
<span class="brief-picker-num">${this.escape(num)}</span>
|
|
@@ -7280,7 +7540,7 @@
|
|
|
7280
7540
|
// opens the picker.
|
|
7281
7541
|
const briefs = Array.isArray(this.currentBriefs) ? this.currentBriefs : [];
|
|
7282
7542
|
const multi = briefs.length > 1;
|
|
7283
|
-
const directHref =
|
|
7543
|
+
const directHref = this.briefViewerHref(this.currentBrief, r.id) || `/report.html?r=${this.escape(r.id)}`;
|
|
7284
7544
|
if (!multi) {
|
|
7285
7545
|
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report>[ View Report ]</a>`;
|
|
7286
7546
|
}
|
|
@@ -8722,7 +8982,7 @@
|
|
|
8722
8982
|
// a failed-brief tab becomes unreachable — clicking it just
|
|
8723
8983
|
// re-renders whichever good brief the salvage path picks.
|
|
8724
8984
|
const goodBrief = bypassSalvage ? null : (this.currentBriefs || []).find(
|
|
8725
|
-
(x) => x && x.id !== b.id && !x.error &&
|
|
8985
|
+
(x) => x && x.id !== b.id && !x.error && this.briefHasBody(x),
|
|
8726
8986
|
);
|
|
8727
8987
|
if (goodBrief) {
|
|
8728
8988
|
const failed = b;
|
|
@@ -8782,6 +9042,7 @@
|
|
|
8782
9042
|
${tabsStripHtml}
|
|
8783
9043
|
<div class="brief-banner">
|
|
8784
9044
|
<span class="brief-banner-tag" style="color: var(--red);">// report</span>
|
|
9045
|
+
<span class="brief-banner-type" style="color: var(--red); border-color: var(--red);">${this.escape(this.briefModeLabel(b))}</span>
|
|
8785
9046
|
<span class="brief-banner-stamp" style="color: var(--red);">${this.escape(copy.stamp)}</span>
|
|
8786
9047
|
</div>
|
|
8787
9048
|
<div class="brief-body brief-body-error">
|
|
@@ -8804,7 +9065,7 @@
|
|
|
8804
9065
|
return;
|
|
8805
9066
|
}
|
|
8806
9067
|
|
|
8807
|
-
const generating = b.isGenerating === true || !b
|
|
9068
|
+
const generating = b.isGenerating === true || !this.briefHasBody(b) || b.title === "Generating…";
|
|
8808
9069
|
const signed = this.currentMembers
|
|
8809
9070
|
.map((a) => `<img src="${this.escape(a.avatarPath)}" alt="${this.escape(a.name)}" title="${this.escape(a.name)}">`)
|
|
8810
9071
|
.join("");
|
|
@@ -8813,12 +9074,12 @@
|
|
|
8813
9074
|
? "GENERATING…"
|
|
8814
9075
|
: "FILED · " + (b.createdAt ? this.timeFmt(b.createdAt) : this.timeFmt(Date.now()));
|
|
8815
9076
|
|
|
8816
|
-
// Open Report
|
|
8817
|
-
//
|
|
8818
|
-
//
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
9077
|
+
// Open Report URL · routes to /bento.html for bento briefs and
|
|
9078
|
+
// /report.html for research-note briefs (default). The bento
|
|
9079
|
+
// renderer doesn't need the room id so the URL is shorter for
|
|
9080
|
+
// that mode.
|
|
9081
|
+
const reportHref = this.briefViewerHref(b, this.currentRoomId)
|
|
9082
|
+
|| (this.currentRoomId ? `/report.html?r=${encodeURIComponent(this.currentRoomId)}` : null);
|
|
8822
9083
|
|
|
8823
9084
|
// Tab strip — already computed at the top of renderBrief as
|
|
8824
9085
|
// `tabsStripHtml` so both error and success paths can mount it.
|
|
@@ -8837,6 +9098,7 @@
|
|
|
8837
9098
|
${tabsHtml}
|
|
8838
9099
|
<div class="brief-banner">
|
|
8839
9100
|
<span class="brief-banner-tag">// report</span>
|
|
9101
|
+
<span class="brief-banner-type">${this.escape(this.briefModeLabel(b))}</span>
|
|
8840
9102
|
<span class="brief-banner-stamp">${filedLabel}</span>
|
|
8841
9103
|
</div>
|
|
8842
9104
|
|
|
@@ -8955,11 +9217,48 @@
|
|
|
8955
9217
|
"Writing the 3 Headline Findings",
|
|
8956
9218
|
"Drafting Convergence + Divergence sections",
|
|
8957
9219
|
"Composing Recommendations",
|
|
8958
|
-
"Writing the Pre-mortem",
|
|
8959
9220
|
"Surfacing New Questions",
|
|
8960
9221
|
"Drafting the Strategic Planning Assumption",
|
|
8961
9222
|
"Polishing the final pass",
|
|
8962
9223
|
],
|
|
9224
|
+
// Bento's `write` stage does different work · one chair-LLM
|
|
9225
|
+
// call that compresses the room into the 8-slot bento layout.
|
|
9226
|
+
// Keyed under `bento-write` and selected at render time when
|
|
9227
|
+
// the brief's mode is bento.
|
|
9228
|
+
"bento-write": [
|
|
9229
|
+
"Compressing to the takeaway · the 1-line title",
|
|
9230
|
+
"Picking the 3 milestones",
|
|
9231
|
+
"Sizing the big-number callouts",
|
|
9232
|
+
"Mapping ranked bars from the room's data",
|
|
9233
|
+
"Distilling the verification signals",
|
|
9234
|
+
"Compressing recommendations into elevator-pitch lines",
|
|
9235
|
+
"Naming the closing flow · before → after",
|
|
9236
|
+
],
|
|
9237
|
+
// Magazine mode · same single-pass chair-LLM call but the
|
|
9238
|
+
// emphasis is on editorial cover-line composition rather
|
|
9239
|
+
// than infographic compression. Keyed under `magazine-write`
|
|
9240
|
+
// and selected at render time when the brief's mode is
|
|
9241
|
+
// magazine.
|
|
9242
|
+
"magazine-write": [
|
|
9243
|
+
"Drafting the cover headline",
|
|
9244
|
+
"Writing the subdeck",
|
|
9245
|
+
"Picking the 5 numbered cards",
|
|
9246
|
+
"Sequencing the 3 setup steps",
|
|
9247
|
+
"Selecting why-this-matters reasons",
|
|
9248
|
+
"Stamping the masthead byline",
|
|
9249
|
+
],
|
|
9250
|
+
// Newspaper mode · same single-pass chair-LLM call but voice
|
|
9251
|
+
// shifts to broadsheet front-page journalism. Keyed under
|
|
9252
|
+
// `newspaper-write` and selected at render time when the
|
|
9253
|
+
// brief's mode is newspaper.
|
|
9254
|
+
"newspaper-write": [
|
|
9255
|
+
"Setting the banner headline",
|
|
9256
|
+
"Writing the subdeck",
|
|
9257
|
+
"Filing the three column stories",
|
|
9258
|
+
"Drafting the bottom-line callout",
|
|
9259
|
+
"Stacking the more-headings sidebar",
|
|
9260
|
+
"Stamping the masthead date",
|
|
9261
|
+
],
|
|
8963
9262
|
},
|
|
8964
9263
|
// System UI · always English. The `zh` key is kept as an alias
|
|
8965
9264
|
// of `en` so any caller indexing BRIEF_SUBSTAGES["zh"] still
|
|
@@ -8980,7 +9279,7 @@
|
|
|
8980
9279
|
}
|
|
8981
9280
|
const stages = b.stages || {};
|
|
8982
9281
|
const anyActive = Object.values(stages).some((s) => s && s.status === "active");
|
|
8983
|
-
const generating = b.isGenerating === true || !b
|
|
9282
|
+
const generating = b.isGenerating === true || !this.briefHasBody(b) || b.title === "Generating…";
|
|
8984
9283
|
if (!anyActive && !generating) {
|
|
8985
9284
|
this.stopBriefStageTick();
|
|
8986
9285
|
return;
|
|
@@ -9031,7 +9330,7 @@
|
|
|
9031
9330
|
if (this._briefStallWatchTimer) return;
|
|
9032
9331
|
const b = this.currentBrief;
|
|
9033
9332
|
if (!b || !b.id || b.error) return;
|
|
9034
|
-
const generating = b.isGenerating === true || !b
|
|
9333
|
+
const generating = b.isGenerating === true || !this.briefHasBody(b) || b.title === "Generating…";
|
|
9035
9334
|
if (!generating) return;
|
|
9036
9335
|
if (!this._lastBriefEventAt) this._lastBriefEventAt = Date.now();
|
|
9037
9336
|
this._lastBriefHealthPollAt = 0;
|
|
@@ -9051,7 +9350,7 @@
|
|
|
9051
9350
|
async tickBriefStallWatch() {
|
|
9052
9351
|
const b = this.currentBrief;
|
|
9053
9352
|
if (!b || b.error) { this.stopBriefStallWatch(); return; }
|
|
9054
|
-
const generating = b.isGenerating === true || !b
|
|
9353
|
+
const generating = b.isGenerating === true || !this.briefHasBody(b) || b.title === "Generating…";
|
|
9055
9354
|
if (!generating) { this.stopBriefStallWatch(); return; }
|
|
9056
9355
|
|
|
9057
9356
|
const now = Date.now();
|
|
@@ -9112,27 +9411,43 @@
|
|
|
9112
9411
|
? (b.bodyMd.trim().match(/\S+/g) || []).length
|
|
9113
9412
|
: 0;
|
|
9114
9413
|
|
|
9115
|
-
// Stage definitions ·
|
|
9116
|
-
//
|
|
9414
|
+
// Stage definitions · mode-aware. The wire format emitted by
|
|
9415
|
+
// emitStage() in src/orchestrator/brief.ts depends on which
|
|
9416
|
+
// pipeline ran:
|
|
9117
9417
|
//
|
|
9118
|
-
// extract → compose → scaffold-
|
|
9418
|
+
// research-note (default) · extract → compose → scaffold-
|
|
9419
|
+
// {anchor,findings,cluster,actions} → write (7 stages · the
|
|
9420
|
+
// 4 scaffold sub-stages are driven by JSON-key arrival in
|
|
9421
|
+
// the streaming buffer, see SCAFFOLD_TRIGGERS in brief.ts)
|
|
9119
9422
|
//
|
|
9120
|
-
//
|
|
9121
|
-
//
|
|
9122
|
-
//
|
|
9123
|
-
//
|
|
9124
|
-
//
|
|
9125
|
-
//
|
|
9423
|
+
// bento + magazine · extract → write (2 stages · runBentoStage
|
|
9424
|
+
// runs ONE chair-LLM call that produces the BentoScaffold;
|
|
9425
|
+
// composer and the scaffold sub-stages are skipped entirely.
|
|
9426
|
+
// Both modes share the same 2-stage rail · only the write
|
|
9427
|
+
// label and renderer differ)
|
|
9428
|
+
//
|
|
9429
|
+
// System UI · always English regardless of brief language. The
|
|
9430
|
+
// pipeline labels are the app's voice (chrome around the
|
|
9126
9431
|
// generation), not the report content itself.
|
|
9127
|
-
const
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9432
|
+
const STRUCTURED_WRITE_LABELS = {
|
|
9433
|
+
bento: "Composing the bento",
|
|
9434
|
+
magazine: "Composing the magazine",
|
|
9435
|
+
newspaper: "Composing the newspaper",
|
|
9436
|
+
};
|
|
9437
|
+
const STAGE_DEFS = this.isStructuredBriefMode(b.mode)
|
|
9438
|
+
? [
|
|
9439
|
+
{ key: "extract", label: "Reading what each director said", pipShort: "read" },
|
|
9440
|
+
{ key: "write", label: STRUCTURED_WRITE_LABELS[b.mode] || "Composing the report", pipShort: "compose" },
|
|
9441
|
+
]
|
|
9442
|
+
: [
|
|
9443
|
+
{ key: "extract", label: "Reading what each director said", pipShort: "read" },
|
|
9444
|
+
{ key: "compose", label: "Picking the report shape", pipShort: "pick" },
|
|
9445
|
+
{ key: "scaffold-anchor", label: "Setting the anchor", pipShort: "anchor" },
|
|
9446
|
+
{ key: "scaffold-findings", label: "Sketching findings", pipShort: "find" },
|
|
9447
|
+
{ key: "scaffold-cluster", label: "Mapping consensus + dissent", pipShort: "split" },
|
|
9448
|
+
{ key: "scaffold-actions", label: "Drafting actions + risks", pipShort: "act" },
|
|
9449
|
+
{ key: "write", label: "Writing the report", pipShort: "write" },
|
|
9450
|
+
];
|
|
9136
9451
|
|
|
9137
9452
|
const meta = this.BRIEF_STAGE_META;
|
|
9138
9453
|
const substages = (this.BRIEF_SUBSTAGES[lang] || this.BRIEF_SUBSTAGES.en);
|
|
@@ -9184,9 +9499,21 @@
|
|
|
9184
9499
|
}
|
|
9185
9500
|
|
|
9186
9501
|
// Rotating sub-line · advances every 3s within the active stage.
|
|
9502
|
+
// Bento mode's `write` stage does different work than the
|
|
9503
|
+
// research-note `write` stage — pick its dedicated rotator
|
|
9504
|
+
// (`bento-write`) when active. Falls through to the regular
|
|
9505
|
+
// key for every other stage / mode.
|
|
9187
9506
|
let substageText = "";
|
|
9188
|
-
|
|
9189
|
-
|
|
9507
|
+
// Structured-mode write stages get their own rotator copy
|
|
9508
|
+
// ("bento-write" / "magazine-write" / "newspaper-write") since
|
|
9509
|
+
// the work is different from research-note's chapter-by-chapter
|
|
9510
|
+
// write.
|
|
9511
|
+
let subKey = activeDef.key;
|
|
9512
|
+
if (activeDef.key === "write" && this.isStructuredBriefMode(b.mode)) {
|
|
9513
|
+
subKey = `${b.mode}-write`;
|
|
9514
|
+
}
|
|
9515
|
+
if (activeStatus === "active" && substages[subKey]?.length) {
|
|
9516
|
+
const list = substages[subKey];
|
|
9190
9517
|
substageText = list[Math.floor(activeElapsed / 3) % list.length];
|
|
9191
9518
|
}
|
|
9192
9519
|
|
|
@@ -9684,6 +10011,27 @@
|
|
|
9684
10011
|
});
|
|
9685
10012
|
return;
|
|
9686
10013
|
}
|
|
10014
|
+
// Brief-mode picker · clicking a label (or its radio) toggles the
|
|
10015
|
+
// .on class on that option AND off on the siblings. Used by both
|
|
10016
|
+
// the adjourn overlay and the supplement overlay. We scope to the
|
|
10017
|
+
// picker's `.adjourn-mode-options` container (set in
|
|
10018
|
+
// renderBriefModePicker) instead of a specific overlay id, so the
|
|
10019
|
+
// same handler works wherever the picker is embedded. The native
|
|
10020
|
+
// radio behaviour handles `:checked` state but we use a manual
|
|
10021
|
+
// class for visual styling (left accent stripe + tint) since we
|
|
10022
|
+
// can't `:has()` reliably on older browsers.
|
|
10023
|
+
const modeOpt = e.target.closest(".adjourn-mode-option");
|
|
10024
|
+
if (modeOpt) {
|
|
10025
|
+
const group = modeOpt.closest(".adjourn-mode-options");
|
|
10026
|
+
if (group) {
|
|
10027
|
+
group.querySelectorAll(".adjourn-mode-option").forEach((el) => el.classList.remove("on"));
|
|
10028
|
+
modeOpt.classList.add("on");
|
|
10029
|
+
const radio = modeOpt.querySelector('input[type="radio"]');
|
|
10030
|
+
if (radio) radio.checked = true;
|
|
10031
|
+
}
|
|
10032
|
+
// Don't preventDefault · let the label's native click behaviour
|
|
10033
|
+
// also tick the radio for keyboard / a11y users.
|
|
10034
|
+
}
|
|
9687
10035
|
// Adjourn overlay · "skip report" footer button — clicking commits
|
|
9688
10036
|
// immediately (mark picked + dispatch + close).
|
|
9689
10037
|
if (e.target.closest("[data-adjourn-skip]")) {
|
|
@@ -9707,6 +10055,18 @@
|
|
|
9707
10055
|
app.closeAdjournOverlay();
|
|
9708
10056
|
return;
|
|
9709
10057
|
}
|
|
10058
|
+
// Adjourn overlay · subject Show more / less toggle. Flips the
|
|
10059
|
+
// `is-clamped` class on the value span and swaps the button label.
|
|
10060
|
+
const subjToggle = e.target.closest("[data-adjourn-subject-toggle]");
|
|
10061
|
+
if (subjToggle) {
|
|
10062
|
+
e.preventDefault();
|
|
10063
|
+
const subjEl = document.querySelector("[data-adjourn-subject]");
|
|
10064
|
+
if (subjEl) {
|
|
10065
|
+
const expanded = subjEl.classList.toggle("is-clamped") === false;
|
|
10066
|
+
subjToggle.textContent = expanded ? "Show less" : "Show more";
|
|
10067
|
+
}
|
|
10068
|
+
return;
|
|
10069
|
+
}
|
|
9710
10070
|
// Brief card · "Add a perspective" → opens the supplement overlay.
|
|
9711
10071
|
if (e.target.closest("[data-brief-supplement]")) {
|
|
9712
10072
|
e.preventDefault();
|