privateboard 0.1.7 → 0.1.9
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 +1423 -46
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +195 -0
- package/public/agent-profile.css +525 -0
- package/public/agent-profile.js +278 -1
- package/public/app.js +634 -118
- package/public/home.html +389 -17
- package/public/index.html +325 -130
- package/public/magazine.html +1685 -0
- package/public/newspaper.html +1892 -0
- package/public/ppt.html +2623 -0
- package/public/report.html +366 -52
- package/public/room-settings.css +40 -4
- package/public/room-settings.js +44 -2
- package/public/user-settings.css +117 -68
- package/public/user-settings.js +77 -46
package/public/user-settings.css
CHANGED
|
@@ -69,33 +69,36 @@
|
|
|
69
69
|
}
|
|
70
70
|
.us-classification .right { color: var(--text-faint, #3A382F); letter-spacing: 0.12em; }
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.us-head .us-close {
|
|
89
|
-
width: 28px; height: 28px;
|
|
72
|
+
/* Close button · absolute-positioned in the modal's top-right
|
|
73
|
+
corner, pinned BELOW the classification strip so it doesn't
|
|
74
|
+
overlap "// LOCAL" at the right edge of that strip. The strip is
|
|
75
|
+
~22-24px tall (6px + 9px font + 6px + 0.5px border). The button
|
|
76
|
+
sits at 48px from the modal top — roughly aligned with the
|
|
77
|
+
first pane's `padding-top: 18px` baseline, so it visually
|
|
78
|
+
anchors to the section content rather than crowding the strip
|
|
79
|
+
below. The previous header strip (`.us-head`) that hosted this
|
|
80
|
+
button has been retired — first the "Preference" title, then
|
|
81
|
+
the surrounding bar with its dashed divider. */
|
|
82
|
+
.us-close {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 38px;
|
|
85
|
+
right: 14px;
|
|
86
|
+
z-index: 1;
|
|
87
|
+
width: 24px; height: 24px;
|
|
90
88
|
background: transparent;
|
|
91
89
|
border: 0.5px solid var(--line-bright, #2A2A26);
|
|
92
90
|
color: var(--text-dim, #5C5A52);
|
|
93
|
-
font-size:
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
line-height: 1;
|
|
94
93
|
cursor: pointer;
|
|
95
94
|
font-family: var(--mono);
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
padding: 0;
|
|
96
99
|
transition: all 0.12s;
|
|
97
100
|
}
|
|
98
|
-
.us-
|
|
101
|
+
.us-close:hover {
|
|
99
102
|
border-color: var(--lime, #6FB572);
|
|
100
103
|
color: var(--lime, #6FB572);
|
|
101
104
|
}
|
|
@@ -261,50 +264,76 @@
|
|
|
261
264
|
margin-top: 5px;
|
|
262
265
|
}
|
|
263
266
|
|
|
264
|
-
/* ─── Toggle row ·
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
/* ─── Toggle row · proper switch + supporting deck text ───
|
|
268
|
+
Switch shape (track + sliding thumb) instead of the prior pill
|
|
269
|
+
button: the affordance reads "on/off state" at a glance, not
|
|
270
|
+
"press me to do something." Track is a 32×18 rounded rect, thumb
|
|
271
|
+
is a 14×14 disc that slides 14px between rest positions. Lime
|
|
272
|
+
fill when on; panel-3 fill + hairline border when off. */
|
|
268
273
|
.us-toggle-row {
|
|
269
274
|
display: flex;
|
|
270
275
|
align-items: center;
|
|
271
276
|
gap: 12px;
|
|
272
277
|
flex-wrap: wrap;
|
|
273
278
|
}
|
|
274
|
-
.us-
|
|
279
|
+
.us-switch {
|
|
275
280
|
appearance: none;
|
|
276
281
|
background: transparent;
|
|
277
|
-
border:
|
|
278
|
-
|
|
279
|
-
font-family: var(--mono);
|
|
280
|
-
font-size: 10.5px;
|
|
281
|
-
letter-spacing: 0.18em;
|
|
282
|
-
text-transform: uppercase;
|
|
283
|
-
font-weight: 700;
|
|
284
|
-
padding: 6px 14px;
|
|
282
|
+
border: none;
|
|
283
|
+
padding: 0;
|
|
285
284
|
display: inline-flex;
|
|
286
285
|
align-items: center;
|
|
287
|
-
gap:
|
|
286
|
+
gap: 10px;
|
|
288
287
|
cursor: pointer;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.us-toggle-pill:hover {
|
|
292
|
-
color: var(--text, #C8C5BE);
|
|
293
|
-
border-color: var(--text-faint, #3A382F);
|
|
288
|
+
font-family: var(--mono);
|
|
289
|
+
color: inherit;
|
|
294
290
|
}
|
|
295
|
-
.us-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
291
|
+
.us-switch-track {
|
|
292
|
+
position: relative;
|
|
293
|
+
display: inline-block;
|
|
294
|
+
width: 32px;
|
|
295
|
+
height: 18px;
|
|
296
|
+
background: var(--panel-3, #21211E);
|
|
297
|
+
border: 0.5px solid var(--line-bright, #2A2A26);
|
|
298
|
+
border-radius: 9px;
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
transition: background 0.18s, border-color 0.18s;
|
|
299
301
|
}
|
|
300
|
-
.us-
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
.us-switch-thumb {
|
|
303
|
+
position: absolute;
|
|
304
|
+
top: 1px;
|
|
305
|
+
left: 1px;
|
|
306
|
+
width: 14px;
|
|
307
|
+
height: 14px;
|
|
303
308
|
border-radius: 50%;
|
|
304
309
|
background: var(--text-faint, #3A382F);
|
|
305
|
-
transition: background 0.
|
|
310
|
+
transition: transform 0.18s ease, background 0.18s;
|
|
311
|
+
}
|
|
312
|
+
.us-switch.on .us-switch-track {
|
|
313
|
+
background: var(--lime, #6FB572);
|
|
314
|
+
border-color: var(--lime, #6FB572);
|
|
315
|
+
}
|
|
316
|
+
.us-switch.on .us-switch-thumb {
|
|
317
|
+
transform: translateX(14px);
|
|
318
|
+
background: var(--bg, #0A0A0A);
|
|
319
|
+
}
|
|
320
|
+
.us-switch:hover .us-switch-track { border-color: var(--text-faint, #3A382F); }
|
|
321
|
+
.us-switch.on:hover .us-switch-track { border-color: var(--lime, #6FB572); }
|
|
322
|
+
.us-switch:focus-visible { outline: none; }
|
|
323
|
+
.us-switch:focus-visible .us-switch-track {
|
|
324
|
+
outline: 1.5px solid var(--lime, #6FB572);
|
|
325
|
+
outline-offset: 2px;
|
|
326
|
+
}
|
|
327
|
+
.us-switch-label {
|
|
328
|
+
font-size: 10.5px;
|
|
329
|
+
letter-spacing: 0.18em;
|
|
330
|
+
text-transform: uppercase;
|
|
331
|
+
font-weight: 700;
|
|
332
|
+
color: var(--text-soft, #8E8B83);
|
|
333
|
+
transition: color 0.12s;
|
|
306
334
|
}
|
|
307
|
-
.us-
|
|
335
|
+
.us-switch.on .us-switch-label { color: var(--lime, #6FB572); }
|
|
336
|
+
|
|
308
337
|
.us-toggle-deck {
|
|
309
338
|
font-size: 11px;
|
|
310
339
|
color: var(--text-faint, #3A382F);
|
|
@@ -777,7 +806,6 @@
|
|
|
777
806
|
|
|
778
807
|
@media (max-width: 600px) {
|
|
779
808
|
.user-settings-overlay { padding: 12px; }
|
|
780
|
-
.us-head { padding-left: 14px; padding-right: 14px; }
|
|
781
809
|
.us-pane { padding: 14px; }
|
|
782
810
|
.us-foot { padding: 10px 14px; }
|
|
783
811
|
}
|
|
@@ -895,9 +923,11 @@
|
|
|
895
923
|
display: flex;
|
|
896
924
|
align-items: flex-end;
|
|
897
925
|
gap: 4px;
|
|
898
|
-
height:
|
|
926
|
+
height: 180px;
|
|
899
927
|
/* The stack within each bar lives at the bottom (flex-end) so
|
|
900
|
-
bar height grows upward like a real chart axis.
|
|
928
|
+
bar height grows upward like a real chart axis. Bumped from
|
|
929
|
+
96px → 180px so the chart reads as a real exhibit instead of
|
|
930
|
+
a squashed sparkline. */
|
|
901
931
|
}
|
|
902
932
|
.us-chart-bar {
|
|
903
933
|
appearance: none;
|
|
@@ -915,27 +945,46 @@
|
|
|
915
945
|
position: relative;
|
|
916
946
|
}
|
|
917
947
|
.us-chart-bar:hover .us-chart-stack { filter: brightness(1.15); }
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
content: "";
|
|
948
|
+
/* Hover tooltip · two-line custom tip rendered via ::before from the
|
|
949
|
+
bar's data-tip-day / data-tip-num attributes. Floats above the
|
|
950
|
+
column so it doesn't overlap the stack or get clipped by the bar's
|
|
951
|
+
contents. Native browser title= is replaced by aria-label= on the
|
|
952
|
+
button to keep a11y without the 500ms native-tooltip delay.
|
|
953
|
+
Suppressed on `.empty` bars · zero-usage days show no tooltip
|
|
954
|
+
(the row is already empty; a tooltip saying "no usage" is noise). */
|
|
955
|
+
.us-chart-bar:not(.empty)::before {
|
|
956
|
+
content: attr(data-tip-day) "\A" attr(data-tip-num);
|
|
957
|
+
white-space: pre;
|
|
958
|
+
text-align: center;
|
|
927
959
|
position: absolute;
|
|
928
|
-
|
|
929
|
-
|
|
960
|
+
bottom: calc(100% + 6px);
|
|
961
|
+
left: 50%;
|
|
962
|
+
transform: translateX(-50%);
|
|
963
|
+
background: var(--bg, #0A0A0A);
|
|
964
|
+
border: 1px solid var(--line-bright, #2A2A26);
|
|
965
|
+
padding: 6px 10px;
|
|
966
|
+
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
967
|
+
font-size: 10px;
|
|
968
|
+
line-height: 1.5;
|
|
969
|
+
letter-spacing: 0.04em;
|
|
970
|
+
color: var(--text, #C8C5BE);
|
|
971
|
+
opacity: 0;
|
|
930
972
|
pointer-events: none;
|
|
973
|
+
transition: opacity 0.12s ease;
|
|
974
|
+
z-index: 10;
|
|
931
975
|
}
|
|
932
|
-
.us-chart-bar.
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
border: 1px solid var(--lime, #6FB572);
|
|
937
|
-
pointer-events: none;
|
|
976
|
+
.us-chart-bar:not(.empty):hover::before { opacity: 1; }
|
|
977
|
+
.us-chart-bar.empty .us-chart-stack {
|
|
978
|
+
height: 1px !important;
|
|
979
|
+
background: var(--line-bright, #2A2A26);
|
|
938
980
|
}
|
|
981
|
+
/* Today / active markers carry only on the tick label below
|
|
982
|
+
(color shift) — no perimeter border around the bar slot.
|
|
983
|
+
Earlier the today/active states drew an outline ring via
|
|
984
|
+
`::before` which read as an ugly box around an otherwise
|
|
985
|
+
clean stacked-bar; the tick-colour change carries enough
|
|
986
|
+
signal. See `.us-chart-bar.today .us-chart-tick` and
|
|
987
|
+
`.us-chart-bar.active .us-chart-tick` below. */
|
|
939
988
|
.us-chart-stack {
|
|
940
989
|
display: flex;
|
|
941
990
|
flex-direction: column-reverse; /* first segment at the bottom */
|
package/public/user-settings.js
CHANGED
|
@@ -226,18 +226,6 @@
|
|
|
226
226
|
</div>
|
|
227
227
|
</div>
|
|
228
228
|
|
|
229
|
-
<div class="us-row">
|
|
230
|
-
<div class="us-row-label">Typing sound</div>
|
|
231
|
-
<div class="us-row-field">
|
|
232
|
-
<div class="us-toggle-row">
|
|
233
|
-
<button type="button" class="us-toggle-pill" data-us-sfx-typing aria-pressed="false">
|
|
234
|
-
<span class="us-toggle-dot"></span>
|
|
235
|
-
<span class="us-toggle-label" data-us-sfx-typing-label>off</span>
|
|
236
|
-
</button>
|
|
237
|
-
<span class="us-toggle-deck">a soft keyboard click as directors stream their replies. Persists locally.</span>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
229
|
</div>
|
|
242
230
|
`;
|
|
243
231
|
}
|
|
@@ -268,6 +256,66 @@
|
|
|
268
256
|
`;
|
|
269
257
|
}
|
|
270
258
|
|
|
259
|
+
/* ── Other settings · misc per-user toggles that don't fit a
|
|
260
|
+
dedicated section. Currently just the typing-sound effect; a
|
|
261
|
+
natural home for future small ambient / UX preferences (sound
|
|
262
|
+
cues, animations, etc.) without growing the nav for each one. */
|
|
263
|
+
function otherSettingsSectionHTML() {
|
|
264
|
+
return `
|
|
265
|
+
<div class="us-pane-head">
|
|
266
|
+
<div class="us-pane-tag">▸ Other settings</div>
|
|
267
|
+
<div class="us-pane-deck">small UX preferences that don't fit elsewhere. All persisted locally.</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="us-pane-body">
|
|
271
|
+
<div class="us-row">
|
|
272
|
+
<div class="us-row-label">Typing sound</div>
|
|
273
|
+
<div class="us-row-field">
|
|
274
|
+
<div class="us-toggle-row">
|
|
275
|
+
<button type="button" class="us-switch" data-us-sfx-typing role="switch" aria-checked="false">
|
|
276
|
+
<span class="us-switch-track" aria-hidden="true">
|
|
277
|
+
<span class="us-switch-thumb"></span>
|
|
278
|
+
</span>
|
|
279
|
+
<span class="us-switch-label" data-us-sfx-typing-label>off</span>
|
|
280
|
+
</button>
|
|
281
|
+
<span class="us-toggle-deck">a soft keyboard click as directors stream their replies in chat. Brief generation stays silent regardless of this setting.</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function wireOtherSettingsSection() {
|
|
290
|
+
if (!paneEl) return;
|
|
291
|
+
// Typing-sound toggle · the persistence + audio context lives in
|
|
292
|
+
// window.boardroomTypingSfx (typing-sfx.js); this row only mirrors
|
|
293
|
+
// the current state and proxies clicks. Reading inside wire-up
|
|
294
|
+
// (not at HTML build time) means the pill always reflects the
|
|
295
|
+
// LATEST stored state when the section re-mounts.
|
|
296
|
+
const sfxBtn = paneEl.querySelector("[data-us-sfx-typing]");
|
|
297
|
+
const sfxLabel = paneEl.querySelector("[data-us-sfx-typing-label]");
|
|
298
|
+
if (sfxBtn && sfxLabel && window.boardroomTypingSfx) {
|
|
299
|
+
const paint = () => {
|
|
300
|
+
const on = window.boardroomTypingSfx.isEnabled();
|
|
301
|
+
sfxBtn.classList.toggle("on", on);
|
|
302
|
+
// role="switch" wants `aria-checked`, not `aria-pressed`.
|
|
303
|
+
sfxBtn.setAttribute("aria-checked", on ? "true" : "false");
|
|
304
|
+
sfxLabel.textContent = on ? "on" : "off";
|
|
305
|
+
};
|
|
306
|
+
paint();
|
|
307
|
+
sfxBtn.addEventListener("click", () => {
|
|
308
|
+
const next = !window.boardroomTypingSfx.isEnabled();
|
|
309
|
+
window.boardroomTypingSfx.setEnabled(next);
|
|
310
|
+
paint();
|
|
311
|
+
// Audible confirmation when turning ON · the click that just
|
|
312
|
+
// toggled also serves as the gesture the AudioContext needs,
|
|
313
|
+
// so this tick is actually heard.
|
|
314
|
+
if (next) window.boardroomTypingSfx.tick();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
271
319
|
/* ── Usage section ────────────────────────────────────────── */
|
|
272
320
|
// Provider → CSS-variable color slot. Lets each model bar/swatch
|
|
273
321
|
// pick up the right accent from the active theme without baking
|
|
@@ -404,11 +452,20 @@
|
|
|
404
452
|
if (isToday) cls.push("today");
|
|
405
453
|
if (isSelected) cls.push("active");
|
|
406
454
|
if (total === 0) cls.push("empty");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
455
|
+
// Custom hover tooltip · two lines (day + token count) rendered
|
|
456
|
+
// via CSS `::before` from `data-tip-day` / `data-tip-num`. We
|
|
457
|
+
// use `aria-label` (not `title`) so screen readers still get
|
|
458
|
+
// the info but the native browser tooltip with its ~500ms
|
|
459
|
+
// delay doesn't fight with the instant custom one.
|
|
460
|
+
const dayLabel = fmtDayLong(d.day);
|
|
461
|
+
const numLabel = total > 0 ? `${fmtTokens(total)} tokens` : "no usage";
|
|
462
|
+
const aria = `${dayLabel} · ${numLabel}`;
|
|
410
463
|
return `
|
|
411
|
-
<button type="button" class="${cls.join(' ')}"
|
|
464
|
+
<button type="button" class="${cls.join(' ')}"
|
|
465
|
+
data-usage-day="${escape(d.day)}"
|
|
466
|
+
data-tip-day="${escape(dayLabel)}"
|
|
467
|
+
data-tip-num="${escape(numLabel)}"
|
|
468
|
+
aria-label="${escape(aria)}">
|
|
412
469
|
<span class="us-chart-stack" style="height:${heightPct.toFixed(2)}%">${segHtml}</span>
|
|
413
470
|
<span class="us-chart-tick">${escape(fmtDayLabel(d.day))}</span>
|
|
414
471
|
</button>
|
|
@@ -931,10 +988,7 @@
|
|
|
931
988
|
<span class="right">// local</span>
|
|
932
989
|
</div>
|
|
933
990
|
|
|
934
|
-
<
|
|
935
|
-
<div class="us-title">Preference</div>
|
|
936
|
-
<button type="button" class="us-close" aria-label="Close">✕</button>
|
|
937
|
-
</header>
|
|
991
|
+
<button type="button" class="us-close" aria-label="Close">✕</button>
|
|
938
992
|
|
|
939
993
|
<div class="us-frame">
|
|
940
994
|
<nav class="us-nav" role="tablist">
|
|
@@ -943,6 +997,7 @@
|
|
|
943
997
|
<a href="#" class="us-nav-item" data-section="usage" role="tab" aria-selected="false">Usage</a>
|
|
944
998
|
<a href="#" class="us-nav-item" data-section="keys" role="tab" aria-selected="false">API Key</a>
|
|
945
999
|
<a href="#" class="us-nav-item" data-section="default" role="tab" aria-selected="false">Default Model</a>
|
|
1000
|
+
<a href="#" class="us-nav-item" data-section="other" role="tab" aria-selected="false">Other settings</a>
|
|
946
1001
|
<div class="us-nav-foot" data-us-version aria-label="App version">
|
|
947
1002
|
<span class="us-nav-foot-label">version</span>
|
|
948
1003
|
<span class="us-nav-foot-value" data-us-version-value>·</span>
|
|
@@ -975,12 +1030,14 @@
|
|
|
975
1030
|
else if (id === "usage") paneEl.innerHTML = usageSectionHTML();
|
|
976
1031
|
else if (id === "keys") paneEl.innerHTML = keysSectionHTML();
|
|
977
1032
|
else if (id === "default") paneEl.innerHTML = defaultModelSectionHTML();
|
|
1033
|
+
else if (id === "other") paneEl.innerHTML = otherSettingsSectionHTML();
|
|
978
1034
|
|
|
979
1035
|
// Section-specific wiring
|
|
980
1036
|
if (id === "user") wireUserSection();
|
|
981
1037
|
if (id === "keys") wireKeysSection();
|
|
982
1038
|
if (id === "usage") wireUsageSection();
|
|
983
1039
|
if (id === "default") wireDefaultModelSection();
|
|
1040
|
+
if (id === "other") wireOtherSettingsSection();
|
|
984
1041
|
|
|
985
1042
|
// Active rail item
|
|
986
1043
|
modal.querySelectorAll(".us-nav-item").forEach((el) => {
|
|
@@ -1035,32 +1092,6 @@
|
|
|
1035
1092
|
});
|
|
1036
1093
|
introCount.textContent = introInput.value.length;
|
|
1037
1094
|
|
|
1038
|
-
// Typing-sound toggle · the persistence + audio context lives in
|
|
1039
|
-
// window.boardroomTypingSfx (typing-sfx.js), so this row only
|
|
1040
|
-
// mirrors the current state and proxies clicks. Reading inside the
|
|
1041
|
-
// wire-up call (not at HTML build time) means the pill always
|
|
1042
|
-
// reflects the LATEST stored state when the User pane re-mounts.
|
|
1043
|
-
const sfxBtn = paneEl.querySelector("[data-us-sfx-typing]");
|
|
1044
|
-
const sfxLabel = paneEl.querySelector("[data-us-sfx-typing-label]");
|
|
1045
|
-
if (sfxBtn && sfxLabel && window.boardroomTypingSfx) {
|
|
1046
|
-
const paint = () => {
|
|
1047
|
-
const on = window.boardroomTypingSfx.isEnabled();
|
|
1048
|
-
sfxBtn.classList.toggle("on", on);
|
|
1049
|
-
sfxBtn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
1050
|
-
sfxLabel.textContent = on ? "on" : "off";
|
|
1051
|
-
};
|
|
1052
|
-
paint();
|
|
1053
|
-
sfxBtn.addEventListener("click", () => {
|
|
1054
|
-
const next = !window.boardroomTypingSfx.isEnabled();
|
|
1055
|
-
window.boardroomTypingSfx.setEnabled(next);
|
|
1056
|
-
paint();
|
|
1057
|
-
// Audible confirmation when turning ON · the click that just
|
|
1058
|
-
// toggled also serves as the gesture the AudioContext needs,
|
|
1059
|
-
// so this tick will actually be heard.
|
|
1060
|
-
if (next) window.boardroomTypingSfx.tick();
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
1095
|
// Regenerate avatar · same pattern as agent-profile's
|
|
1065
1096
|
// regenerateProfileAvatar: pull a fresh randomSeed, persist it to
|
|
1066
1097
|
// the user prefs, repaint. No counter, no name/intro composition —
|