@bongos/core 1.25.17 → 1.25.18
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/.bongos-core.json +131 -61
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +15 -1
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -0
- package/docs/api/openapi.json +243 -6
- package/docs/api-reference.md +12 -3
- package/docs/copy-inventory.md +118 -108
- package/docs/copy-registry.json +315 -172
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +62 -62
- package/modules/app-host/CLAUDE.md +15 -0
- package/modules/app-host/module.json +20 -0
- package/modules/app-host/public/settings-widget.js +143 -0
- package/modules/app-host/routes/app-host.js +88 -0
- package/modules/government/charter-advice.js +103 -0
- package/modules/government/presets.js +42 -0
- package/modules/government/recommendations.js +102 -7
- package/modules/government/routes/government.js +1 -0
- package/modules/lifecycle/first-read-catalog.js +62 -0
- package/modules/lifecycle/first-read-db.js +47 -9
- package/modules/lifecycle/first-read.js +88 -23
- package/modules/lifecycle/migrations/lifecycle_023_repo_map_dismissed.sql +19 -0
- package/modules/lifecycle/routes/first-read.js +27 -5
- package/modules/onboarding/genesis-work.js +101 -4
- package/modules/onboarding/routes/genesis-work.js +2 -2
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/sso-app-status.js +65 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/seams.js +32 -1
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/public-landing/public/assets/guide-core.js +475 -0
- package/modules/public-landing/public/assets/guide-hall.css +121 -0
- package/modules/public-landing/public/assets/guide-hall.js +164 -0
- package/modules/public-landing/public/assets/guide-scene.css +9 -0
- package/modules/public-landing/public/assets/guide-scene.js +58 -381
- package/modules/public-landing/public/index.html +46 -1
- package/modules/public-landing/public/projects.html +11 -2
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +18 -0
- package/scripts/bongos/first-read.js +45 -11
- package/scripts/bongos/genesis.js +21 -1
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/seam-wiring-guard.js +3 -2
- package/scripts/hall-preview/server.js +2 -0
- package/src/bongos/auth-admission.js +64 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/module-scope-map.js +4 -0
- package/src/bongos/routes/core-update.js +3 -16
- package/src/bongos/routes/modules.js +11 -0
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +6 -1
- package/src/module-loader/loader.js +2 -2
- package/tests/app_host.mjs +413 -0
- package/tests/first_read.mjs +184 -8
- package/tests/genesis_work.mjs +74 -3
- package/tests/government_charter_advice.mjs +150 -0
- package/tests/landing_guide_hall.mjs +364 -0
- package/tests/landing_guide_scene.mjs +8 -5
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_where.mjs +13 -3
|
@@ -27,309 +27,38 @@
|
|
|
27
27
|
- write innerHTML, or move anything from script: the waiting dots are CSS, so the world's
|
|
28
28
|
reduced-motion switch (world.css) stops them.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
Since task 1005276 the conversation itself (the state machine, the plan reader, the
|
|
31
|
+
server's client rules, the doors) is assets/guide-core.js, shared with the plain hall
|
|
32
|
+
presentation (assets/guide-hall.js); this file is the game's look on top of it, and
|
|
33
|
+
re-exports the core's pure half for tests/landing_guide_scene.mjs. "Use the simple
|
|
34
|
+
version" swaps to the hall panel with the same chat. */
|
|
32
35
|
(function (factory) {
|
|
33
36
|
'use strict';
|
|
34
|
-
var
|
|
37
|
+
var core = typeof module === 'object' && module.exports ? require('./guide-core.js') : window.GuideCore;
|
|
38
|
+
var api = factory(core);
|
|
35
39
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
36
40
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
37
41
|
window.GuideScene = api;
|
|
38
42
|
api.boot(window, document);
|
|
39
43
|
}
|
|
40
|
-
})(function () {
|
|
44
|
+
})(function (core) {
|
|
41
45
|
'use strict';
|
|
42
46
|
|
|
43
|
-
var
|
|
44
|
-
var MODULE_KEY = 'front-door-guide';
|
|
45
|
-
// the server refuses a first message sooner than 1500ms after the start (a bot); wait a
|
|
46
|
-
// little longer so a clock edge never turns a person into one
|
|
47
|
-
var MIN_FIRST_MS = 1600;
|
|
48
|
-
// the chat's { id, token } in local storage: the server's contract keeps it there, not in
|
|
49
|
-
// session storage, so a closed browser comes back to the same chat
|
|
50
|
-
var STORE_KEY = 'cb-front-door-guide';
|
|
47
|
+
var COPY = core.COPY;
|
|
51
48
|
var HELM_FONTS = 'https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600&family=Barlow:wght@400;500&display=swap';
|
|
52
|
-
var SIGN_UP = 'Sign up and get started';
|
|
53
|
-
|
|
54
|
-
// Every sentence the scene says on its own. The guide's replies are the server's.
|
|
55
|
-
var COPY = {
|
|
56
|
-
fallback: 'The guide is resting right now. You can still start your project.',
|
|
57
|
-
lost: 'This chat has ended. You can still start your project.',
|
|
58
|
-
failed: 'That did not send. Try again.',
|
|
59
|
-
planFailed: 'Your plan did not come through. Try again.',
|
|
60
|
-
tooEarly: 'Tell the guide a little about your project first.',
|
|
61
|
-
empty: 'Type a message first.',
|
|
62
|
-
tooLong: function (n) { return 'That is too long. Keep it under ' + n + ' characters.'; },
|
|
63
|
-
tooFast: function (s) { return 'Too quick. Try again in ' + s + (s === 1 ? ' second.' : ' seconds.'); },
|
|
64
|
-
left: function (left, total) {
|
|
65
|
-
if (left <= 0) return 'No messages left';
|
|
66
|
-
return left + ' of ' + total + (total === 1 ? ' message left' : ' messages left');
|
|
67
|
-
},
|
|
68
|
-
waitTalk: 'Your guide is thinking',
|
|
69
|
-
waitPlan: 'Your guide is writing your plan',
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/* ── is the guide on for this page? The module set the server injected. ── */
|
|
73
|
-
function guideOn(mods) {
|
|
74
|
-
return !!(mods && mods.enabled && mods.enabled[MODULE_KEY] === true);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* ── the plan, read leniently. The server's Plan (modules/front-door-guide/plan.js) is
|
|
78
|
-
{ summary, project_type, how_it_works[], modules[{label, summary, why}], steps[{title,
|
|
79
|
-
detail}], needs[{item, detail}], open_questions[], cost_and_time }: each list becomes a
|
|
80
|
-
section under its own heading, in that order. Anything else it carries is read as text
|
|
81
|
-
where it can be and dropped where it cannot; no plan at all is null. ── */
|
|
82
|
-
var SECTIONS = [
|
|
83
|
-
['how_it_works', 'How it works'],
|
|
84
|
-
['steps', 'Steps'],
|
|
85
|
-
['modules', 'Modules to start with'],
|
|
86
|
-
['needs', 'You will need'],
|
|
87
|
-
['open_questions', 'Still to decide'],
|
|
88
|
-
];
|
|
89
|
-
function asText(v) {
|
|
90
|
-
if (typeof v === 'string') return v.trim();
|
|
91
|
-
if (typeof v === 'number') return String(v);
|
|
92
|
-
if (v && typeof v === 'object') {
|
|
93
|
-
var head = [v.title, v.label, v.item, v.name, v.heading].filter(function (s) { return typeof s === 'string' && s.trim(); })[0];
|
|
94
|
-
var body = [v.why, v.detail, v.text, v.summary, v.description].filter(function (s) { return typeof s === 'string' && s.trim(); })[0];
|
|
95
|
-
return [head, body].filter(Boolean).map(function (s) { return s.trim(); }).join(': ');
|
|
96
|
-
}
|
|
97
|
-
return '';
|
|
98
|
-
}
|
|
99
|
-
function asItems(arr) {
|
|
100
|
-
return (Array.isArray(arr) ? arr : []).map(asText).filter(Boolean).slice(0, 12);
|
|
101
|
-
}
|
|
102
|
-
function readPlan(raw) {
|
|
103
|
-
if (raw == null) return null;
|
|
104
|
-
if (typeof raw === 'string') return raw.trim() ? { summary: raw.trim(), sections: [], note: '' } : null;
|
|
105
|
-
if (typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
106
|
-
var plan = { summary: asText(raw.summary || ''), sections: [], note: typeof raw.cost_and_time === 'string' ? raw.cost_and_time.trim() : '' };
|
|
107
|
-
var seen = {};
|
|
108
|
-
SECTIONS.forEach(function (s) {
|
|
109
|
-
seen[s[0]] = true;
|
|
110
|
-
var its = asItems(raw[s[0]]);
|
|
111
|
-
if (its.length) plan.sections.push({ heading: s[1], items: its });
|
|
112
|
-
});
|
|
113
|
-
Object.keys(raw).forEach(function (k) {
|
|
114
|
-
if (seen[k] || !Array.isArray(raw[k])) return;
|
|
115
|
-
var its = asItems(raw[k]);
|
|
116
|
-
if (its.length) plan.sections.push({ heading: '', items: its });
|
|
117
|
-
});
|
|
118
|
-
return plan.summary || plan.sections.length ? plan : null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/* ── the state machine. One state object; every event returns a new one. ──
|
|
122
|
-
phase: closed · starting · chat · sending · planning · ended · fallback
|
|
123
|
-
A chat that has ended with no plan yet (the plan call failed, or a resumed chat never
|
|
124
|
-
asked) is 'ended' with plan null: the end strip then offers "Make my plan" again. */
|
|
125
|
-
function initial() {
|
|
126
|
-
return { phase: 'closed', messages: [], left: 0, total: 0, maxChars: 1000, plan: null, error: '', fallback: '', conv: null, startedAt: 0, restore: '', saved: '', next: SIGN_UP, sent: 0 };
|
|
127
|
-
}
|
|
128
|
-
function copyState(s) {
|
|
129
|
-
var n = {};
|
|
130
|
-
Object.keys(s).forEach(function (k) { n[k] = s[k]; });
|
|
131
|
-
n.messages = s.messages.slice();
|
|
132
|
-
return n;
|
|
133
|
-
}
|
|
134
|
-
function conversationCounts(n, c) {
|
|
135
|
-
if (!c || typeof c !== 'object') return;
|
|
136
|
-
if (typeof c.messages_left === 'number') n.left = Math.max(0, c.messages_left);
|
|
137
|
-
if (typeof c.messages_used === 'number') n.sent = c.messages_used;
|
|
138
|
-
if (typeof c.messages_used === 'number' && typeof c.messages_left === 'number') n.total = Math.max(n.total, c.messages_used + c.messages_left);
|
|
139
|
-
if (typeof c.max_message_chars === 'number') n.maxChars = c.max_message_chars;
|
|
140
|
-
}
|
|
141
|
-
function takeExtras(n, b) {
|
|
142
|
-
if (b.saved && typeof b.saved.message === 'string' && b.saved.message) n.saved = b.saved.message;
|
|
143
|
-
if (b.next && typeof b.next.label === 'string' && b.next.label) n.next = b.next.label;
|
|
144
|
-
var p = readPlan(b.plan);
|
|
145
|
-
if (p) n.plan = p;
|
|
146
|
-
}
|
|
147
|
-
function toFallback(n, message) {
|
|
148
|
-
n.phase = 'fallback';
|
|
149
|
-
n.fallback = message || COPY.fallback;
|
|
150
|
-
n.error = '';
|
|
151
|
-
n.restore = '';
|
|
152
|
-
return n;
|
|
153
|
-
}
|
|
154
|
-
function dropLastVisitor(n) {
|
|
155
|
-
for (var i = n.messages.length - 1; i >= 0; i--) {
|
|
156
|
-
if (n.messages[i].role === 'visitor') { n.restore = n.messages[i].text; n.messages.splice(i, 1); n.sent = Math.max(0, n.sent - 1); return; }
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function errCode(body) {
|
|
160
|
-
return body && (body.error && typeof body.error === 'object' ? body.error.code : body.error || body.code);
|
|
161
|
-
}
|
|
162
|
-
function reduce(state, ev) {
|
|
163
|
-
var n = copyState(state || initial());
|
|
164
|
-
n.restore = '';
|
|
165
|
-
var b = (ev && ev.body) || {};
|
|
166
|
-
var ok = ev && ev.status >= 200 && ev.status < 300;
|
|
167
|
-
switch (ev && ev.type) {
|
|
168
|
-
case 'start':
|
|
169
|
-
n.phase = 'starting';
|
|
170
|
-
n.error = '';
|
|
171
|
-
return n;
|
|
172
|
-
case 'started':
|
|
173
|
-
if (ok && b.mode === 'chat' && b.conversation && b.conversation.id && b.conversation.token) {
|
|
174
|
-
n.phase = 'chat';
|
|
175
|
-
n.conv = { id: String(b.conversation.id), token: String(b.conversation.token) };
|
|
176
|
-
n.startedAt = ev.now || 0;
|
|
177
|
-
n.total = 0;
|
|
178
|
-
conversationCounts(n, b.conversation);
|
|
179
|
-
n.messages = typeof b.greeting === 'string' && b.greeting ? [{ role: 'guide', text: b.greeting }] : [];
|
|
180
|
-
takeExtras(n, b);
|
|
181
|
-
return n;
|
|
182
|
-
}
|
|
183
|
-
return toFallback(n, b.mode === 'form' ? b.message : '');
|
|
184
|
-
case 'resumed': {
|
|
185
|
-
// GET /conversations/:id on load: the transcript (no greeting), the plan if written
|
|
186
|
-
if (!ok || !b.conversation || !Array.isArray(b.transcript)) return initial();
|
|
187
|
-
n.conv = ev.conv;
|
|
188
|
-
n.total = 0;
|
|
189
|
-
conversationCounts(n, b.conversation);
|
|
190
|
-
n.messages = b.transcript
|
|
191
|
-
.filter(function (m) { return m && typeof m.text === 'string' && (m.role === 'visitor' || m.role === 'guide'); })
|
|
192
|
-
.map(function (m) { return { role: m.role, text: m.text }; });
|
|
193
|
-
n.sent = Math.max(n.sent, n.messages.filter(function (m) { return m.role === 'visitor'; }).length);
|
|
194
|
-
takeExtras(n, b);
|
|
195
|
-
n.phase = b.conversation.status === 'ended' || n.plan ? 'ended' : 'chat';
|
|
196
|
-
// a resumed chat with nothing said yet is new enough for the bot check to apply
|
|
197
|
-
n.startedAt = n.sent ? 0 : (ev.now || 0);
|
|
198
|
-
return n;
|
|
199
|
-
}
|
|
200
|
-
case 'send':
|
|
201
|
-
n.phase = 'sending';
|
|
202
|
-
n.error = '';
|
|
203
|
-
n.sent += 1;
|
|
204
|
-
n.messages.push({ role: 'visitor', text: ev.text });
|
|
205
|
-
return n;
|
|
206
|
-
case 'answer': {
|
|
207
|
-
if (ok) {
|
|
208
|
-
takeExtras(n, b);
|
|
209
|
-
if (b.mode === 'chat') {
|
|
210
|
-
if (b.reply && typeof b.reply.text === 'string') n.messages.push({ role: 'guide', text: b.reply.text });
|
|
211
|
-
conversationCounts(n, b.conversation);
|
|
212
|
-
n.phase = n.left > 0 ? 'chat' : 'planning';
|
|
213
|
-
return n;
|
|
214
|
-
}
|
|
215
|
-
if (b.mode === 'ended') {
|
|
216
|
-
if (b.reply && typeof b.reply.text === 'string') n.messages.push({ role: 'guide', text: b.reply.text });
|
|
217
|
-
conversationCounts(n, b.conversation);
|
|
218
|
-
n.left = 0;
|
|
219
|
-
if (typeof b.message === 'string' && b.message) n.messages.push({ role: 'note', text: b.message });
|
|
220
|
-
// next_step 'plan': ask for it now; 'sign_up': it already exists
|
|
221
|
-
n.phase = b.next_step === 'sign_up' && n.plan ? 'ended' : 'planning';
|
|
222
|
-
return n;
|
|
223
|
-
}
|
|
224
|
-
return toFallback(n, b.mode === 'form' ? b.message : '');
|
|
225
|
-
}
|
|
226
|
-
var code = errCode(b);
|
|
227
|
-
if (ev.status === 404) return toFallback(n, COPY.lost);
|
|
228
|
-
dropLastVisitor(n);
|
|
229
|
-
n.phase = 'chat';
|
|
230
|
-
if (ev.status === 400 && code === 'too_long') {
|
|
231
|
-
n.maxChars = Number(b.max_message_chars) || n.maxChars;
|
|
232
|
-
n.error = COPY.tooLong(n.maxChars);
|
|
233
|
-
} else if (ev.status === 400 && code === 'empty') {
|
|
234
|
-
n.error = COPY.empty;
|
|
235
|
-
} else if (ev.status === 429) {
|
|
236
|
-
n.error = COPY.tooFast(Math.max(1, Math.ceil(Number(b.retry_after_seconds) || 5)));
|
|
237
|
-
} else {
|
|
238
|
-
n.error = COPY.failed;
|
|
239
|
-
}
|
|
240
|
-
return n;
|
|
241
|
-
}
|
|
242
|
-
case 'failed':
|
|
243
|
-
dropLastVisitor(n);
|
|
244
|
-
n.phase = n.conv ? 'chat' : 'fallback';
|
|
245
|
-
if (n.phase === 'fallback') n.fallback = COPY.fallback;
|
|
246
|
-
else n.error = COPY.failed;
|
|
247
|
-
return n;
|
|
248
|
-
case 'plan':
|
|
249
|
-
n.phase = 'planning';
|
|
250
|
-
n.error = '';
|
|
251
|
-
return n;
|
|
252
|
-
case 'planned': {
|
|
253
|
-
if (ok && b.mode === 'plan') {
|
|
254
|
-
takeExtras(n, b);
|
|
255
|
-
conversationCounts(n, b.conversation);
|
|
256
|
-
n.left = 0;
|
|
257
|
-
n.phase = 'ended';
|
|
258
|
-
return n;
|
|
259
|
-
}
|
|
260
|
-
if (ok && b.mode === 'form') return toFallback(n, b.message);
|
|
261
|
-
if (ev.status === 404) return toFallback(n, COPY.lost);
|
|
262
|
-
if (ev.status === 409 && errCode(b) === 'too_early') { n.phase = 'chat'; n.error = COPY.tooEarly; return n; }
|
|
263
|
-
// anything else: the chat is over but the plan can be asked for again
|
|
264
|
-
n.phase = 'ended';
|
|
265
|
-
n.error = ev.status === 429 ? COPY.tooFast(Math.max(1, Math.ceil(Number(b.retry_after_seconds) || 5))) : COPY.planFailed;
|
|
266
|
-
return n;
|
|
267
|
-
}
|
|
268
|
-
default:
|
|
269
|
-
return n;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// how long the first send must still wait (0 once the bot check is behind us)
|
|
274
|
-
function firstSendWait(state, now) {
|
|
275
|
-
if (state.sent || !state.startedAt) return 0;
|
|
276
|
-
return Math.max(0, state.startedAt + MIN_FIRST_MS - now);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function readStore(win) {
|
|
280
|
-
try {
|
|
281
|
-
var raw = JSON.parse(win.localStorage.getItem(STORE_KEY) || 'null');
|
|
282
|
-
return raw && typeof raw.id === 'string' && typeof raw.token === 'string' ? raw : null;
|
|
283
|
-
} catch (e) { return null; }
|
|
284
|
-
}
|
|
285
|
-
function writeStore(win, conv) {
|
|
286
|
-
try {
|
|
287
|
-
if (conv) win.localStorage.setItem(STORE_KEY, JSON.stringify({ id: conv.id, token: conv.token }));
|
|
288
|
-
else win.localStorage.removeItem(STORE_KEY);
|
|
289
|
-
} catch (e) { /* storage off: the chat still works, it just does not come back */ }
|
|
290
|
-
}
|
|
291
49
|
|
|
292
50
|
/* ──────────────────────────── the DOM half ──────────────────────────── */
|
|
293
51
|
function boot(win, doc) {
|
|
294
52
|
var scene = doc.getElementById('guide');
|
|
295
|
-
if (!scene || !guideOn(win.__MODULES__)) return;
|
|
53
|
+
if (!scene || !core.guideOn(win.__MODULES__)) return;
|
|
54
|
+
var ctl = core.controller(win, doc);
|
|
55
|
+
if (!ctl) return;
|
|
296
56
|
var $ = function (name) { return scene.querySelector('[data-gd="' + name + '"]'); };
|
|
297
57
|
var log = $('log'), err = $('err'), form = $('form'), text = $('text'), hp = $('website');
|
|
298
58
|
var send = $('send'), end = $('end'), go = $('go'), left = $('left'), segs = $('segs');
|
|
299
59
|
var planBody = $('plan-body'), planToggle = $('plan-toggle'), planClose = $('plan-close');
|
|
300
60
|
var makePlan = $('make-plan'), againPlan = $('plan-again'), saved = $('saved');
|
|
301
|
-
var
|
|
302
|
-
var chatOK = false; // the status read said 'chat'
|
|
303
|
-
var opener = null;
|
|
304
|
-
var limits = null;
|
|
305
|
-
|
|
306
|
-
function json(method, url, body, headers) {
|
|
307
|
-
var h = { accept: 'application/json' };
|
|
308
|
-
if (body) h['content-type'] = 'application/json';
|
|
309
|
-
Object.keys(headers || {}).forEach(function (k) { h[k] = headers[k]; });
|
|
310
|
-
return win.fetch(url, { method: method, credentials: 'same-origin', headers: h, body: body ? JSON.stringify(body) : undefined })
|
|
311
|
-
.then(function (res) {
|
|
312
|
-
return res.json().then(function (b) { return { status: res.status, body: b || {} }; }, function () { return { status: res.status, body: {} }; });
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
function convUrl(suffix) { return API + '/conversations/' + encodeURIComponent(state.conv.id) + (suffix || ''); }
|
|
316
|
-
|
|
317
|
-
// ask once, on load, whether the guide can talk: a click must never wait on it
|
|
318
|
-
json('GET', API + '/status').then(function (r) {
|
|
319
|
-
chatOK = r.status === 200 && r.body.mode === 'chat';
|
|
320
|
-
limits = r.body.limits;
|
|
321
|
-
}, function () { chatOK = false; });
|
|
322
|
-
// and pick up a chat this browser already holds
|
|
323
|
-
var held = readStore(win);
|
|
324
|
-
if (held) {
|
|
325
|
-
json('GET', API + '/conversations/' + encodeURIComponent(held.id), null, { 'x-guide-token': held.token }).then(function (r) {
|
|
326
|
-
if (state.phase !== 'closed') return;
|
|
327
|
-
if (r.status === 404 || r.status === 400) { writeStore(win, null); return; }
|
|
328
|
-
state = reduce(state, { type: 'resumed', status: r.status, body: r.body, conv: held, now: Date.now() });
|
|
329
|
-
// a held chat can always be shown, even once the day's budget says 'form'
|
|
330
|
-
if (state.conv) chatOK = true;
|
|
331
|
-
}, function () { /* offline: a click starts fresh or falls back */ });
|
|
332
|
-
}
|
|
61
|
+
var simple = $('switch');
|
|
333
62
|
|
|
334
63
|
function el(tag, cls, txt) {
|
|
335
64
|
var e = doc.createElement(tag);
|
|
@@ -348,7 +77,7 @@
|
|
|
348
77
|
wait.appendChild(p);
|
|
349
78
|
return wait;
|
|
350
79
|
}
|
|
351
|
-
function paintLog() {
|
|
80
|
+
function paintLog(state) {
|
|
352
81
|
clear(log);
|
|
353
82
|
state.messages.forEach(function (m) {
|
|
354
83
|
var li = el('li', 'gd__msg gd__msg--' + m.role);
|
|
@@ -367,7 +96,8 @@
|
|
|
367
96
|
log.scrollTop = log.scrollHeight;
|
|
368
97
|
}
|
|
369
98
|
|
|
370
|
-
function paintCount() {
|
|
99
|
+
function paintCount(state) {
|
|
100
|
+
var limits = ctl.limits();
|
|
371
101
|
var total = state.total || (limits && limits.max_messages) || 0;
|
|
372
102
|
var shown = state.phase !== 'fallback' && state.phase !== 'starting' && total > 0;
|
|
373
103
|
left.hidden = !shown;
|
|
@@ -378,15 +108,11 @@
|
|
|
378
108
|
for (var i = 0; i < total; i++) segs.appendChild(el('span', 'gd__seg' + (i < total - state.left ? ' gd__seg--used' : '')));
|
|
379
109
|
}
|
|
380
110
|
|
|
381
|
-
function paintPlan() {
|
|
111
|
+
function paintPlan(state) {
|
|
382
112
|
clear(planBody);
|
|
383
113
|
var plan = state.plan;
|
|
384
114
|
if (!plan) {
|
|
385
|
-
|
|
386
|
-
: state.phase === 'ended' ? 'No plan yet.'
|
|
387
|
-
: state.phase === 'fallback' ? 'No plan to show.'
|
|
388
|
-
: 'When the chat ends, your guide writes your plan here.';
|
|
389
|
-
planBody.appendChild(el('p', 'gd__planwait', wait));
|
|
115
|
+
planBody.appendChild(el('p', 'gd__planwait', COPY.planWait(state.phase)));
|
|
390
116
|
} else {
|
|
391
117
|
if (plan.summary) planBody.appendChild(el('p', 'gd__plansum', plan.summary));
|
|
392
118
|
plan.sections.forEach(function (s) {
|
|
@@ -401,11 +127,11 @@
|
|
|
401
127
|
saved.hidden = !state.saved || state.phase === 'fallback';
|
|
402
128
|
}
|
|
403
129
|
|
|
404
|
-
function paint() {
|
|
130
|
+
function paint(state) {
|
|
405
131
|
scene.setAttribute('data-phase', state.phase);
|
|
406
|
-
paintLog();
|
|
407
|
-
paintCount();
|
|
408
|
-
paintPlan();
|
|
132
|
+
paintLog(state);
|
|
133
|
+
paintCount(state);
|
|
134
|
+
paintPlan(state);
|
|
409
135
|
var talking = state.phase === 'chat' || state.phase === 'sending' || state.phase === 'starting';
|
|
410
136
|
form.hidden = !talking;
|
|
411
137
|
end.hidden = talking;
|
|
@@ -422,14 +148,6 @@
|
|
|
422
148
|
scene.classList.toggle('gd--done', state.phase === 'ended' && !!state.plan);
|
|
423
149
|
}
|
|
424
150
|
|
|
425
|
-
function dispatch(ev) {
|
|
426
|
-
state = reduce(state, ev);
|
|
427
|
-
if (ev.type === 'started' && state.conv) writeStore(win, state.conv);
|
|
428
|
-
if (state.phase === 'fallback' && (ev.status === 404)) writeStore(win, null);
|
|
429
|
-
paint();
|
|
430
|
-
if (state.phase === 'planning' && ev.type !== 'plan') askPlan();
|
|
431
|
-
}
|
|
432
|
-
|
|
433
151
|
/* the Helm faces: loaded the first time the scene opens, never on a plain visit */
|
|
434
152
|
function loadFaces() {
|
|
435
153
|
if (doc.querySelector('link[data-gd-faces]')) return;
|
|
@@ -446,121 +164,80 @@
|
|
|
446
164
|
function (n) { return n.offsetParent !== null; });
|
|
447
165
|
}
|
|
448
166
|
|
|
449
|
-
function
|
|
450
|
-
opener = door;
|
|
167
|
+
function show() {
|
|
451
168
|
loadFaces();
|
|
169
|
+
// the simple version is offered only when the page carries it
|
|
170
|
+
if (simple) simple.hidden = !ctl.has('hall');
|
|
452
171
|
scene.hidden = false;
|
|
453
172
|
doc.documentElement.classList.add('gd-open');
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}, function () {
|
|
460
|
-
dispatch({ type: 'started', status: 0, body: {} });
|
|
461
|
-
focusFirst();
|
|
462
|
-
});
|
|
463
|
-
} else {
|
|
464
|
-
paint();
|
|
465
|
-
}
|
|
466
|
-
focusFirst();
|
|
173
|
+
}
|
|
174
|
+
function hide() {
|
|
175
|
+
scene.hidden = true;
|
|
176
|
+
setPlan(false);
|
|
177
|
+
doc.documentElement.classList.remove('gd-open');
|
|
467
178
|
}
|
|
468
179
|
function focusFirst() {
|
|
469
180
|
if (scene.hidden) return;
|
|
470
181
|
var target = !text.disabled && !form.hidden ? text : (!end.hidden && !go.hidden ? go : $('close'));
|
|
471
182
|
try { target.focus(); } catch (e) { /* not focusable yet */ }
|
|
472
183
|
}
|
|
473
|
-
function close() {
|
|
474
|
-
scene.hidden = true;
|
|
475
|
-
setPlan(false);
|
|
476
|
-
doc.documentElement.classList.remove('gd-open');
|
|
477
|
-
if (opener) { try { opener.focus(); } catch (e) { /* gone */ } }
|
|
478
|
-
}
|
|
479
184
|
function setPlan(on) {
|
|
480
185
|
scene.classList.toggle('gd--plan', !!on);
|
|
481
186
|
planToggle.setAttribute('aria-expanded', on ? 'true' : 'false');
|
|
482
187
|
if (on) { try { planClose.focus(); } catch (e) { /* hidden */ } }
|
|
483
188
|
}
|
|
484
189
|
|
|
190
|
+
ctl.addView('game', {
|
|
191
|
+
show: show,
|
|
192
|
+
hide: hide,
|
|
193
|
+
paint: paint,
|
|
194
|
+
focusFirst: focusFirst,
|
|
195
|
+
website: function () { return hp.value; },
|
|
196
|
+
// one column: the plan is the reward, so it opens by itself
|
|
197
|
+
onPlanned: function (state) { if (state.plan && planToggle.offsetParent !== null) setPlan(true); },
|
|
198
|
+
});
|
|
199
|
+
|
|
485
200
|
function submit() {
|
|
486
|
-
if (state.phase !== 'chat') return;
|
|
201
|
+
if (ctl.state().phase !== 'chat') return;
|
|
487
202
|
var words = text.value.trim();
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
var conv = state.conv;
|
|
491
|
-
text.value = '';
|
|
492
|
-
dispatch({ type: 'send', text: words });
|
|
493
|
-
win.setTimeout(function () {
|
|
494
|
-
json('POST', API + '/conversations/' + encodeURIComponent(conv.id) + '/messages', { token: conv.token, text: words, website: hp.value })
|
|
495
|
-
.then(function (r) { dispatch({ type: 'answer', status: r.status, body: r.body }); },
|
|
496
|
-
function () { dispatch({ type: 'failed' }); })
|
|
497
|
-
.then(focusFirst);
|
|
498
|
-
}, wait);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// the plan: asked for once the chat ends (or the visitor presses "Make my plan");
|
|
502
|
-
// the server writes it once and re-reads it for free after that
|
|
503
|
-
var asking = false;
|
|
504
|
-
function askPlan() {
|
|
505
|
-
if (asking || !state.conv) return;
|
|
506
|
-
asking = true;
|
|
507
|
-
if (state.phase !== 'planning') dispatch({ type: 'plan' });
|
|
508
|
-
json('POST', convUrl('/plan'), { token: state.conv.token, website: hp.value })
|
|
509
|
-
.then(function (r) { asking = false; dispatch({ type: 'planned', status: r.status, body: r.body }); },
|
|
510
|
-
function () { asking = false; dispatch({ type: 'planned', status: 0, body: {} }); })
|
|
511
|
-
.then(function () {
|
|
512
|
-
focusFirst();
|
|
513
|
-
// one column: the plan is the reward, so it opens by itself
|
|
514
|
-
if (state.plan && planToggle.offsetParent !== null) setPlan(true);
|
|
515
|
-
});
|
|
203
|
+
if (words) text.value = '';
|
|
204
|
+
ctl.send(words);
|
|
516
205
|
}
|
|
517
206
|
|
|
518
|
-
/* the doors: a click is the guide only when everything says it can talk */
|
|
519
|
-
doc.addEventListener('click', function (e) {
|
|
520
|
-
var door = e.target && e.target.closest && e.target.closest('[data-guide-door]');
|
|
521
|
-
if (!door || !chatOK || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
522
|
-
e.preventDefault();
|
|
523
|
-
open(door);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
207
|
form.addEventListener('submit', function (e) { e.preventDefault(); submit(); });
|
|
527
208
|
text.addEventListener('keydown', function (e) {
|
|
528
209
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); submit(); }
|
|
529
210
|
});
|
|
530
|
-
makePlan.addEventListener('click', askPlan);
|
|
531
|
-
againPlan.addEventListener('click', askPlan);
|
|
532
|
-
$('close').addEventListener('click', close);
|
|
211
|
+
makePlan.addEventListener('click', ctl.askPlan);
|
|
212
|
+
againPlan.addEventListener('click', ctl.askPlan);
|
|
213
|
+
$('close').addEventListener('click', ctl.close);
|
|
214
|
+
if (simple) simple.addEventListener('click', function () { ctl.switchTo('hall'); });
|
|
533
215
|
planToggle.addEventListener('click', function () { setPlan(planToggle.getAttribute('aria-expanded') !== 'true'); });
|
|
534
216
|
planClose.addEventListener('click', function () { setPlan(false); try { planToggle.focus(); } catch (e) { /* hidden */ } });
|
|
535
|
-
scene.addEventListener('click', function (e) { if (e.target === scene) close(); });
|
|
217
|
+
scene.addEventListener('click', function (e) { if (e.target === scene) ctl.close(); });
|
|
536
218
|
scene.addEventListener('keydown', function (e) {
|
|
537
219
|
if (e.key === 'Escape') {
|
|
538
220
|
e.preventDefault();
|
|
539
221
|
if (scene.classList.contains('gd--plan')) { setPlan(false); try { planToggle.focus(); } catch (x) { /* hidden */ } }
|
|
540
|
-
else close();
|
|
222
|
+
else ctl.close();
|
|
541
223
|
return;
|
|
542
224
|
}
|
|
543
|
-
if (e.key
|
|
544
|
-
var f = focusables();
|
|
545
|
-
if (!f.length) return;
|
|
546
|
-
var first = f[0], last = f[f.length - 1];
|
|
547
|
-
if (e.shiftKey && doc.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
548
|
-
else if (!e.shiftKey && doc.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
225
|
+
if (e.key === 'Tab') core.trapTab(doc, focusables(), e);
|
|
549
226
|
});
|
|
550
227
|
var face = scene.querySelector('.gd__face img');
|
|
551
228
|
if (face) face.addEventListener('error', function () { face.hidden = true; });
|
|
552
229
|
}
|
|
553
230
|
|
|
554
231
|
return {
|
|
555
|
-
API: API,
|
|
556
|
-
MIN_FIRST_MS: MIN_FIRST_MS,
|
|
557
|
-
STORE_KEY: STORE_KEY,
|
|
232
|
+
API: core.API,
|
|
233
|
+
MIN_FIRST_MS: core.MIN_FIRST_MS,
|
|
234
|
+
STORE_KEY: core.STORE_KEY,
|
|
558
235
|
COPY: COPY,
|
|
559
|
-
guideOn: guideOn,
|
|
560
|
-
readPlan: readPlan,
|
|
561
|
-
initial: initial,
|
|
562
|
-
reduce: reduce,
|
|
563
|
-
firstSendWait: firstSendWait,
|
|
236
|
+
guideOn: core.guideOn,
|
|
237
|
+
readPlan: core.readPlan,
|
|
238
|
+
initial: core.initial,
|
|
239
|
+
reduce: core.reduce,
|
|
240
|
+
firstSendWait: core.firstSendWait,
|
|
564
241
|
boot: boot,
|
|
565
242
|
};
|
|
566
243
|
});
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
<!-- the start-a-project guide, as a scene in the game (task 1005275): drawn only when the
|
|
47
47
|
front-door-guide module is on and its server can talk -->
|
|
48
48
|
<link rel="stylesheet" href="/assets/guide-scene.css">
|
|
49
|
+
<!-- and plainly, for a visitor who does not want the game (task 1005276) -->
|
|
50
|
+
<link rel="stylesheet" href="/assets/guide-hall.css">
|
|
49
51
|
<style>
|
|
50
52
|
/* ── PAGE-LOCAL SHEET ────────────────────────────────────────────────────
|
|
51
53
|
The composition: how this page arranges the world's sculpture, and nothing
|
|
@@ -583,6 +585,8 @@ body{
|
|
|
583
585
|
--gd-gutter:var(--ds-space-gutter-narrow);--gd-face:56px;--gd-face-img:48px;
|
|
584
586
|
--gd-planbtn:inline-flex;--gd-planx:inline-grid;--gd-go-w:100%;
|
|
585
587
|
--gd-plan-show:none;--gd-plan-pos:absolute;--gd-plan-edge:0px;--gd-talk-under-plan:hidden;
|
|
588
|
+
/* and its plain twin (assets/guide-hall.css, task 1005276): the whole screen, square */
|
|
589
|
+
--gd-hall-radius:0;
|
|
586
590
|
}
|
|
587
591
|
:root[data-mode="dark"] body{
|
|
588
592
|
--layer-pos:relative;
|
|
@@ -974,6 +978,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
974
978
|
<section class="gd__talk" aria-labelledby="gdTitle">
|
|
975
979
|
<div class="gd__head">
|
|
976
980
|
<h2 class="gd__title" id="gdTitle">Start a project</h2>
|
|
981
|
+
<button type="button" class="gd__switch" data-gd="switch" hidden>Use the simple version</button>
|
|
977
982
|
<button type="button" class="gd__x" data-gd="close" aria-label="Close"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg></button>
|
|
978
983
|
</div>
|
|
979
984
|
<div class="gd__who-row">
|
|
@@ -1013,6 +1018,43 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
1013
1018
|
</div>
|
|
1014
1019
|
</div>
|
|
1015
1020
|
|
|
1021
|
+
<!-- THE GUIDE, PLAIN (task 1005276): the same conversation for a visitor who does not want
|
|
1022
|
+
the game. One chat behind both (assets/guide-core.js); the doors open the game unless
|
|
1023
|
+
this browser picked this one, and each offers the other. Hidden, like the scene. -->
|
|
1024
|
+
<div class="gh" id="guideHall" role="dialog" aria-modal="true" aria-labelledby="ghTitle" hidden>
|
|
1025
|
+
<section class="gh__panel" aria-labelledby="ghTitle">
|
|
1026
|
+
<div class="gh__head">
|
|
1027
|
+
<h2 class="gh__title" id="ghTitle">Start a project</h2>
|
|
1028
|
+
<button type="button" class="gh__link" data-gh="switch">Use the game version</button>
|
|
1029
|
+
<button type="button" class="gh__x" data-gh="close" aria-label="Close"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg></button>
|
|
1030
|
+
</div>
|
|
1031
|
+
<p class="gh__left" data-gh="left" aria-live="polite" hidden></p>
|
|
1032
|
+
<div class="gh__body" data-gh="body">
|
|
1033
|
+
<ol class="gh__log" data-gh="log" aria-live="polite" aria-label="Conversation"></ol>
|
|
1034
|
+
<section class="gh__plan" data-gh="plan" aria-labelledby="ghPlanH" hidden>
|
|
1035
|
+
<h3 class="gh__planh" id="ghPlanH">Your plan</h3>
|
|
1036
|
+
<div data-gh="plan-body"></div>
|
|
1037
|
+
</section>
|
|
1038
|
+
<p class="gh__saved" data-gh="saved" hidden></p>
|
|
1039
|
+
</div>
|
|
1040
|
+
<p class="gh__err" data-gh="err" role="alert" hidden></p>
|
|
1041
|
+
<form class="gh__say" data-gh="form" hidden>
|
|
1042
|
+
<label class="gh__label" for="ghText">Your message</label>
|
|
1043
|
+
<textarea class="gh__text" id="ghText" data-gh="text" rows="3" maxlength="1000" placeholder="What do you want to build?"></textarea>
|
|
1044
|
+
<input class="gh__hp" type="text" name="website" data-gh="website" tabindex="-1" autocomplete="off" aria-hidden="true">
|
|
1045
|
+
<div class="gh__row">
|
|
1046
|
+
<button type="submit" class="door door--go" data-gh="send">Send</button>
|
|
1047
|
+
<button type="button" class="door" data-gh="make-plan" hidden>Make my plan</button>
|
|
1048
|
+
<a class="gh__link" href="/projects?view=new">Skip to sign up</a>
|
|
1049
|
+
</div>
|
|
1050
|
+
</form>
|
|
1051
|
+
<div class="gh__end" data-gh="end" hidden>
|
|
1052
|
+
<button type="button" class="door" data-gh="plan-again" hidden>Make my plan</button>
|
|
1053
|
+
<a class="door door--go" data-gh="go" href="/projects?view=new">Sign up and get started</a>
|
|
1054
|
+
</div>
|
|
1055
|
+
</section>
|
|
1056
|
+
</div>
|
|
1057
|
+
|
|
1016
1058
|
<footer>
|
|
1017
1059
|
<div class="footrow">
|
|
1018
1060
|
<p><!--apex-only:start-->A non-profit, capture-resistant commons. <!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).</p>
|
|
@@ -1360,8 +1402,11 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
1360
1402
|
});
|
|
1361
1403
|
})();
|
|
1362
1404
|
</script>
|
|
1363
|
-
<!-- the start-a-project guide (task 1005275)
|
|
1405
|
+
<!-- the start-a-project guide: the shared conversation, then the game scene (task 1005275)
|
|
1406
|
+
and its plain twin (task 1005276) -->
|
|
1407
|
+
<script src="/assets/guide-core.js"></script>
|
|
1364
1408
|
<script src="/assets/guide-scene.js"></script>
|
|
1409
|
+
<script src="/assets/guide-hall.js"></script>
|
|
1365
1410
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
1366
1411
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
1367
1412
|
</body>
|
|
@@ -2766,7 +2766,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2766
2766
|
try { id = new URLSearchParams(location.search).get('id'); } catch (e) {}
|
|
2767
2767
|
if (id) { manageId = String(id); return 'manage'; }
|
|
2768
2768
|
try { handedProject = new URLSearchParams(location.search).get('project') || null; } catch (e) {}
|
|
2769
|
-
if (handedProject && /^#m(Delete|Disconnect)$/.test(location.hash || '')) handedSection = location.hash.slice(1);
|
|
2769
|
+
if (handedProject && /^#m(Delete|Disconnect|AppCard)$/.test(location.hash || '')) handedSection = location.hash.slice(1);
|
|
2770
2770
|
return 'mine';
|
|
2771
2771
|
}
|
|
2772
2772
|
return VIEWS[v] ? v : 'map';
|
|
@@ -9693,7 +9693,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
9693
9693
|
if (handedSection) {
|
|
9694
9694
|
var target = $(handedSection);
|
|
9695
9695
|
handedSection = null;
|
|
9696
|
-
|
|
9696
|
+
/* the app card (#mAppCard, the hall's Connect Render link — task 1004571) is
|
|
9697
|
+
drawn by its own read below, so it is still hidden here: land on it once shown */
|
|
9698
|
+
if (target && target.hidden && typeof MutationObserver === 'function') {
|
|
9699
|
+
var seen = new MutationObserver(function () {
|
|
9700
|
+
if (target.hidden) return;
|
|
9701
|
+
seen.disconnect();
|
|
9702
|
+
if (target.scrollIntoView) target.scrollIntoView({ block: 'start' });
|
|
9703
|
+
});
|
|
9704
|
+
seen.observe(target, { attributes: true, attributeFilter: ['hidden'] });
|
|
9705
|
+
} else if (target && target.scrollIntoView) target.scrollIntoView({ block: 'start' });
|
|
9697
9706
|
}
|
|
9698
9707
|
/* the SECOND, GitHub-backed read — fired only here (entry + tab
|
|
9699
9708
|
return), never from renderManage, which the fetch-free tick calls */
|