@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.
@@ -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, '&lt;')
14
+ .replace(/>/g, '&gt;');
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
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'
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
+ }