@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,394 @@
|
|
|
1
|
+
import { api, connectWs, getToken } from '../app.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight markdown-to-HTML renderer.
|
|
5
|
+
* Handles: bold, italic, code blocks, inline code, headers, lists, links, line breaks.
|
|
6
|
+
*/
|
|
7
|
+
function renderMarkdown(text) {
|
|
8
|
+
if (!text) return '';
|
|
9
|
+
|
|
10
|
+
// Escape HTML first
|
|
11
|
+
let html = text
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>');
|
|
15
|
+
|
|
16
|
+
// Fenced code blocks: ```lang\ncode\n```
|
|
17
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) => {
|
|
18
|
+
return `<pre class="code-block"><code class="lang-${lang || 'text'}">${code.trim()}</code></pre>`;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Inline code: `code`
|
|
22
|
+
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
|
23
|
+
|
|
24
|
+
// Headers: ### heading
|
|
25
|
+
html = html.replace(/^### (.+)$/gm, '<strong class="md-h3">$1</strong>');
|
|
26
|
+
html = html.replace(/^## (.+)$/gm, '<strong class="md-h2">$1</strong>');
|
|
27
|
+
html = html.replace(/^# (.+)$/gm, '<strong class="md-h1">$1</strong>');
|
|
28
|
+
|
|
29
|
+
// Bold: **text**
|
|
30
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
31
|
+
|
|
32
|
+
// Italic: _text_ or *text*
|
|
33
|
+
html = html.replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>');
|
|
34
|
+
html = html.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
|
|
35
|
+
|
|
36
|
+
// Links: [text](url)
|
|
37
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
38
|
+
|
|
39
|
+
// Unordered lists: - item or * item (at start of line)
|
|
40
|
+
html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
|
|
41
|
+
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
|
42
|
+
|
|
43
|
+
// Ordered lists: 1. item
|
|
44
|
+
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
45
|
+
|
|
46
|
+
// Horizontal rules
|
|
47
|
+
html = html.replace(/^(?:---|\*\*\*|___)$/gm, '<hr>');
|
|
48
|
+
|
|
49
|
+
// Line breaks
|
|
50
|
+
html = html.replace(/\n\n/g, '</p><p>');
|
|
51
|
+
html = html.replace(/\n/g, '<br>');
|
|
52
|
+
|
|
53
|
+
html = `<p>${html}</p>`;
|
|
54
|
+
html = html.replace(/<p><\/p>/g, '');
|
|
55
|
+
|
|
56
|
+
return html;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let chatWs = null;
|
|
60
|
+
let currentSession = 'web';
|
|
61
|
+
|
|
62
|
+
export async function renderChatView(container, blockName) {
|
|
63
|
+
let renderedMessageCount = 0;
|
|
64
|
+
// Fetch block details to get monitor name + emoji
|
|
65
|
+
let monitorLabel = blockName;
|
|
66
|
+
let monitorEmoji = '';
|
|
67
|
+
try {
|
|
68
|
+
const data = await api(`/api/blocks/${blockName}`, { method: 'GET' });
|
|
69
|
+
if (data.config) {
|
|
70
|
+
monitorLabel = data.config.monitorName || blockName;
|
|
71
|
+
monitorEmoji = data.config.monitorEmoji || '';
|
|
72
|
+
}
|
|
73
|
+
} catch { /* fallback to block name */ }
|
|
74
|
+
|
|
75
|
+
const displayName = monitorEmoji ? `${monitorEmoji} ${monitorLabel}` : monitorLabel;
|
|
76
|
+
|
|
77
|
+
// Create a full-screen overlay that sits on top of everything
|
|
78
|
+
const overlay = document.createElement('div');
|
|
79
|
+
overlay.id = 'chat-overlay';
|
|
80
|
+
overlay.className = 'chat-overlay';
|
|
81
|
+
overlay.innerHTML = `
|
|
82
|
+
<div class="chat-overlay-inner">
|
|
83
|
+
<div class="chat-container">
|
|
84
|
+
<div class="chat-sidebar" id="chat-sidebar">
|
|
85
|
+
<div class="sidebar-header">
|
|
86
|
+
<h3>Sessions</h3>
|
|
87
|
+
<button class="sidebar-toggle" id="sidebar-toggle" title="Collapse sidebar">
|
|
88
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="session-list" id="chat-sessions">
|
|
92
|
+
<div class="session-tab active" data-session="web">
|
|
93
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
94
|
+
Web Channel
|
|
95
|
+
</div>
|
|
96
|
+
<div class="session-tab" data-session="cli">
|
|
97
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
|
98
|
+
CLI Channel
|
|
99
|
+
</div>
|
|
100
|
+
<div class="session-tab" data-session="telegram">
|
|
101
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/></svg>
|
|
102
|
+
Telegram
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<button class="sidebar-expand-btn hidden" id="sidebar-expand-btn" title="Show sidebar">
|
|
108
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
<div class="chat-main">
|
|
112
|
+
<div class="chat-header">
|
|
113
|
+
<div class="chat-header-left">
|
|
114
|
+
<button class="chat-close-btn" id="chat-close-btn" title="Close chat">
|
|
115
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
116
|
+
</button>
|
|
117
|
+
<h2>${escapeHtml(displayName)}</h2>
|
|
118
|
+
</div>
|
|
119
|
+
<span class="status-indicator" id="chat-status">
|
|
120
|
+
<span class="status-dot sleeping"></span>
|
|
121
|
+
<span id="chat-status-text">Checking...</span>
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="chat-messages" id="chat-messages">
|
|
126
|
+
<div class="chat-msg system">
|
|
127
|
+
<div class="msg-content">Joined Web Channel for ${escapeHtml(displayName)}.</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="chat-input-area">
|
|
132
|
+
<textarea id="chat-input" placeholder="Type a message... (Enter to send, Shift+Enter for new line)" rows="1"></textarea>
|
|
133
|
+
<button id="chat-send-btn" class="action-btn primary"><span class="action-icon">↑</span></button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
document.body.appendChild(overlay);
|
|
141
|
+
// Trigger transition
|
|
142
|
+
requestAnimationFrame(() => overlay.classList.add('visible'));
|
|
143
|
+
|
|
144
|
+
// Close button — remove overlay and go back
|
|
145
|
+
function closeChat() {
|
|
146
|
+
if (chatWs) { chatWs.close(); chatWs = null; }
|
|
147
|
+
overlay.classList.remove('visible');
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
overlay.remove();
|
|
150
|
+
}, 300);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
document.getElementById('chat-close-btn').addEventListener('click', closeChat);
|
|
154
|
+
|
|
155
|
+
// ESC key to close
|
|
156
|
+
const escHandler = (e) => { if (e.key === 'Escape') closeChat(); };
|
|
157
|
+
document.addEventListener('keydown', escHandler);
|
|
158
|
+
|
|
159
|
+
// Sidebar toggle
|
|
160
|
+
const sidebar = document.getElementById('chat-sidebar');
|
|
161
|
+
const expandBtn = document.getElementById('sidebar-expand-btn');
|
|
162
|
+
document.getElementById('sidebar-toggle').addEventListener('click', () => {
|
|
163
|
+
sidebar.classList.add('collapsed');
|
|
164
|
+
expandBtn.classList.remove('hidden');
|
|
165
|
+
});
|
|
166
|
+
expandBtn.addEventListener('click', () => {
|
|
167
|
+
sidebar.classList.remove('collapsed');
|
|
168
|
+
expandBtn.classList.add('hidden');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Session tab switching
|
|
172
|
+
const messagesDiv = document.getElementById('chat-messages');
|
|
173
|
+
|
|
174
|
+
document.querySelectorAll('.session-tab').forEach(tab => {
|
|
175
|
+
tab.addEventListener('click', () => {
|
|
176
|
+
const session = tab.dataset.session;
|
|
177
|
+
if (session === currentSession) return;
|
|
178
|
+
|
|
179
|
+
currentSession = session;
|
|
180
|
+
document.querySelectorAll('.session-tab').forEach(t => t.classList.remove('active'));
|
|
181
|
+
tab.classList.add('active');
|
|
182
|
+
|
|
183
|
+
const channelLabels = { web: 'Web Channel', cli: 'CLI Channel', telegram: 'Telegram' };
|
|
184
|
+
messagesDiv.innerHTML = '';
|
|
185
|
+
renderedMessageCount = 0;
|
|
186
|
+
removeTypingIndicator();
|
|
187
|
+
appendMessage('system', `Viewing ${channelLabels[session] || session} logs for ${escapeHtml(displayName)}.`);
|
|
188
|
+
loadChatHistory(session, true);
|
|
189
|
+
|
|
190
|
+
// Disable input if not web channel
|
|
191
|
+
const inputArea = document.querySelector('.chat-input-area');
|
|
192
|
+
if (session !== 'web') {
|
|
193
|
+
inputArea.classList.add('disabled-input');
|
|
194
|
+
chatInput.disabled = true;
|
|
195
|
+
sendBtn.disabled = true;
|
|
196
|
+
chatInput.placeholder = 'Viewing mode (switch to Web Channel to chat)';
|
|
197
|
+
} else {
|
|
198
|
+
inputArea.classList.remove('disabled-input');
|
|
199
|
+
chatInput.disabled = false;
|
|
200
|
+
sendBtn.disabled = false;
|
|
201
|
+
chatInput.placeholder = 'Type a message... (Enter to send, Shift+Enter for new line)';
|
|
202
|
+
chatInput.focus();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const chatInput = document.getElementById('chat-input');
|
|
208
|
+
const sendBtn = document.getElementById('chat-send-btn');
|
|
209
|
+
|
|
210
|
+
function removeTypingIndicator() {
|
|
211
|
+
const ind = document.getElementById('typing-indicator');
|
|
212
|
+
if (ind) ind.remove();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function showTypingIndicator() {
|
|
216
|
+
if (document.getElementById('typing-indicator')) return;
|
|
217
|
+
const msgDiv = document.createElement('div');
|
|
218
|
+
msgDiv.id = 'typing-indicator';
|
|
219
|
+
msgDiv.className = `chat-msg assistant typing-indicator`;
|
|
220
|
+
msgDiv.innerHTML = `<div class="msg-content"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>`;
|
|
221
|
+
messagesDiv.appendChild(msgDiv);
|
|
222
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function appendMessage(role, content) {
|
|
226
|
+
removeTypingIndicator();
|
|
227
|
+
const msgDiv = document.createElement('div');
|
|
228
|
+
msgDiv.className = `chat-msg ${role}`;
|
|
229
|
+
|
|
230
|
+
if (role === 'assistant' || role === 'system') {
|
|
231
|
+
msgDiv.innerHTML = `<div class="msg-content">${renderMarkdown(content)}</div>`;
|
|
232
|
+
} else if (role === 'error') {
|
|
233
|
+
msgDiv.innerHTML = `<div class="msg-content">${escapeHtml(content)}</div>`;
|
|
234
|
+
} else {
|
|
235
|
+
msgDiv.innerHTML = `<div class="msg-content">${escapeHtml(content).replace(/\n/g, '<br>')}</div>`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
messagesDiv.appendChild(msgDiv);
|
|
239
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let currentStreamDiv = null;
|
|
243
|
+
let currentStreamText = '';
|
|
244
|
+
|
|
245
|
+
function handleStreamChunk(chunk) {
|
|
246
|
+
if (!currentStreamDiv) {
|
|
247
|
+
removeTypingIndicator();
|
|
248
|
+
const msgDiv = document.createElement('div');
|
|
249
|
+
msgDiv.className = `chat-msg assistant`;
|
|
250
|
+
currentStreamDiv = document.createElement('div');
|
|
251
|
+
currentStreamDiv.className = 'msg-content';
|
|
252
|
+
msgDiv.appendChild(currentStreamDiv);
|
|
253
|
+
messagesDiv.appendChild(msgDiv);
|
|
254
|
+
currentStreamText = '';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
currentStreamText += chunk;
|
|
258
|
+
currentStreamDiv.innerHTML = renderMarkdown(currentStreamText);
|
|
259
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resetStreamDiv() {
|
|
263
|
+
if (currentStreamDiv) {
|
|
264
|
+
currentStreamDiv.parentElement.remove();
|
|
265
|
+
currentStreamDiv = null;
|
|
266
|
+
currentStreamText = '';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function loadChatHistory(channel, initialLoad = false) {
|
|
271
|
+
try {
|
|
272
|
+
const data = await api(`/api/blocks/${blockName}/chat?channel=${channel || 'web'}`, { method: 'GET' });
|
|
273
|
+
if (data.messages && data.messages.length > renderedMessageCount) {
|
|
274
|
+
const newMessages = data.messages.slice(renderedMessageCount);
|
|
275
|
+
renderedMessageCount = data.messages.length;
|
|
276
|
+
|
|
277
|
+
resetStreamDiv();
|
|
278
|
+
|
|
279
|
+
for (const m of newMessages) {
|
|
280
|
+
appendMessage(m.role, m.content);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// failed to load history
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function updateConnectionStatus() {
|
|
289
|
+
try {
|
|
290
|
+
const data = await api(`/api/blocks/${blockName}`, { method: 'GET' });
|
|
291
|
+
const statusDot = document.querySelector('#chat-status .status-dot');
|
|
292
|
+
const statusText = document.getElementById('chat-status-text');
|
|
293
|
+
if (!statusDot || !statusText) return;
|
|
294
|
+
|
|
295
|
+
const pulseStatus = data.pulse?.status || 'SLEEPING';
|
|
296
|
+
|
|
297
|
+
if (pulseStatus === 'ACTIVE') {
|
|
298
|
+
statusDot.className = 'status-dot active';
|
|
299
|
+
statusText.textContent = 'Connected';
|
|
300
|
+
} else if (pulseStatus === 'ERROR') {
|
|
301
|
+
statusDot.className = 'status-dot error';
|
|
302
|
+
statusText.textContent = 'Error';
|
|
303
|
+
} else {
|
|
304
|
+
statusDot.className = 'status-dot sleeping';
|
|
305
|
+
statusText.textContent = 'Sleeping';
|
|
306
|
+
}
|
|
307
|
+
} catch { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function sendMessage() {
|
|
311
|
+
const text = chatInput.value.trim();
|
|
312
|
+
if (!text) return;
|
|
313
|
+
|
|
314
|
+
chatInput.value = '';
|
|
315
|
+
chatInput.style.height = 'auto';
|
|
316
|
+
appendMessage('user', text);
|
|
317
|
+
renderedMessageCount++;
|
|
318
|
+
|
|
319
|
+
showTypingIndicator();
|
|
320
|
+
|
|
321
|
+
// Wake the block if asleep
|
|
322
|
+
try {
|
|
323
|
+
await api(`/api/blocks/${blockName}/start`, { method: 'POST' });
|
|
324
|
+
} catch { /* already active */ }
|
|
325
|
+
|
|
326
|
+
// Show connecting status
|
|
327
|
+
const statusDot = document.querySelector('#chat-status .status-dot');
|
|
328
|
+
const statusText = document.getElementById('chat-status-text');
|
|
329
|
+
if (statusDot && statusText) {
|
|
330
|
+
statusDot.className = 'status-dot connecting';
|
|
331
|
+
statusText.textContent = 'Connecting...';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const res = await api(`/api/blocks/${blockName}/chat`, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
body: JSON.stringify({ message: text })
|
|
338
|
+
});
|
|
339
|
+
if (res.error) {
|
|
340
|
+
removeTypingIndicator();
|
|
341
|
+
appendMessage('error', res.error);
|
|
342
|
+
renderedMessageCount--;
|
|
343
|
+
} else if (res.note) {
|
|
344
|
+
appendMessage('system', res.note);
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
removeTypingIndicator();
|
|
348
|
+
appendMessage('error', 'Error: ' + err.message);
|
|
349
|
+
renderedMessageCount--;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
chatInput.addEventListener('keydown', (e) => {
|
|
354
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
sendMessage();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Auto-resize textarea
|
|
361
|
+
chatInput.addEventListener('input', () => {
|
|
362
|
+
chatInput.style.height = 'auto';
|
|
363
|
+
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
367
|
+
|
|
368
|
+
if (chatWs) chatWs.close();
|
|
369
|
+
chatWs = connectWs(blockName, () => {
|
|
370
|
+
loadChatHistory(currentSession, false); // initialLoad = false for ws refresh
|
|
371
|
+
updateConnectionStatus();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
chatWs.addEventListener('message', (e) => {
|
|
375
|
+
try {
|
|
376
|
+
const msg = JSON.parse(e.data);
|
|
377
|
+
if (msg.type === 'stream') {
|
|
378
|
+
handleStreamChunk(msg.chunk);
|
|
379
|
+
}
|
|
380
|
+
} catch {}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
currentSession = 'web';
|
|
384
|
+
loadChatHistory('web', true);
|
|
385
|
+
updateConnectionStatus();
|
|
386
|
+
setTimeout(() => chatInput.focus(), 300);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function escapeHtml(str) {
|
|
390
|
+
if (!str) return '';
|
|
391
|
+
return str.replace(/[&<>'"]/g, tag => ({
|
|
392
|
+
'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'
|
|
393
|
+
}[tag] || tag));
|
|
394
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// create-block.js — Block creation form component
|
|
2
|
+
|
|
3
|
+
export function renderCreateBlock(container, { apiBase, token, onCreated }) {
|
|
4
|
+
container.innerHTML = `
|
|
5
|
+
<div class="create-block-view">
|
|
6
|
+
<div class="view-header" style="margin-bottom: 24px;">
|
|
7
|
+
<button class="back-btn" id="create-back">
|
|
8
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:2px; vertical-align:text-bottom;"><path d="m15 18-6-6 6-6"/></svg> back to blocks
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="create-card" style="background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 48px; text-align: center; max-width: 540px; margin: 32px auto 0; box-shadow: 0 10px 40px rgba(0,0,0,0.15);">
|
|
13
|
+
<div class="create-icon" style="background: var(--accent-glow); color: var(--accent); width: 72px; height: 72px; border-radius: 20px; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 24px;">
|
|
14
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
|
|
15
|
+
</div>
|
|
16
|
+
<h2 style="font-size: 1.8rem; margin-bottom: 12px; font-weight: 600; font-family: 'Outfit', sans-serif;">Deploy a New Block</h2>
|
|
17
|
+
<p style="color: var(--text-dim); margin-bottom: 40px; font-size: 1.05rem; line-height: 1.5;">Create an isolated intelligence wrapper primitive. Highly optimized background daemons for massive scaled logic flow tracking.</p>
|
|
18
|
+
|
|
19
|
+
<form id="create-block-form" class="create-form" style="text-align: left;">
|
|
20
|
+
<div class="form-group">
|
|
21
|
+
<label for="block-name" style="font-weight: 500; display: block; margin-bottom: 12px; color: var(--text);">Block Identifier <span style="color:var(--text-dim);font-weight:400;font-size:0.85rem;margin-left:8px;">(lowercase, hyphens)</span></label>
|
|
22
|
+
<input type="text" id="block-name" class="form-input" style="width: 100%; padding: 16px 18px; font-size: 1.15rem; border-radius: 12px; background: var(--bg); border: 1px solid var(--border); transition: border-color 0.2s;"
|
|
23
|
+
placeholder="e.g. data-analyzer"
|
|
24
|
+
pattern="[a-z0-9][a-z0-9\\-]{0,31}"
|
|
25
|
+
required
|
|
26
|
+
autocomplete="off" />
|
|
27
|
+
</div>
|
|
28
|
+
<div id="create-error" class="form-error" style="display:none; color: var(--error); background: rgba(255,59,48,0.1); padding: 14px; border-radius: 8px; margin-top: 16px; font-size: 0.95rem; border: 1px solid rgba(255,59,48,0.2);"></div>
|
|
29
|
+
|
|
30
|
+
<button type="submit" class="action-btn primary" id="create-submit" style="width: 100%; padding: 18px; margin-top: 32px; font-size: 1.1rem; justify-content: center; border-radius: 12px; font-weight: 600;">
|
|
31
|
+
Initialize Primitive
|
|
32
|
+
</button>
|
|
33
|
+
</form>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
// Back button
|
|
39
|
+
document.getElementById('create-back').addEventListener('click', () => {
|
|
40
|
+
window.location.hash = '#/blocks';
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Name validation in real-time
|
|
44
|
+
const nameInput = document.getElementById('block-name');
|
|
45
|
+
nameInput.addEventListener('input', () => {
|
|
46
|
+
const val = nameInput.value;
|
|
47
|
+
const valid = /^[a-z0-9][a-z0-9\-]{0,31}$/.test(val);
|
|
48
|
+
nameInput.classList.toggle('invalid', val.length > 0 && !valid);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Submit
|
|
52
|
+
document.getElementById('create-block-form').addEventListener('submit', async (e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
const name = nameInput.value.trim();
|
|
55
|
+
const errorEl = document.getElementById('create-error');
|
|
56
|
+
const submitBtn = document.getElementById('create-submit');
|
|
57
|
+
|
|
58
|
+
if (!/^[a-z0-9][a-z0-9\-]{0,31}$/.test(name)) {
|
|
59
|
+
errorEl.textContent = 'Invalid name. Use lowercase letters, numbers, and hyphens only.';
|
|
60
|
+
errorEl.style.display = 'block';
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
submitBtn.disabled = true;
|
|
65
|
+
submitBtn.textContent = 'Creating...';
|
|
66
|
+
errorEl.style.display = 'none';
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`${apiBase}/api/blocks`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Authorization': `Bearer ${token}`,
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({ name }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
if (!res.ok) throw new Error(data.error || 'Failed to create block');
|
|
80
|
+
|
|
81
|
+
showToast(`Block "${name}" created successfully.`);
|
|
82
|
+
if (onCreated) onCreated(name);
|
|
83
|
+
window.location.hash = '#/blocks';
|
|
84
|
+
} catch (err) {
|
|
85
|
+
errorEl.textContent = err.message;
|
|
86
|
+
errorEl.style.display = 'block';
|
|
87
|
+
submitBtn.disabled = false;
|
|
88
|
+
submitBtn.textContent = 'Create Block';
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Focus
|
|
93
|
+
nameInput.focus();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Toast notification helper
|
|
97
|
+
export function showToast(message, duration = 3000) {
|
|
98
|
+
let toast = document.getElementById('toast-notification');
|
|
99
|
+
if (!toast) {
|
|
100
|
+
toast = document.createElement('div');
|
|
101
|
+
toast.id = 'toast-notification';
|
|
102
|
+
toast.className = 'toast';
|
|
103
|
+
document.body.appendChild(toast);
|
|
104
|
+
}
|
|
105
|
+
toast.textContent = message;
|
|
106
|
+
toast.classList.add('visible');
|
|
107
|
+
setTimeout(() => toast.classList.remove('visible'), duration);
|
|
108
|
+
}
|