sidebud 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/dist/main.js +4441 -704
- package/package.json +2 -1
- package/skills/widget-packs/SKILL.md +8 -7
- package/src/manage/index.html +4 -0
- package/src/manage/manage.css +38 -0
- package/src/manage/manage.js +91 -6
- package/src/manage/packs.js +11 -4
- package/src/manage/settings.js +1 -1
- package/src/manage/setup.js +72 -11
package/src/manage/setup.js
CHANGED
|
@@ -4,6 +4,8 @@ const SETUP_STEPS = [
|
|
|
4
4
|
{ id: 'execution', title: 'Choose who does the work', short: 'Execution agent' },
|
|
5
5
|
{ id: 'voice', title: 'Choose a voice', short: 'Voice agent' },
|
|
6
6
|
{ id: 'integrations', title: 'Add integrations', short: 'Integrations' },
|
|
7
|
+
{ id: 'agents', title: 'Choose your default agents', short: 'Default agents' },
|
|
8
|
+
{ id: 'decisions', title: 'Fast decisions', short: 'Fast decisions', beta: true, optional: true },
|
|
7
9
|
{ id: 'phone', title: 'Pair your phone', short: 'Pair phone' },
|
|
8
10
|
{ id: 'done', title: 'All set', short: 'Finish' },
|
|
9
11
|
];
|
|
@@ -62,15 +64,15 @@ function checkCard(kind, intro) {
|
|
|
62
64
|
function setupStepper(current) {
|
|
63
65
|
return `<ol class="setup-steps">${SETUP_STEPS.map((step, index) => {
|
|
64
66
|
const done = step.id === 'done' ? Boolean(state.setup.completedAt) : state.setup.steps[step.id].done;
|
|
65
|
-
return `<li><button data-action="setup-step" data-step="${step.id}" class="${step.id === current ? 'current' : ''} ${done ? 'done' : ''}" ${step.id === current ? 'aria-current="step"' : ''}><span class="step-mark" aria-hidden="true">${done ? '✓' : index + 1}</span><span>${esc(step.short)}</span></button></li>`;
|
|
67
|
+
return `<li><button data-action="setup-step" data-step="${step.id}" class="${step.id === current ? 'current' : ''} ${done ? 'done' : ''}" ${step.id === current ? 'aria-current="step"' : ''}><span class="step-mark" aria-hidden="true">${done ? '✓' : index + 1}</span><span>${esc(step.short)}${step.beta ? '<span class="tag-beta">Beta</span>' : ''}</span></button></li>`;
|
|
66
68
|
}).join('')}</ol>`;
|
|
67
69
|
}
|
|
68
|
-
function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip = true } = {}) {
|
|
70
|
+
function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip = true, skipAction = 'setup-next' } = {}) {
|
|
69
71
|
const index = SETUP_STEPS.findIndex((step) => step.id === current);
|
|
70
|
-
return `<div class="setup-nav">${index > 0 ? `<button data-action="setup-step" data-step="${SETUP_STEPS[index - 1].id}">Back</button>` : '<span></span>'}<div class="actions">${skip ?
|
|
72
|
+
return `<div class="setup-nav">${index > 0 ? `<button data-action="setup-step" data-step="${SETUP_STEPS[index - 1].id}">Back</button>` : '<span></span>'}<div class="actions">${skip ? `<button data-action="${skipAction}">Skip for now</button>` : ''}<button class="primary" data-action="${nextAction}">${esc(next)}</button></div></div>`;
|
|
71
73
|
}
|
|
72
74
|
function appStep() {
|
|
73
|
-
return `<div class="setup-grid"><div class="card stack"><h3>Install Sidebud on your phone</h3><p>Scan this code with your phone’s camera. It opens the right store for your phone.</p><img class="download-qr" src="${esc(state.download.qr)}" alt="QR code to download the Sidebud app"><p class="hint">Or open <a href="${esc(state.download.url)}" target="_blank" rel="noopener">${esc(state.download.url.replace(/^https:\/\//, ''))}</a> on your phone. iPhone builds are available through TestFlight during the beta.</p></div><div class="card stack"><h3>What happens next</h3><ol class="plain-steps"><li><strong>Pick an execution agent.</strong> It does the work on this computer, and we test it with a real prompt.</li><li><strong>Pick a voice.</strong> We test that the voice can hand requests to your agent.</li><li><strong>Add integrations</strong> from the widget library, or have your agent build your own.</li><li><strong>Pair your phone</strong> by scanning a code in the app. You can <a href="#setup/phone">pair now</a> if the app is already installed.</li></ol></div></div>${setupNav('app', { next: 'I have the app', nextAction: 'setup-app-done', skip: false })}`;
|
|
75
|
+
return `<div class="setup-grid"><div class="card stack"><h3>Install Sidebud on your phone</h3><p>Scan this code with your phone’s camera. It opens the right store for your phone.</p><img class="download-qr" src="${esc(state.download.qr)}" alt="QR code to download the Sidebud app"><p class="hint">Or open <a href="${esc(state.download.url)}" target="_blank" rel="noopener">${esc(state.download.url.replace(/^https:\/\//, ''))}</a> on your phone. iPhone builds are available through TestFlight during the beta.</p></div><div class="card stack"><h3>What happens next</h3><ol class="plain-steps"><li><strong>Pick an execution agent.</strong> It does the work on this computer, and we test it with a real prompt.</li><li><strong>Pick a voice.</strong> We test that the voice can hand requests to your agent.</li><li><strong>Add integrations</strong> from the widget library, or have your agent build your own.</li><li><strong>Choose your default agents</strong>: the agents app with your main work agents, such as T3 Agents.</li><li><strong>Turn on fast decisions</strong> (optional, beta) so simple requests are answered in about a second.</li><li><strong>Pair your phone</strong> by scanning a code in the app. You can <a href="#setup/phone">pair now</a> if the app is already installed.</li></ol></div></div>${setupNav('app', { next: 'I have the app', nextAction: 'setup-app-done', skip: false })}`;
|
|
74
76
|
}
|
|
75
77
|
function executionStep() {
|
|
76
78
|
if (!state.providers || state.connector.kind === 'memory')
|
|
@@ -86,7 +88,7 @@ function voiceStep() {
|
|
|
86
88
|
const model = provider.models.some((option) => option.id === saved.model) ? saved.model : provider.defaultModel;
|
|
87
89
|
const agent = state.setup.companionAgent;
|
|
88
90
|
// Same controls and ids as the Voice page, so its provider/model handlers and the voice sample work here too.
|
|
89
|
-
return `<div class="card stack"><h3>Voice agent</h3><p class="hint">The voice agent talks with you in calls and typed chat, and hands work to your execution agent. Its API key stays on this computer.</p><label>Voice provider<select id="voice-provider">${Object.values(catalog).map((option) => `<option value="${esc(option.id)}" ${option.id === selectedId ? 'selected' : ''}>${esc(option.label)}${option.id === 'gemini' ? ' (recommended)' : ''}</option>`).join('')}</select></label><p id="voice-untested" class="hint" ${provider.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><label>Voice model<select id="voice-model">${voiceModelOptions(provider, model)}</select></label><div id="voice-endpoint" class="stack">${voiceEndpointFields(provider, saved)}</div><label>Voice<select id="voice-name">${voiceNameOptions(voiceOptionsFor(provider, model), saved.voiceName || provider.defaultVoice)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice">Play voice sample</button><button type="button" data-action="stop-voice-preview" hidden>Stop sample</button><audio id="voice-preview-audio" controls preload="none" hidden></audio></div><p id="voice-preview-status" class="hint" role="status" aria-live="polite">Save a key below, then play a sample.</p></div><div id="voice-key-panel" class="stack">${voiceKeysView(selectedId)}</div><div class="actions"><button class="primary" data-action="setup-save-voice">Save voice and test it</button></div>${checkCard('voice', agent ? `Sends the voice agent a request as if you typed it, and checks that ${esc(agent.label)} receives and answers it.` : 'Finish the execution agent step first.')}${setupNav('voice', { next: state.setup.steps.voice.done ? 'Continue' : 'Continue without testing', skip: false })}`;
|
|
91
|
+
return `<div class="card stack"><h3>Voice agent</h3><p class="hint">The voice agent talks with you in calls and typed chat, and hands work to your execution agent. Its API key stays on this computer.</p><label>Voice provider<select id="voice-provider">${Object.values(catalog).map((option) => `<option value="${esc(option.id)}" ${option.id === selectedId ? 'selected' : ''}>${esc(option.label)}${option.id === 'gemini' ? ' (recommended)' : ''}</option>`).join('')}</select></label><p id="voice-untested" class="hint" ${provider.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><label>Voice model<select id="voice-model">${voiceModelOptions(provider, model)}</select></label><div id="voice-endpoint" class="stack">${voiceEndpointFields(provider, saved)}</div><label>Voice<select id="voice-name">${voiceNameOptions(voiceOptionsFor(provider, model), saved.voiceName || provider.defaultVoice)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice">Play voice sample</button><button type="button" data-action="stop-voice-preview" hidden>Stop sample</button><audio id="voice-preview-audio" controls preload="none" hidden></audio></div><p id="voice-preview-status" class="hint" role="status" aria-live="polite">Save a key below, then play a sample.</p></div><div id="voice-key-panel" class="stack">${voiceKeysView(selectedId)}</div><div class="card stack"><h3>Spoken mute and unmute</h3>${commandPhraseFields(voice.commands)}<p class="hint">The defaults work as they are. Change them any time on the <a href="#voice">Voice</a> page, along with auto-mute.</p><div class="actions"><button data-action="setup-save-commands">Save phrases</button></div></div><div class="actions"><button class="primary" data-action="setup-save-voice">Save voice and test it</button></div>${checkCard('voice', agent ? `Sends the voice agent a request as if you typed it, and checks that ${esc(agent.label)} receives and answers it.` : 'Finish the execution agent step first.')}${setupNav('voice', { next: state.setup.steps.voice.done ? 'Continue' : 'Continue without testing', skip: false })}`;
|
|
90
92
|
}
|
|
91
93
|
/** Library packs whose app is on this computer are suggested; the owner can untick any of them. */
|
|
92
94
|
function integrationSelections() {
|
|
@@ -108,6 +110,25 @@ function integrationsStep() {
|
|
|
108
110
|
: '<p class="hint">The widget library has no packs yet.</p>';
|
|
109
111
|
return `<div class="card stack"><h3>What should Sidebud reach?</h3><p class="hint">These are widget packs from the Sidebud widget library. Each is added disabled. Your execution agent then sets it up, uses settings and keys it finds on this computer, and reports on your phone what it needs from you. Your agent can also build packs for anything else you use.</p>${packs.length ? `<div class="integration-options">${packs.map(option).join('')}</div>` : empty}<p class="hint">More integrations, and ways to make your own, are under <a href="#mcp">MCP & widgets</a>.</p></div>${setupNav('integrations', { next: 'Add selected and continue', nextAction: 'setup-add-integrations' })}`;
|
|
110
112
|
}
|
|
113
|
+
/** The user's default agents: their main work agents app, which is not the execution agent. */
|
|
114
|
+
function agentsStep() {
|
|
115
|
+
const apps = (state.packs?.packs || []).filter((pack) => pack.agentsApp);
|
|
116
|
+
const chosen = state.packs?.defaultAgents || '';
|
|
117
|
+
const status = (pack) => (pack.status.state === 'ok' ? '' : pack.status.state === 'disabled' ? ' · not turned on yet' : ' · being set up');
|
|
118
|
+
const body = apps.length
|
|
119
|
+
? `<div class="integration-options">${apps.map((pack) => `<label class="integration-option"><input type="radio" name="default-agents" value="${esc(pack.id)}" ${pack.id === chosen ? 'checked' : ''}><span><strong>${esc(pack.name)}</strong>${pack.id === chosen ? ' <span class="pill">Default</span>' : ''}<br><span class="hint">${esc(pack.description)}${esc(status(pack))}</span></span></label>`).join('')}</div>`
|
|
120
|
+
: '<p class="muted">No agents app is installed yet. Add T3 Agents, Hermes, OpenClaw, or another agents app under <a href="#mcp">MCP & widgets</a>, then choose it under Execution → Default agents.</p>';
|
|
121
|
+
return `<div class="card stack"><h3>Where do your main work agents live?</h3><p class="hint">Your default agents are the agents app you work with most. When you ask “what agents are running?”, Sidebud checks this app. They are not your execution agent: the execution agent takes requests from voice and does the work on this computer.</p>${body}</div>${setupNav('agents', apps.length ? { next: 'Save and continue', nextAction: 'setup-save-agents' } : { next: 'Continue', nextAction: 'setup-ack-agents', skip: false })}`;
|
|
122
|
+
}
|
|
123
|
+
/** Optional while in beta: a fast decision model (the user's own TypeSafe key, or a local server). */
|
|
124
|
+
function decisionsStep() {
|
|
125
|
+
const jev = state.jev;
|
|
126
|
+
const on = jev.status.state === 'ready';
|
|
127
|
+
return `<div class="card stack"><div class="row"><div><h3>Answer simple requests in about a second <span class="tag-beta">Beta</span></h3><p class="hint">A fast decision model (TypeSafe’s Jev) decides whether this computer can handle a request itself: open or quit apps, volume, music, reminders, what your default agents are doing, one integration’s data, and home tiles. Anything else, or anything it is unsure about, goes to your execution agent as usual. You can skip this and turn it on later under Fast decisions.</p></div><span class="badge ${on ? 'good' : 'warn'}">${on ? `On · ${jev.status.kind === 'local' ? 'local server' : 'TypeSafe'}` : 'Off'}</span></div>
|
|
128
|
+
<label>TypeSafe API key<input id="setup-jev-key" type="password" autocomplete="new-password" placeholder="${jev.key.isSet ? 'A key is saved. Paste a new one to replace it.' : 'Paste your key from typesafe.ai'}"></label>
|
|
129
|
+
<label>Or a local Jev-compatible server (optional)<input id="setup-jev-url" type="url" placeholder="Empty: TypeSafe’s hosted API" value="${esc(jev.config.url ?? '')}"></label>
|
|
130
|
+
<p class="hint">Your key stays in this computer’s keychain and is only sent to TypeSafe. A local server gets no key.</p></div>${setupNav('decisions', { next: on ? 'Test and continue' : 'Save and test', nextAction: 'setup-save-decisions', skipAction: 'setup-skip-decisions' })}`;
|
|
131
|
+
}
|
|
111
132
|
function phoneStep() {
|
|
112
133
|
const active = state.devices.filter((device) => !device.revokedAt);
|
|
113
134
|
if (active.length)
|
|
@@ -120,6 +141,7 @@ function completedView() {
|
|
|
120
141
|
{ hash: 'execution', title: 'Execution agents', text: 'Add agents, change the default, set thinking levels and prompts.' },
|
|
121
142
|
{ hash: 'mcp', title: 'MCP & widgets', text: 'Add integrations, set them up, and choose what your phone shows and hears.' },
|
|
122
143
|
{ hash: 'voice', title: 'Voice', text: 'Change the voice agent, model, and voice.' },
|
|
144
|
+
{ hash: 'decisions', title: 'Fast decisions (Beta)', text: 'Add or change your Jev key, or a local server, and test it.' },
|
|
123
145
|
{ hash: 'connection', title: 'Connection', text: 'Pair another phone or remove one.' },
|
|
124
146
|
];
|
|
125
147
|
return `<div class="card stack setup-complete"><div class="row"><span class="done-badge" aria-hidden="true">✓</span><div><h3>Well done. Sidebud is set up on ${esc(state.name)}.</h3><p>Continue in the Sidebud app on your phone from here: start a voice chat, send a message, and check on your widgets and running work.</p></div></div></div><div class="card stack"><div><h3>Manage everything from this dashboard, any time</h3><p class="hint">Keep the companion running on this computer. Open this page again with <span class="code">sidebud manage</span> or <span class="code">sidebud setup</span>.</p></div><div class="manage-links">${manage.map((item) => `<a class="manage-link" href="#${item.hash}"><strong>${esc(item.title)}</strong><span class="hint">${esc(item.text)}</span></a>`).join('')}</div></div><div class="actions"><button data-action="setup-reopen">Run setup again</button></div>`;
|
|
@@ -128,17 +150,17 @@ function doneStep() {
|
|
|
128
150
|
if (state.setup.completedAt) return completedView();
|
|
129
151
|
const rows = SETUP_STEPS.filter((step) => step.id !== 'done').map((step) => {
|
|
130
152
|
const done = state.setup.steps[step.id].done;
|
|
131
|
-
return `<li class="${done ? 'done' : ''}"><span class="step-mark" aria-hidden="true">${done ? '✓' : '•'}</span><span>${esc(step.title)}</span>${done ? '' : `<button data-action="setup-step" data-step="${step.id}">Finish this</button>`}</li>`;
|
|
153
|
+
return `<li class="${done ? 'done' : ''}"><span class="step-mark" aria-hidden="true">${done ? '✓' : '•'}</span><span>${esc(step.title)}${step.optional ? ' <span class="hint">(optional)</span>' : ''}</span>${done ? '' : `<button data-action="setup-step" data-step="${step.id}">Finish this</button>`}</li>`;
|
|
132
154
|
}).join('');
|
|
133
|
-
const ready = SETUP_STEPS.every((step) => step.id === 'done' || state.setup.steps[step.id].done);
|
|
155
|
+
const ready = SETUP_STEPS.every((step) => step.id === 'done' || step.optional || state.setup.steps[step.id].done);
|
|
134
156
|
return `<div class="card stack"><h3>${state.setup.completedAt ? 'Setup is finished' : ready ? 'Everything is ready' : 'Almost there'}</h3><ul class="setup-summary">${rows}</ul><p class="hint">Change any of this later from the sections on the left. Your phone shows your widgets, running tasks, and a button to start a voice chat.</p><div class="actions">${state.setup.completedAt ? '<button data-action="setup-reopen">Run setup again</button>' : '<button class="primary" data-action="setup-complete">Finish setup</button>'}</div></div>`;
|
|
135
157
|
}
|
|
136
158
|
function setupView() {
|
|
137
159
|
const current = currentSetupStep();
|
|
138
160
|
const step = SETUP_STEPS.find((candidate) => candidate.id === current);
|
|
139
|
-
const body = { app: appStep, execution: executionStep, voice: voiceStep, integrations: integrationsStep, phone: phoneStep, done: doneStep }[current]();
|
|
161
|
+
const body = { app: appStep, execution: executionStep, voice: voiceStep, integrations: integrationsStep, agents: agentsStep, decisions: decisionsStep, phone: phoneStep, done: doneStep }[current]();
|
|
140
162
|
const title = current === 'done' && state.setup.completedAt ? 'Well done' : step.title;
|
|
141
|
-
return `<section class="section setup"><div><h2>${esc(title)}</h2><p class="lede">Set up Sidebud on ${esc(state.name)}. Keys, accounts, and agents stay on this computer.</p></div>${setupStepper(current)}${body}</section>`;
|
|
163
|
+
return `<section class="section setup"><div><h2>${esc(title)}${step.beta ? ' <span class="tag-beta">Beta</span>' : ''}</h2><p class="lede">Set up Sidebud on ${esc(state.name)}. Keys, accounts, and agents stay on this computer.</p></div>${setupStepper(current)}${body}</section>`;
|
|
142
164
|
}
|
|
143
165
|
/** Handles setup buttons; returns false for actions it does not own. */
|
|
144
166
|
async function handleSetupAction(action, button) {
|
|
@@ -160,16 +182,25 @@ async function handleSetupAction(action, button) {
|
|
|
160
182
|
const live = state.voice.catalog[provider];
|
|
161
183
|
const endpoint = voiceEndpoint();
|
|
162
184
|
if (live?.needsWorkspaceId && !endpoint.workspaceId) throw new Error(`Add your ${live.label} workspace ID.`);
|
|
185
|
+
const phrases = commandPhrases();
|
|
186
|
+
if (phrases.error) throw new Error(phrases.error);
|
|
163
187
|
const typedKey = document.querySelector(`[data-voice-key="${provider}"]`)?.value;
|
|
164
188
|
if (typedKey) await api('voice-key', { provider, value: typedKey });
|
|
165
189
|
else if (!state.voice.keyStates[provider]?.isSet) throw new Error(`Add a ${live.label} API key first.`);
|
|
166
|
-
// Keep the owner's other voice settings (
|
|
167
|
-
await api('voice', { voice: { ...state.voice.voice, provider, model: $('#voice-model').value || null, voiceName: $('#voice-name').value || null, ...endpoint } });
|
|
190
|
+
// Keep the owner's other voice settings (call limits, auto-mute); setup changes the agent and the spoken commands.
|
|
191
|
+
await api('voice', { voice: { ...state.voice.voice, provider, model: $('#voice-model').value || null, voiceName: $('#voice-name').value || null, ...endpoint, commands: phrases.commands } });
|
|
168
192
|
await reload();
|
|
169
193
|
if (await applyChanges()) {
|
|
170
194
|
await api('setup/check', { kind: 'voice' });
|
|
171
195
|
await reload();
|
|
172
196
|
}
|
|
197
|
+
} else if (action === 'setup-save-commands') {
|
|
198
|
+
// Saved on their own too, so they need no voice key and survive skipping the voice test.
|
|
199
|
+
const phrases = commandPhrases();
|
|
200
|
+
if (phrases.error) throw new Error(phrases.error);
|
|
201
|
+
await api('voice', { voice: { ...state.voice.voice, commands: phrases.commands } });
|
|
202
|
+
notice('Spoken commands saved.');
|
|
203
|
+
await reload();
|
|
173
204
|
} else if (action === 'setup-add-integrations') {
|
|
174
205
|
const chosen = [...document.querySelectorAll('[data-integration]')].filter((box) => box.checked && !box.disabled).map((box) => box.dataset.integration);
|
|
175
206
|
const failures = [];
|
|
@@ -189,6 +220,36 @@ async function handleSetupAction(action, button) {
|
|
|
189
220
|
if (failures.length) notice(`Added, but some need attention: ${failures.join(' ')}`, true);
|
|
190
221
|
else if (chosen.length) notice(`Added. Your execution agent is setting up ${chosen.length === 1 ? 'the integration' : 'the integrations'} and reports on your phone when it needs you.`);
|
|
191
222
|
next();
|
|
223
|
+
} else if (action === 'setup-save-agents') {
|
|
224
|
+
const picked = document.querySelector('input[name="default-agents"]:checked')?.value;
|
|
225
|
+
if (!picked) throw new Error('Choose the agents app with your main work agents, or skip for now.');
|
|
226
|
+
await api('default-agents', { packId: picked });
|
|
227
|
+
await api('setup/ack', { step: 'agents' });
|
|
228
|
+
await reload();
|
|
229
|
+
next();
|
|
230
|
+
} else if (action === 'setup-ack-agents') {
|
|
231
|
+
await api('setup/ack', { step: 'agents' });
|
|
232
|
+
await reload();
|
|
233
|
+
next();
|
|
234
|
+
} else if (action === 'setup-skip-decisions') {
|
|
235
|
+
// Optional while in beta: skipping counts as done, so it never holds up finishing setup.
|
|
236
|
+
await api('setup/ack', { step: 'decisions' });
|
|
237
|
+
await reload();
|
|
238
|
+
next();
|
|
239
|
+
} else if (action === 'setup-save-decisions') {
|
|
240
|
+
const key = $('#setup-jev-key').value.trim();
|
|
241
|
+
const url = $('#setup-jev-url').value.trim();
|
|
242
|
+
if (!key && !url && !state.jev.key.isSet) throw new Error('Paste your TypeSafe key, or add a local server, or skip for now.');
|
|
243
|
+
if (key) await api('jev-key', { value: key });
|
|
244
|
+
if (url !== (state.jev.config.url ?? '') || state.jev.config.mode !== 'auto') await api('jev', { jev: { ...state.jev.config, mode: 'auto', url: url || null } });
|
|
245
|
+
await reload();
|
|
246
|
+
// Keys and servers apply on restart; the test then runs against the new setting.
|
|
247
|
+
if (state.restart.pending && !(await applyChanges())) return true;
|
|
248
|
+
const result = await api('jev/test', {});
|
|
249
|
+
await api('setup/ack', { step: 'decisions' });
|
|
250
|
+
await reload();
|
|
251
|
+
notice(`Fast decisions work: ${result.model} answered in ${result.ms} ms.`);
|
|
252
|
+
next();
|
|
192
253
|
} else if (action === 'setup-complete') {
|
|
193
254
|
await api('setup/complete', {});
|
|
194
255
|
history.pushState(null, '', '#setup/done');
|