@memoryblock/web 0.1.0-beta
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/LICENSE +21 -0
- package/package.json +39 -0
- package/public/app.js +205 -0
- package/public/components/archive.js +110 -0
- package/public/components/auth.js +70 -0
- package/public/components/blocks.js +444 -0
- package/public/components/chat.js +394 -0
- package/public/components/create-block.js +108 -0
- package/public/components/settings.js +363 -0
- package/public/components/setup.js +301 -0
- package/public/index.html +16 -0
- package/public/style.css +1871 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings component — theme, plugins, session.
|
|
3
|
+
* Plugin settings panels are auto-generated from plugin settings schemas.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { clearToken, getTheme, setTheme, api } from '../app.js';
|
|
7
|
+
|
|
8
|
+
export function renderSettings(container) {
|
|
9
|
+
const currentTheme = getTheme();
|
|
10
|
+
|
|
11
|
+
container.innerHTML = `
|
|
12
|
+
<div class="settings-grid">
|
|
13
|
+
<div class="settings-nav">
|
|
14
|
+
<button class="settings-nav-item active" data-target="general">General</button>
|
|
15
|
+
<button class="settings-nav-item" data-target="appearance">Appearance</button>
|
|
16
|
+
<button class="settings-nav-item" data-target="plugins">Plugins</button>
|
|
17
|
+
<button class="settings-nav-item" data-target="advanced">Advanced</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="settings-content">
|
|
21
|
+
<!-- General Settings -->
|
|
22
|
+
<div class="settings-content-section active" id="setting-general">
|
|
23
|
+
<div class="settings-panel">
|
|
24
|
+
<h3>Workspace</h3>
|
|
25
|
+
<p class="desc">Manage your core memoryblock environment and data.</p>
|
|
26
|
+
<div class="setting-row">
|
|
27
|
+
<div>
|
|
28
|
+
<div class="setting-label">API Endpoint</div>
|
|
29
|
+
<div class="setting-desc">The local daemon server URL for memoryblock.</div>
|
|
30
|
+
</div>
|
|
31
|
+
<span class="dim" style="font-family: monospace;">${location.origin}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="setting-row">
|
|
34
|
+
<div>
|
|
35
|
+
<div class="setting-label">Channel Connectivity Alerts</div>
|
|
36
|
+
<div class="setting-desc">Broadcast "online" and "offline" status announcements to channels.</div>
|
|
37
|
+
</div>
|
|
38
|
+
<label class="plugin-toggle">
|
|
39
|
+
<input type="checkbox" id="setting-channel-alerts" disabled>
|
|
40
|
+
<span class="toggle-slider"></span>
|
|
41
|
+
</label>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="setting-row">
|
|
44
|
+
<div>
|
|
45
|
+
<div class="setting-label">Session Status</div>
|
|
46
|
+
<div class="setting-desc">You are connected to the API correctly.</div>
|
|
47
|
+
</div>
|
|
48
|
+
<button class="btn-small danger" id="logout-btn">disconnect</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Appearance Settings -->
|
|
54
|
+
<div class="settings-content-section" id="setting-appearance">
|
|
55
|
+
<div class="settings-panel">
|
|
56
|
+
<h3>Appearance</h3>
|
|
57
|
+
<p class="desc">Customize the look and feel of your memoryblock dashboard.</p>
|
|
58
|
+
<div class="setting-row">
|
|
59
|
+
<div>
|
|
60
|
+
<div class="setting-label">Theme</div>
|
|
61
|
+
<div class="setting-desc">Select between light and dark mode.</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="theme-selector">
|
|
64
|
+
<button class="theme-option ${currentTheme === 'light' ? 'active' : ''}" data-theme="light">☀ light</button>
|
|
65
|
+
<button class="theme-option ${currentTheme === 'dark' ? 'active' : ''}" data-theme="dark">◑ dark</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Plugins Settings -->
|
|
72
|
+
<div class="settings-content-section" id="setting-plugins">
|
|
73
|
+
<div class="settings-panel">
|
|
74
|
+
<h3>Plugins Configuration</h3>
|
|
75
|
+
<p class="desc">Manage settings and API keys for installed plugins.</p>
|
|
76
|
+
<div id="plugin-settings-list" class="plugin-settings-list">
|
|
77
|
+
<div class="loading">Loading plugins...</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Advanced Settings -->
|
|
83
|
+
<div class="settings-content-section" id="setting-advanced">
|
|
84
|
+
<div class="settings-panel">
|
|
85
|
+
<h3>Advanced Setup</h3>
|
|
86
|
+
<p class="desc">Experimental features and dangerous actions.</p>
|
|
87
|
+
<div class="empty-state" style="padding: 20px;">
|
|
88
|
+
<span class="empty-icon" style="font-size: 1.5rem">🚧</span>
|
|
89
|
+
<p>More features coming soon.</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
// Tab Navigation
|
|
98
|
+
const navItems = container.querySelectorAll('.settings-nav-item');
|
|
99
|
+
navItems.forEach(btn => {
|
|
100
|
+
btn.addEventListener('click', () => {
|
|
101
|
+
navItems.forEach(b => b.classList.remove('active'));
|
|
102
|
+
btn.classList.add('active');
|
|
103
|
+
|
|
104
|
+
container.querySelectorAll('.settings-content-section').forEach(sec => {
|
|
105
|
+
sec.classList.remove('active');
|
|
106
|
+
});
|
|
107
|
+
container.querySelector(`#setting-${btn.dataset.target}`).classList.add('active');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Theme toggle
|
|
112
|
+
container.querySelectorAll('.theme-option').forEach(btn => {
|
|
113
|
+
btn.addEventListener('click', () => {
|
|
114
|
+
const theme = btn.dataset.theme;
|
|
115
|
+
setTheme(theme);
|
|
116
|
+
container.querySelectorAll('.theme-option').forEach(b => b.classList.remove('active'));
|
|
117
|
+
btn.classList.add('active');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Logout
|
|
122
|
+
document.getElementById('logout-btn').addEventListener('click', () => {
|
|
123
|
+
clearToken();
|
|
124
|
+
window.dispatchEvent(new Event('auth:logout'));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Load plugin settings
|
|
128
|
+
loadPluginSettings();
|
|
129
|
+
|
|
130
|
+
// Load general settings
|
|
131
|
+
loadGeneralSettings();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadGeneralSettings() {
|
|
135
|
+
try {
|
|
136
|
+
const data = await api('/api/config');
|
|
137
|
+
if (!data || !data.config) return;
|
|
138
|
+
|
|
139
|
+
const alertsToggle = document.getElementById('setting-channel-alerts');
|
|
140
|
+
if (alertsToggle) {
|
|
141
|
+
alertsToggle.checked = data.config.channelAlerts !== false;
|
|
142
|
+
alertsToggle.disabled = false;
|
|
143
|
+
|
|
144
|
+
alertsToggle.addEventListener('change', async () => {
|
|
145
|
+
alertsToggle.disabled = true;
|
|
146
|
+
try {
|
|
147
|
+
await api('/api/config', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ channelAlerts: alertsToggle.checked })
|
|
151
|
+
});
|
|
152
|
+
} catch {
|
|
153
|
+
alertsToggle.checked = !alertsToggle.checked; // revert on fail
|
|
154
|
+
} finally {
|
|
155
|
+
alertsToggle.disabled = false;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// silently fail to load config
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function loadPluginSettings() {
|
|
165
|
+
const list = document.getElementById('plugin-settings-list');
|
|
166
|
+
if (!list) return;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const data = await api('/api/plugins');
|
|
170
|
+
const plugins = data.plugins || [];
|
|
171
|
+
|
|
172
|
+
if (plugins.length === 0) {
|
|
173
|
+
list.innerHTML = '<div class="dim">No plugins installed.</div>';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
list.innerHTML = plugins.map(pl => `
|
|
178
|
+
<div class="plugin-settings-card" data-plugin="${pl.id}">
|
|
179
|
+
<div class="plugin-settings-header" style="align-items: center; display: flex; justify-content: space-between;">
|
|
180
|
+
<div class="plugin-settings-info" style="flex: 1;">
|
|
181
|
+
<span class="plugin-settings-name">
|
|
182
|
+
${pl.core ? '●' : '○'} ${pl.name}
|
|
183
|
+
${pl.core ? '<span class="badge-core">core</span>' : ''}
|
|
184
|
+
</span>
|
|
185
|
+
<span class="plugin-settings-desc">${pl.description}</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
188
|
+
<label class="plugin-toggle" title="${pl.core ? 'Core plugins cannot be disabled' : 'Toggle plugin installation'}">
|
|
189
|
+
<input type="checkbox" class="plugin-install-toggle" data-id="${pl.id}" ${pl.installed ? 'checked' : ''} ${pl.core ? 'disabled' : ''}>
|
|
190
|
+
<span class="toggle-slider"></span>
|
|
191
|
+
</label>
|
|
192
|
+
${pl.settings && Object.keys(pl.settings).length > 0
|
|
193
|
+
? `<button class="btn-small plugin-expand" data-id="${pl.id}">settings ▾</button>`
|
|
194
|
+
: ''}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<!-- Logs container for install/uninstall streaming -->
|
|
198
|
+
<div class="plugin-logs" id="plugin-logs-${pl.id}" style="display:none; margin: 12px 0; background: var(--bg-deep); padding: 12px; border-radius: 6px; font-family: monospace; font-size: 0.85rem; color: #a3a3a3; max-height: 200px; overflow-y: auto; white-space: pre-wrap;"></div>
|
|
199
|
+
<div class="plugin-settings-body" id="plugin-body-${pl.id}" style="display:none;">
|
|
200
|
+
${renderPluginFields(pl)}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
`).join('');
|
|
204
|
+
|
|
205
|
+
// Expand/collapse settings
|
|
206
|
+
list.querySelectorAll('.plugin-expand').forEach(btn => {
|
|
207
|
+
btn.addEventListener('click', () => {
|
|
208
|
+
const body = document.getElementById(`plugin-body-${btn.dataset.id}`);
|
|
209
|
+
if (body) {
|
|
210
|
+
const open = body.style.display !== 'none';
|
|
211
|
+
body.style.display = open ? 'none' : 'block';
|
|
212
|
+
btn.textContent = open ? 'settings ▾' : 'settings ▴';
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Install/Uninstall toggle
|
|
218
|
+
list.querySelectorAll('.plugin-install-toggle').forEach(toggle => {
|
|
219
|
+
toggle.addEventListener('change', async (e) => {
|
|
220
|
+
const isChecked = toggle.checked; // the target state
|
|
221
|
+
const pluginId = toggle.dataset.id;
|
|
222
|
+
const action = isChecked ? 'install' : 'uninstall';
|
|
223
|
+
const logsDiv = document.getElementById(`plugin-logs-${pluginId}`);
|
|
224
|
+
|
|
225
|
+
toggle.disabled = true; // prevent multi-click
|
|
226
|
+
logsDiv.style.display = 'block';
|
|
227
|
+
logsDiv.textContent = `Starting ${action} for ${pluginId}...\n`;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch(`${location.origin}/api/plugins/${pluginId}/${action}`, {
|
|
231
|
+
method: action === 'install' ? 'POST' : 'DELETE',
|
|
232
|
+
headers: { 'Authorization': `Bearer ${document.cookie || localStorage.getItem('mblk_token')}` } // api fetch overrides
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!res.body) throw new Error('ReadableStream not supported');
|
|
236
|
+
const reader = res.body.getReader();
|
|
237
|
+
const decoder = new TextDecoder('utf-8');
|
|
238
|
+
|
|
239
|
+
while (true) {
|
|
240
|
+
const { done, value } = await reader.read();
|
|
241
|
+
if (done) break;
|
|
242
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
243
|
+
|
|
244
|
+
// Parse the __RESULT__ marker if present
|
|
245
|
+
if (chunk.includes('__RESULT__')) {
|
|
246
|
+
const [logPart, resultStr] = chunk.split('__RESULT__');
|
|
247
|
+
logsDiv.textContent += logPart;
|
|
248
|
+
try {
|
|
249
|
+
const result = JSON.parse(resultStr);
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
logsDiv.textContent += `\nError: ${result.message}`;
|
|
252
|
+
toggle.checked = !isChecked; // revert
|
|
253
|
+
} else {
|
|
254
|
+
logsDiv.textContent += `\nSuccess: ${result.message}`;
|
|
255
|
+
}
|
|
256
|
+
} catch { }
|
|
257
|
+
} else {
|
|
258
|
+
logsDiv.textContent += chunk;
|
|
259
|
+
}
|
|
260
|
+
logsDiv.scrollTop = logsDiv.scrollHeight;
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
logsDiv.textContent += `\nError: ${err.message}`;
|
|
264
|
+
toggle.checked = !isChecked; // revert
|
|
265
|
+
} finally {
|
|
266
|
+
toggle.disabled = false;
|
|
267
|
+
setTimeout(() => { if (logsDiv) logsDiv.style.display = 'none'; }, 3000);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Save handlers
|
|
273
|
+
list.querySelectorAll('.plugin-save').forEach(btn => {
|
|
274
|
+
btn.addEventListener('click', async () => {
|
|
275
|
+
const pluginId = btn.dataset.id;
|
|
276
|
+
const body = document.getElementById(`plugin-body-${pluginId}`);
|
|
277
|
+
if (!body) return;
|
|
278
|
+
|
|
279
|
+
const values = {};
|
|
280
|
+
body.querySelectorAll('[data-field]').forEach(input => {
|
|
281
|
+
if (input.type === 'checkbox') {
|
|
282
|
+
values[input.dataset.field] = input.checked;
|
|
283
|
+
} else if (input.type === 'number') {
|
|
284
|
+
values[input.dataset.field] = parseInt(input.value, 10);
|
|
285
|
+
} else {
|
|
286
|
+
values[input.dataset.field] = input.value;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await api(`/api/plugins/${pluginId}/settings`, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: { 'Content-Type': 'application/json' },
|
|
294
|
+
body: JSON.stringify(values),
|
|
295
|
+
});
|
|
296
|
+
btn.textContent = '✓ saved';
|
|
297
|
+
setTimeout(() => btn.textContent = 'save', 1500);
|
|
298
|
+
} catch {
|
|
299
|
+
btn.textContent = '✗ error';
|
|
300
|
+
setTimeout(() => btn.textContent = 'save', 1500);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
list.innerHTML = '<div class="dim">Could not load plugins.</div>';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderPluginFields(plugin) {
|
|
310
|
+
if (!plugin.settings) return '';
|
|
311
|
+
|
|
312
|
+
const fields = Object.entries(plugin.settings).map(([key, field]) => {
|
|
313
|
+
const val = field.default ?? '';
|
|
314
|
+
|
|
315
|
+
if (field.type === 'select') {
|
|
316
|
+
const opts = (field.options || []).map(o =>
|
|
317
|
+
`<option value="${o}" ${o === val ? 'selected' : ''}>${o}</option>`
|
|
318
|
+
).join('');
|
|
319
|
+
return `
|
|
320
|
+
<div class="setup-field">
|
|
321
|
+
<label>${field.label}</label>
|
|
322
|
+
<select data-field="${key}" class="settings-input">${opts}</select>
|
|
323
|
+
</div>`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (field.type === 'toggle') {
|
|
327
|
+
return `
|
|
328
|
+
<div class="setup-field toggle-field">
|
|
329
|
+
<label>${field.label}</label>
|
|
330
|
+
<label class="plugin-toggle">
|
|
331
|
+
<input type="checkbox" data-field="${key}" ${val ? 'checked' : ''}>
|
|
332
|
+
<span class="toggle-slider"></span>
|
|
333
|
+
</label>
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (field.type === 'number') {
|
|
338
|
+
return `
|
|
339
|
+
<div class="setup-field">
|
|
340
|
+
<label>${field.label}</label>
|
|
341
|
+
<input type="number" data-field="${key}" value="${val}"
|
|
342
|
+
${field.min !== undefined ? `min="${field.min}"` : ''}
|
|
343
|
+
${field.max !== undefined ? `max="${field.max}"` : ''}
|
|
344
|
+
class="settings-input">
|
|
345
|
+
</div>`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return `
|
|
349
|
+
<div class="setup-field">
|
|
350
|
+
<label>${field.label}</label>
|
|
351
|
+
<input type="${field.type === 'password' ? 'password' : 'text'}"
|
|
352
|
+
data-field="${key}" value="${val}"
|
|
353
|
+
placeholder="${field.placeholder || ''}"
|
|
354
|
+
class="settings-input">
|
|
355
|
+
</div>`;
|
|
356
|
+
}).join('');
|
|
357
|
+
|
|
358
|
+
return `
|
|
359
|
+
${fields}
|
|
360
|
+
<div class="plugin-settings-actions">
|
|
361
|
+
<button class="btn-small plugin-save" data-id="${plugin.id}">save</button>
|
|
362
|
+
</div>`;
|
|
363
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoryblock web ui — setup wizard.
|
|
3
|
+
* Multi-step card-based onboarding flow.
|
|
4
|
+
* Only shown when no workspace is configured or on /setup route.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PROVIDERS = [
|
|
8
|
+
{ id: 'bedrock', name: 'AWS Bedrock', hint: 'Claude, Llama via AWS', fields: ['accessKeyId', 'secretAccessKey', 'region'] },
|
|
9
|
+
{ id: 'anthropic', name: 'Anthropic', hint: 'Claude API direct', fields: ['apiKey'] },
|
|
10
|
+
{ id: 'openai', name: 'OpenAI', hint: 'GPT-4, GPT-4o', fields: ['apiKey'] },
|
|
11
|
+
{ id: 'gemini', name: 'Google Gemini', hint: 'Gemini Pro, Flash', fields: ['apiKey'] },
|
|
12
|
+
{ id: 'ollama', name: 'Ollama (local)', hint: 'No API key required', fields: [] },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const CHANNELS = [
|
|
16
|
+
{ id: 'cli', name: 'Terminal (CLI)', hint: 'always enabled', locked: true },
|
|
17
|
+
{ id: 'telegram', name: 'Telegram', hint: 'bot token required', fields: ['botToken', 'chatId'] },
|
|
18
|
+
{ id: 'discord', name: 'Discord', hint: 'coming soon', disabled: true },
|
|
19
|
+
{ id: 'slack', name: 'Slack', hint: 'coming soon', disabled: true },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const PLUGINS = [
|
|
23
|
+
{ id: 'agents', name: 'Multi-Agent Orchestration', locked: true, installed: true },
|
|
24
|
+
{ id: 'web-search', name: 'Web Search', locked: false, installed: false },
|
|
25
|
+
{ id: 'fetch-webpage', name: 'Fetch Webpage', locked: false, installed: false },
|
|
26
|
+
{ id: 'aws', name: 'AWS Tools', locked: false, installed: false },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const STEPS = ['welcome', 'providers', 'channels', 'plugins', 'credentials', 'block', 'finish'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render the setup wizard
|
|
33
|
+
*/
|
|
34
|
+
export function renderSetup(container, { api, onComplete }) {
|
|
35
|
+
let currentStep = 0;
|
|
36
|
+
let selections = {
|
|
37
|
+
providers: ['bedrock'],
|
|
38
|
+
channels: ['cli'],
|
|
39
|
+
plugins: ['agents'],
|
|
40
|
+
credentials: {},
|
|
41
|
+
blockName: 'home',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function render() {
|
|
45
|
+
const step = STEPS[currentStep];
|
|
46
|
+
container.innerHTML = `
|
|
47
|
+
<div class="setup-overlay">
|
|
48
|
+
<div class="setup-container">
|
|
49
|
+
<div class="setup-progress">
|
|
50
|
+
${STEPS.map((s, i) => `<div class="setup-dot ${i === currentStep ? 'active' : i < currentStep ? 'done' : ''}"></div>`).join('')}
|
|
51
|
+
</div>
|
|
52
|
+
<div class="setup-card" id="setup-card">
|
|
53
|
+
${renderStep(step)}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
bindEvents(step);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderStep(step) {
|
|
62
|
+
switch (step) {
|
|
63
|
+
case 'welcome': return `
|
|
64
|
+
<div class="setup-welcome">
|
|
65
|
+
<div class="setup-logo">⬡</div>
|
|
66
|
+
<h1>memoryblock</h1>
|
|
67
|
+
<p class="setup-subtitle">Deploy isolated AI workspaces on your machine.</p>
|
|
68
|
+
<p class="setup-hint">Let's set things up. You can skip any step.</p>
|
|
69
|
+
<div class="setup-actions">
|
|
70
|
+
<button class="btn-primary" id="setup-next">Get Started</button>
|
|
71
|
+
<button class="btn-ghost" id="setup-skip-all">Skip to Dashboard</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>`;
|
|
74
|
+
|
|
75
|
+
case 'providers': return `
|
|
76
|
+
<h2>Choose Providers</h2>
|
|
77
|
+
<p class="setup-hint">Select the AI providers you want to use.</p>
|
|
78
|
+
<div class="setup-options">
|
|
79
|
+
${PROVIDERS.map(p => `
|
|
80
|
+
<label class="setup-option ${selections.providers.includes(p.id) ? 'selected' : ''}">
|
|
81
|
+
<input type="checkbox" value="${p.id}" ${selections.providers.includes(p.id) ? 'checked' : ''}>
|
|
82
|
+
<span class="option-name">${p.name}</span>
|
|
83
|
+
<span class="option-hint">${p.hint}</span>
|
|
84
|
+
</label>
|
|
85
|
+
`).join('')}
|
|
86
|
+
</div>
|
|
87
|
+
<div class="setup-actions">
|
|
88
|
+
<button class="btn-ghost" id="setup-back">Back</button>
|
|
89
|
+
<button class="btn-ghost" id="setup-skip">Skip</button>
|
|
90
|
+
<button class="btn-primary" id="setup-next">Next</button>
|
|
91
|
+
</div>`;
|
|
92
|
+
|
|
93
|
+
case 'channels': return `
|
|
94
|
+
<h2>Enable Channels</h2>
|
|
95
|
+
<p class="setup-hint">How do you want to talk to your blocks?</p>
|
|
96
|
+
<div class="setup-options">
|
|
97
|
+
${CHANNELS.map(ch => `
|
|
98
|
+
<label class="setup-option ${ch.locked ? 'locked' : ''} ${ch.disabled ? 'disabled' : ''} ${selections.channels.includes(ch.id) ? 'selected' : ''}">
|
|
99
|
+
<input type="checkbox" value="${ch.id}"
|
|
100
|
+
${selections.channels.includes(ch.id) ? 'checked' : ''}
|
|
101
|
+
${ch.locked || ch.disabled ? 'disabled' : ''}>
|
|
102
|
+
<span class="option-name">${ch.name}</span>
|
|
103
|
+
<span class="option-hint">${ch.hint}</span>
|
|
104
|
+
</label>
|
|
105
|
+
`).join('')}
|
|
106
|
+
</div>
|
|
107
|
+
<div class="setup-actions">
|
|
108
|
+
<button class="btn-ghost" id="setup-back">Back</button>
|
|
109
|
+
<button class="btn-ghost" id="setup-skip">Skip</button>
|
|
110
|
+
<button class="btn-primary" id="setup-next">Next</button>
|
|
111
|
+
</div>`;
|
|
112
|
+
|
|
113
|
+
case 'plugins': return `
|
|
114
|
+
<h2>Plugins</h2>
|
|
115
|
+
<p class="setup-hint">Extend your blocks with additional capabilities.</p>
|
|
116
|
+
<div class="setup-plugins-table">
|
|
117
|
+
<div class="plugin-header">
|
|
118
|
+
<span>Plugin</span>
|
|
119
|
+
<span>Status</span>
|
|
120
|
+
</div>
|
|
121
|
+
${PLUGINS.map(pl => `
|
|
122
|
+
<div class="plugin-row ${pl.locked ? 'locked' : ''}">
|
|
123
|
+
<span class="plugin-name">${pl.name}</span>
|
|
124
|
+
<label class="plugin-toggle">
|
|
125
|
+
<input type="checkbox" value="${pl.id}"
|
|
126
|
+
${pl.installed || selections.plugins.includes(pl.id) ? 'checked' : ''}
|
|
127
|
+
${pl.locked ? 'disabled' : ''}>
|
|
128
|
+
<span class="toggle-slider"></span>
|
|
129
|
+
</label>
|
|
130
|
+
</div>
|
|
131
|
+
`).join('')}
|
|
132
|
+
</div>
|
|
133
|
+
<div class="setup-actions">
|
|
134
|
+
<button class="btn-ghost" id="setup-back">Back</button>
|
|
135
|
+
<button class="btn-ghost" id="setup-skip">Skip</button>
|
|
136
|
+
<button class="btn-primary" id="setup-next">Next</button>
|
|
137
|
+
</div>`;
|
|
138
|
+
|
|
139
|
+
case 'credentials': return renderCredentialsStep();
|
|
140
|
+
|
|
141
|
+
case 'block': return `
|
|
142
|
+
<h2>Your First Block</h2>
|
|
143
|
+
<p class="setup-hint">A block is an isolated AI workspace with its own memory.</p>
|
|
144
|
+
<div class="setup-field">
|
|
145
|
+
<label>Block Name</label>
|
|
146
|
+
<input type="text" id="block-name" value="${selections.blockName}" placeholder="home"
|
|
147
|
+
pattern="[a-z0-9][a-z0-9\\-]{0,31}">
|
|
148
|
+
<span class="field-hint">lowercase, numbers, hyphens (max 32)</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="setup-actions">
|
|
151
|
+
<button class="btn-ghost" id="setup-back">Back</button>
|
|
152
|
+
<button class="btn-primary" id="setup-next">Create & Finish</button>
|
|
153
|
+
</div>`;
|
|
154
|
+
|
|
155
|
+
case 'finish': return `
|
|
156
|
+
<div class="setup-welcome">
|
|
157
|
+
<div class="setup-logo done">✓</div>
|
|
158
|
+
<h1>You're all set</h1>
|
|
159
|
+
<div class="setup-summary" id="setup-results"></div>
|
|
160
|
+
<div class="setup-actions">
|
|
161
|
+
<button class="btn-primary" id="setup-launch">Launch Dashboard</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderCredentialsStep() {
|
|
168
|
+
// Show fields only for selected providers and channels
|
|
169
|
+
const fields = [];
|
|
170
|
+
|
|
171
|
+
for (const pid of selections.providers) {
|
|
172
|
+
const provider = PROVIDERS.find(p => p.id === pid);
|
|
173
|
+
if (!provider || provider.fields.length === 0) continue;
|
|
174
|
+
fields.push(`<h3>${provider.name}</h3>`);
|
|
175
|
+
for (const f of provider.fields) {
|
|
176
|
+
const key = `${pid}.${f}`;
|
|
177
|
+
const val = selections.credentials[key] || '';
|
|
178
|
+
const isSecret = f !== 'region' && f !== 'chatId';
|
|
179
|
+
fields.push(`
|
|
180
|
+
<div class="setup-field">
|
|
181
|
+
<label>${f.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase())}</label>
|
|
182
|
+
<input type="${isSecret ? 'password' : 'text'}" data-key="${key}" value="${val}"
|
|
183
|
+
placeholder="${f === 'region' ? 'us-east-1' : ''}">
|
|
184
|
+
</div>
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const cid of selections.channels) {
|
|
190
|
+
const channel = CHANNELS.find(c => c.id === cid);
|
|
191
|
+
if (!channel?.fields) continue;
|
|
192
|
+
fields.push(`<h3>${channel.name}</h3>`);
|
|
193
|
+
for (const f of channel.fields) {
|
|
194
|
+
const key = `${cid}.${f}`;
|
|
195
|
+
const val = selections.credentials[key] || '';
|
|
196
|
+
const isSecret = f === 'botToken';
|
|
197
|
+
fields.push(`
|
|
198
|
+
<div class="setup-field">
|
|
199
|
+
<label>${f.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase())}</label>
|
|
200
|
+
<input type="${isSecret ? 'password' : 'text'}" data-key="${key}" value="${val}">
|
|
201
|
+
</div>
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (fields.length === 0) {
|
|
207
|
+
fields.push('<p class="setup-hint">No credentials needed for the selected services.</p>');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return `
|
|
211
|
+
<h2>Credentials</h2>
|
|
212
|
+
<p class="setup-hint">Enter API keys for the services you selected.</p>
|
|
213
|
+
<div class="setup-credentials">${fields.join('')}</div>
|
|
214
|
+
<div id="connection-status"></div>
|
|
215
|
+
<div class="setup-actions">
|
|
216
|
+
<button class="btn-ghost" id="setup-back">Back</button>
|
|
217
|
+
<button class="btn-ghost" id="setup-skip">Skip</button>
|
|
218
|
+
<button class="btn-primary" id="setup-next">Test & Continue</button>
|
|
219
|
+
</div>`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function bindEvents(step) {
|
|
223
|
+
const next = document.getElementById('setup-next');
|
|
224
|
+
const back = document.getElementById('setup-back');
|
|
225
|
+
const skip = document.getElementById('setup-skip');
|
|
226
|
+
const skipAll = document.getElementById('setup-skip-all');
|
|
227
|
+
const launch = document.getElementById('setup-launch');
|
|
228
|
+
|
|
229
|
+
if (next) next.addEventListener('click', () => handleNext(step));
|
|
230
|
+
if (back) back.addEventListener('click', () => { currentStep--; render(); });
|
|
231
|
+
if (skip) skip.addEventListener('click', () => { currentStep++; render(); });
|
|
232
|
+
if (skipAll) skipAll.addEventListener('click', () => onComplete());
|
|
233
|
+
if (launch) launch.addEventListener('click', () => onComplete());
|
|
234
|
+
|
|
235
|
+
// Checkbox bindings
|
|
236
|
+
if (step === 'providers' || step === 'channels') {
|
|
237
|
+
const key = step;
|
|
238
|
+
container.querySelectorAll('.setup-option input[type="checkbox"]').forEach(cb => {
|
|
239
|
+
cb.addEventListener('change', () => {
|
|
240
|
+
const checked = [...container.querySelectorAll('.setup-option input:checked')].map(c => c.value);
|
|
241
|
+
selections[key] = checked;
|
|
242
|
+
render();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (step === 'plugins') {
|
|
248
|
+
container.querySelectorAll('.plugin-row input[type="checkbox"]').forEach(cb => {
|
|
249
|
+
cb.addEventListener('change', () => {
|
|
250
|
+
const checked = [...container.querySelectorAll('.plugin-row input:checked')].map(c => c.value);
|
|
251
|
+
selections.plugins = checked;
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (step === 'credentials') {
|
|
257
|
+
container.querySelectorAll('.setup-credentials input').forEach(inp => {
|
|
258
|
+
inp.addEventListener('input', () => {
|
|
259
|
+
selections.credentials[inp.dataset.key] = inp.value;
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Show connection test results on finish step
|
|
265
|
+
if (step === 'finish') {
|
|
266
|
+
const el = document.getElementById('setup-results');
|
|
267
|
+
if (el) {
|
|
268
|
+
const lines = [];
|
|
269
|
+
if (selections.providers.length) lines.push(`<p>Providers: ${selections.providers.join(', ')}</p>`);
|
|
270
|
+
if (selections.channels.length) lines.push(`<p>Channels: ${selections.channels.join(', ')}</p>`);
|
|
271
|
+
lines.push(`<p>Block: <strong>${selections.blockName}</strong></p>`);
|
|
272
|
+
el.innerHTML = lines.join('');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function handleNext(step) {
|
|
278
|
+
if (step === 'block') {
|
|
279
|
+
const input = document.getElementById('block-name');
|
|
280
|
+
if (input) selections.blockName = input.value || 'home';
|
|
281
|
+
|
|
282
|
+
// Save configuration via API
|
|
283
|
+
try {
|
|
284
|
+
await api('/api/setup', {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { 'Content-Type': 'application/json' },
|
|
287
|
+
body: JSON.stringify(selections),
|
|
288
|
+
});
|
|
289
|
+
} catch { }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
currentStep++;
|
|
293
|
+
if (currentStep >= STEPS.length) {
|
|
294
|
+
onComplete();
|
|
295
|
+
} else {
|
|
296
|
+
render();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
render();
|
|
301
|
+
}
|