@webmcp-auto-ui/agent 2.5.25 → 2.5.27
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/package.json +1 -1
- package/src/autoui-server.ts +44 -0
- package/src/diagnostics.ts +6 -6
- package/src/discovery-cache.ts +17 -3
- package/src/index.ts +18 -4
- package/src/loop.ts +31 -34
- package/src/notebook-widgets/compact.ts +312 -0
- package/src/notebook-widgets/document.ts +372 -0
- package/src/notebook-widgets/editorial.ts +348 -0
- package/src/notebook-widgets/recipes/compact.md +104 -0
- package/src/notebook-widgets/recipes/document.md +100 -0
- package/src/notebook-widgets/recipes/editorial.md +104 -0
- package/src/notebook-widgets/recipes/workspace.md +94 -0
- package/src/notebook-widgets/shared.ts +1064 -0
- package/src/notebook-widgets/workspace.ts +328 -0
- package/src/prompts/claude-prompt-builder.ts +81 -0
- package/src/prompts/gemma4-prompt-builder.ts +205 -0
- package/src/prompts/index.ts +55 -0
- package/src/prompts/mistral-prompt-builder.ts +90 -0
- package/src/prompts/qwen-prompt-builder.ts +90 -0
- package/src/prompts/tool-call-parsers.ts +322 -0
- package/src/prompts/tool-refs.ts +196 -0
- package/src/providers/factory.ts +20 -3
- package/src/providers/transformers-models.ts +143 -0
- package/src/providers/transformers-serialize.ts +81 -0
- package/src/providers/transformers.ts +329 -0
- package/src/providers/transformers.worker.ts +667 -0
- package/src/providers/wasm.ts +150 -510
- package/src/recipes/_generated.ts +515 -0
- package/src/recipes/canary-data.md +50 -0
- package/src/recipes/canary-display.md +99 -0
- package/src/recipes/canary-middle.md +32 -0
- package/src/recipes/hackathon-assemblee-nationale.md +111 -0
- package/src/recipes/hummingbird-data.md +32 -0
- package/src/recipes/hummingbird-display.md +36 -0
- package/src/recipes/hummingbird-middle.md +18 -0
- package/src/recipes/notebook-playbook.md +129 -0
- package/src/tool-layers.ts +33 -157
- package/src/trace-observer.ts +669 -0
- package/src/types.ts +20 -5
- package/src/util/opfs-cache.ts +265 -0
- package/tests/gemma-prompt.test.ts +472 -0
- package/tests/loop.test.ts +5 -5
- package/tests/transformers-serialize.test.ts +103 -0
- package/src/providers/gemma.worker.legacy.ts +0 -123
- package/src/providers/litert.worker.ts +0 -294
- package/src/recipes/widgets/actions.md +0 -28
- package/src/recipes/widgets/alert.md +0 -27
- package/src/recipes/widgets/cards.md +0 -41
- package/src/recipes/widgets/carousel.md +0 -39
- package/src/recipes/widgets/chart-rich.md +0 -51
- package/src/recipes/widgets/chart.md +0 -32
- package/src/recipes/widgets/code.md +0 -21
- package/src/recipes/widgets/d3.md +0 -36
- package/src/recipes/widgets/data-table.md +0 -46
- package/src/recipes/widgets/gallery.md +0 -39
- package/src/recipes/widgets/grid-data.md +0 -57
- package/src/recipes/widgets/hemicycle.md +0 -43
- package/src/recipes/widgets/js-sandbox.md +0 -32
- package/src/recipes/widgets/json-viewer.md +0 -27
- package/src/recipes/widgets/kv.md +0 -31
- package/src/recipes/widgets/list.md +0 -24
- package/src/recipes/widgets/log.md +0 -39
- package/src/recipes/widgets/map.md +0 -49
- package/src/recipes/widgets/profile.md +0 -49
- package/src/recipes/widgets/recipe-browser.md +0 -102
- package/src/recipes/widgets/sankey.md +0 -54
- package/src/recipes/widgets/stat-card.md +0 -43
- package/src/recipes/widgets/stat.md +0 -35
- package/src/recipes/widgets/tags.md +0 -30
- package/src/recipes/widgets/text.md +0 -19
- package/src/recipes/widgets/timeline.md +0 -38
- package/src/recipes/widgets/trombinoscope.md +0 -39
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// notebook-compact — reactive minimalist layout (marimo-like)
|
|
4
|
+
// Left gutter with type label + vertical line, named outputs, fresh/stale status.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createState, injectStyles, mountRunControls, mountHistoryPanel,
|
|
9
|
+
setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
|
|
10
|
+
logHistory, autosize, openShareModal, registerHistoryObserver,
|
|
11
|
+
buildServersButton,
|
|
12
|
+
type NotebookState, type NotebookCell,
|
|
13
|
+
} from './shared.js';
|
|
14
|
+
|
|
15
|
+
export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
|
|
16
|
+
injectStyles();
|
|
17
|
+
injectLayoutStyles();
|
|
18
|
+
|
|
19
|
+
const state: NotebookState = createState({
|
|
20
|
+
id: data.id as string,
|
|
21
|
+
title: data.title as string,
|
|
22
|
+
mode: (data.mode as any) ?? 'edit',
|
|
23
|
+
cells: data.cells as any,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
container.classList.add('nb-root');
|
|
27
|
+
container.classList.toggle('nb-view-mode', state.mode === 'view');
|
|
28
|
+
|
|
29
|
+
container.innerHTML = `
|
|
30
|
+
<div class="nbc-shell">
|
|
31
|
+
<div class="nbc-toolbar">
|
|
32
|
+
<div class="nbc-status">
|
|
33
|
+
<span class="nbc-status-dot"></span>
|
|
34
|
+
<span class="nbc-status-text">reactive · 0 cells</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="nbc-actions">
|
|
37
|
+
<div class="nb-mode-switch">
|
|
38
|
+
<button class="nb-mode-edit nb-on">edit</button>
|
|
39
|
+
<button class="nb-mode-view">view</button>
|
|
40
|
+
</div>
|
|
41
|
+
<button class="nb-btn nb-add-cell" data-add="md">+ md</button>
|
|
42
|
+
<button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
|
|
43
|
+
<button class="nb-btn nb-add-cell" data-add="js">+ js</button>
|
|
44
|
+
<button class="nb-btn nbc-history-btn">⟲ history</button>
|
|
45
|
+
<span class="nbc-servers-slot"></span>
|
|
46
|
+
<button class="nb-btn nbc-share-btn">share</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="nb-history-panel nbc-history-panel"></div>
|
|
50
|
+
<div class="nbc-cells"></div>
|
|
51
|
+
</div>`;
|
|
52
|
+
|
|
53
|
+
const shell = container.querySelector('.nbc-shell') as HTMLElement;
|
|
54
|
+
const cellsEl = shell.querySelector('.nbc-cells') as HTMLElement;
|
|
55
|
+
const historyPanel = shell.querySelector('.nbc-history-panel') as HTMLElement;
|
|
56
|
+
|
|
57
|
+
function renderCells() {
|
|
58
|
+
cellsEl.innerHTML = '';
|
|
59
|
+
state.cells.forEach((cell) => cellsEl.appendChild(renderCell(cell, state, rerender)));
|
|
60
|
+
updateStatus();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function updateStatus() {
|
|
64
|
+
const n = state.cells.length;
|
|
65
|
+
const stale = state.cells.filter((c) => c.status === 'stale').length;
|
|
66
|
+
(shell.querySelector('.nbc-status-text') as HTMLElement).textContent =
|
|
67
|
+
stale > 0 ? `reactive · ${n} cells · ${stale} stale` : `reactive · ${n} cells · synced`;
|
|
68
|
+
(shell.querySelector('.nbc-status-dot') as HTMLElement).classList.toggle('nbc-stale', stale > 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rerender() {
|
|
72
|
+
mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
|
|
73
|
+
renderCells();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Toolbar bindings
|
|
77
|
+
shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
|
|
78
|
+
btn.addEventListener('click', () => {
|
|
79
|
+
const type = btn.dataset.add as any;
|
|
80
|
+
addCell(state, type, { varname: type === 'sql' ? 'rows_' + (state.cells.length + 1) : undefined });
|
|
81
|
+
rerender();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
(shell.querySelector('.nbc-history-btn') as HTMLElement).addEventListener('click', () => {
|
|
85
|
+
historyPanel.classList.toggle('nb-open');
|
|
86
|
+
});
|
|
87
|
+
(shell.querySelector('.nbc-share-btn') as HTMLElement).addEventListener('click', () => {
|
|
88
|
+
openShareModal(state, (fmt) => console.log('[notebook-compact] share as', fmt, state));
|
|
89
|
+
});
|
|
90
|
+
const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
|
|
91
|
+
const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
|
|
92
|
+
editBtn.addEventListener('click', () => {
|
|
93
|
+
state.mode = 'edit';
|
|
94
|
+
container.classList.remove('nb-view-mode');
|
|
95
|
+
editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
|
|
96
|
+
});
|
|
97
|
+
viewBtn.addEventListener('click', () => {
|
|
98
|
+
state.mode = 'view';
|
|
99
|
+
container.classList.add('nb-view-mode');
|
|
100
|
+
viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
buildServersButton(state, shell.querySelector('.nbc-servers-slot') as HTMLElement, data, rerender);
|
|
104
|
+
|
|
105
|
+
setupDnD(cellsEl, state, rerender);
|
|
106
|
+
const unsubHistory = registerHistoryObserver(() => mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); }));
|
|
107
|
+
|
|
108
|
+
rerender();
|
|
109
|
+
|
|
110
|
+
return () => { unsubHistory(); };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Cell rendering (compact layout)
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
function renderCell(cell: NotebookCell, state: NotebookState, rerender: () => void): HTMLElement {
|
|
118
|
+
const wrap = document.createElement('div');
|
|
119
|
+
wrap.className = 'nb-cell-wrapper nbc-cell';
|
|
120
|
+
wrap.dataset.id = cell.id;
|
|
121
|
+
|
|
122
|
+
const row = document.createElement('div');
|
|
123
|
+
row.className = 'nbc-row';
|
|
124
|
+
|
|
125
|
+
const handle = document.createElement('span');
|
|
126
|
+
handle.className = 'nb-drag-handle';
|
|
127
|
+
handle.draggable = true;
|
|
128
|
+
handle.textContent = '⋮⋮';
|
|
129
|
+
row.appendChild(handle);
|
|
130
|
+
|
|
131
|
+
const gutter = document.createElement('div');
|
|
132
|
+
gutter.className = `nbc-gutter nbc-gutter-${cell.type}`;
|
|
133
|
+
gutter.innerHTML = `<span class="nbc-type-label">${cell.type}</span><span class="nbc-line"></span>`;
|
|
134
|
+
row.appendChild(gutter);
|
|
135
|
+
|
|
136
|
+
const body = document.createElement('div');
|
|
137
|
+
body.className = 'nbc-body';
|
|
138
|
+
body.style.minWidth = '0';
|
|
139
|
+
|
|
140
|
+
if (cell.type === 'md') {
|
|
141
|
+
const mdBody = document.createElement('div');
|
|
142
|
+
mdBody.className = 'nbc-md-body';
|
|
143
|
+
const ta = document.createElement('textarea');
|
|
144
|
+
ta.className = 'nb-md-edit';
|
|
145
|
+
ta.value = cell.content;
|
|
146
|
+
ta.rows = 2;
|
|
147
|
+
ta.placeholder = 'write markdown…';
|
|
148
|
+
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); });
|
|
149
|
+
mdBody.appendChild(ta);
|
|
150
|
+
body.appendChild(mdBody);
|
|
151
|
+
setTimeout(() => autosize(ta), 0);
|
|
152
|
+
|
|
153
|
+
const del = document.createElement('button');
|
|
154
|
+
del.className = 'nb-icon-btn nb-danger nbc-md-del';
|
|
155
|
+
del.textContent = '✕';
|
|
156
|
+
del.title = 'delete cell';
|
|
157
|
+
del.addEventListener('click', () => deleteCellWithConfirm(state, cell, (c) => 'markdown cell', rerender));
|
|
158
|
+
wrap.appendChild(del);
|
|
159
|
+
} else {
|
|
160
|
+
const codeCell = document.createElement('div');
|
|
161
|
+
codeCell.className = 'nb-code-cell nbc-code-cell';
|
|
162
|
+
|
|
163
|
+
// Cell title row with run controls FIRST (left), then meta
|
|
164
|
+
const titleRow = document.createElement('div');
|
|
165
|
+
titleRow.className = 'nbc-title-row';
|
|
166
|
+
titleRow.innerHTML = `
|
|
167
|
+
<span class="nbc-run-controls"></span>
|
|
168
|
+
${cell.varname ? `<span class="nbc-arrow-var">→ ${cell.varname}</span>` : ''}
|
|
169
|
+
<span class="nbc-meta-info">${cell.type === 'sql' ? '4 rows' : 'depends on rows'}</span>
|
|
170
|
+
<button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
|
|
171
|
+
<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>
|
|
172
|
+
<button class="nb-icon-btn nb-danger nbc-code-del">✕</button>
|
|
173
|
+
`;
|
|
174
|
+
codeCell.appendChild(titleRow);
|
|
175
|
+
mountRunControls(titleRow.querySelector('.nbc-run-controls') as HTMLElement, cell, wrap, rerender);
|
|
176
|
+
|
|
177
|
+
const codeBody = document.createElement('div');
|
|
178
|
+
codeBody.className = 'nbc-code-body' + (cell.hideSource ? ' nbc-hidden' : '');
|
|
179
|
+
const ta = document.createElement('textarea');
|
|
180
|
+
ta.className = 'nb-code-edit';
|
|
181
|
+
ta.value = cell.content;
|
|
182
|
+
ta.rows = 1;
|
|
183
|
+
ta.spellcheck = false;
|
|
184
|
+
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
|
|
185
|
+
codeBody.appendChild(ta);
|
|
186
|
+
codeCell.appendChild(codeBody);
|
|
187
|
+
setTimeout(() => autosize(ta), 0);
|
|
188
|
+
|
|
189
|
+
const result = document.createElement('div');
|
|
190
|
+
result.className = 'nbc-result-body' + (cell.hideResult ? ' nbc-hidden' : '');
|
|
191
|
+
if (cell.type === 'sql') {
|
|
192
|
+
result.innerHTML = `
|
|
193
|
+
<div class="nbc-result-row">
|
|
194
|
+
<span>row_1</span><span>42</span>
|
|
195
|
+
<span>row_2</span><span>17</span>
|
|
196
|
+
<span>row_3</span><span>8</span>
|
|
197
|
+
<span>row_4</span><span>3</span>
|
|
198
|
+
</div>`;
|
|
199
|
+
} else {
|
|
200
|
+
result.className = 'nbc-chart-result' + (cell.hideResult ? ' nbc-hidden' : '');
|
|
201
|
+
result.innerHTML = `
|
|
202
|
+
<div class="nbc-bar" style="height:100%"></div>
|
|
203
|
+
<div class="nbc-bar" style="height:68%"></div>
|
|
204
|
+
<div class="nbc-bar" style="height:52%"></div>
|
|
205
|
+
<div class="nbc-bar" style="height:22%"></div>`;
|
|
206
|
+
}
|
|
207
|
+
codeCell.appendChild(result);
|
|
208
|
+
body.appendChild(codeCell);
|
|
209
|
+
|
|
210
|
+
(titleRow.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
|
|
211
|
+
(titleRow.querySelector('.nb-toggle-res') as HTMLElement).addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
|
|
212
|
+
(titleRow.querySelector('.nbc-code-del') as HTMLElement).addEventListener('click', () =>
|
|
213
|
+
deleteCellWithConfirm(state, cell, (c) => `${c.type} cell${c.varname ? ' → ' + c.varname : ''}`, rerender)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
row.appendChild(body);
|
|
218
|
+
wrap.appendChild(row);
|
|
219
|
+
return wrap;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Layout-specific styles
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
function injectLayoutStyles(): void {
|
|
227
|
+
if (document.getElementById('nbc-styles')) return;
|
|
228
|
+
const style = document.createElement('style');
|
|
229
|
+
style.id = 'nbc-styles';
|
|
230
|
+
style.textContent = `
|
|
231
|
+
.nbc-shell {
|
|
232
|
+
background: var(--color-surface); border: 1px solid var(--color-border);
|
|
233
|
+
border-radius: 12px; padding: 18px;
|
|
234
|
+
}
|
|
235
|
+
.nbc-toolbar {
|
|
236
|
+
display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;
|
|
237
|
+
}
|
|
238
|
+
.nbc-status {
|
|
239
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
240
|
+
font-size: 11px; color: var(--color-text2);
|
|
241
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
242
|
+
}
|
|
243
|
+
.nbc-status-dot {
|
|
244
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
245
|
+
background: var(--color-teal);
|
|
246
|
+
}
|
|
247
|
+
.nbc-status-dot.nbc-stale { background: var(--color-amber); }
|
|
248
|
+
.nbc-actions { display: flex; gap: 6px; align-items: center; }
|
|
249
|
+
.nbc-history-panel { margin-bottom: 12px; }
|
|
250
|
+
|
|
251
|
+
.nbc-cell { margin-bottom: 14px; position: relative; }
|
|
252
|
+
.nbc-cell:last-child { margin-bottom: 0; }
|
|
253
|
+
.nbc-row {
|
|
254
|
+
display: grid; grid-template-columns: 20px 34px 1fr; gap: 6px;
|
|
255
|
+
}
|
|
256
|
+
.nbc-gutter {
|
|
257
|
+
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
|
258
|
+
}
|
|
259
|
+
.nbc-type-label {
|
|
260
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
261
|
+
font-size: 10px; letter-spacing: 0.12em; color: var(--color-text2);
|
|
262
|
+
}
|
|
263
|
+
.nbc-gutter-sql .nbc-type-label { color: var(--color-accent); }
|
|
264
|
+
.nbc-gutter-js .nbc-type-label { color: var(--color-teal); }
|
|
265
|
+
.nbc-line { width: 1px; flex: 1; background: var(--color-border); }
|
|
266
|
+
.nbc-md-body { padding: 4px 2px; border: 1px dashed transparent; border-radius: 4px; }
|
|
267
|
+
.nbc-md-body:focus-within { border-color: var(--color-border); background: var(--color-bg); }
|
|
268
|
+
.nbc-md-del {
|
|
269
|
+
position: absolute; top: 4px; right: 4px;
|
|
270
|
+
opacity: 0; transition: opacity 0.15s;
|
|
271
|
+
}
|
|
272
|
+
.nbc-cell:hover .nbc-md-del { opacity: 0.5; }
|
|
273
|
+
.nbc-md-del:hover { opacity: 1 !important; }
|
|
274
|
+
|
|
275
|
+
.nbc-code-cell {
|
|
276
|
+
background: var(--color-bg); border: 1px solid var(--color-border);
|
|
277
|
+
border-radius: 8px; overflow: hidden;
|
|
278
|
+
transition: border-color 0.15s;
|
|
279
|
+
}
|
|
280
|
+
.nbc-code-cell:focus-within { border-color: var(--color-border2); }
|
|
281
|
+
.nbc-title-row {
|
|
282
|
+
display: flex; align-items: center; gap: 8px;
|
|
283
|
+
padding: 8px 12px; border-bottom: 1px solid var(--color-border);
|
|
284
|
+
background: var(--color-surface2);
|
|
285
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
286
|
+
font-size: 10.5px; color: var(--color-text2);
|
|
287
|
+
}
|
|
288
|
+
.nbc-title-row .nbc-arrow-var { color: var(--color-accent); }
|
|
289
|
+
.nbc-title-row .nbc-meta-info { margin-right: auto; }
|
|
290
|
+
.nbc-code-body { padding: 10px 14px; }
|
|
291
|
+
.nbc-hidden { display: none !important; }
|
|
292
|
+
.nbc-result-body {
|
|
293
|
+
padding: 10px 14px;
|
|
294
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
border-top: 1px solid var(--color-border);
|
|
297
|
+
}
|
|
298
|
+
.nbc-result-row {
|
|
299
|
+
display: grid; grid-template-columns: 1fr auto; gap: 3px 24px;
|
|
300
|
+
color: var(--color-text2);
|
|
301
|
+
}
|
|
302
|
+
.nbc-result-row span:nth-child(even) {
|
|
303
|
+
color: var(--color-text1); font-variant-numeric: tabular-nums;
|
|
304
|
+
}
|
|
305
|
+
.nbc-chart-result {
|
|
306
|
+
padding: 14px; display: flex; align-items: flex-end; gap: 6px;
|
|
307
|
+
height: 92px; border-top: 1px solid var(--color-border);
|
|
308
|
+
}
|
|
309
|
+
.nbc-bar { flex: 1; background: var(--color-accent); border-radius: 2px 2px 0 0; opacity: 0.55; }
|
|
310
|
+
`;
|
|
311
|
+
document.head.appendChild(style);
|
|
312
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// notebook-document — collaborative doc layout (deepnote-like)
|
|
4
|
+
// Title + avatars, inline highlights, margin comments, minimal cell chrome.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createState, injectStyles, mountRunControls, mountHistoryPanel,
|
|
9
|
+
setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
|
|
10
|
+
autosize, openShareModal, registerHistoryObserver,
|
|
11
|
+
buildServersButton,
|
|
12
|
+
type NotebookState, type NotebookCell,
|
|
13
|
+
} from './shared.js';
|
|
14
|
+
|
|
15
|
+
export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
|
|
16
|
+
injectStyles();
|
|
17
|
+
injectLayoutStyles();
|
|
18
|
+
|
|
19
|
+
const state: NotebookState = createState({
|
|
20
|
+
id: data.id as string,
|
|
21
|
+
title: data.title as string ?? 'Untitled notebook',
|
|
22
|
+
mode: (data.mode as any) ?? 'edit',
|
|
23
|
+
cells: data.cells as any,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
container.classList.add('nb-root');
|
|
27
|
+
container.classList.toggle('nb-view-mode', state.mode === 'view');
|
|
28
|
+
|
|
29
|
+
container.innerHTML = `
|
|
30
|
+
<div class="nbd-shell">
|
|
31
|
+
<div class="nbd-presence">
|
|
32
|
+
<div class="nbd-avatars">
|
|
33
|
+
<div class="nbd-av nbd-av1">A</div>
|
|
34
|
+
<div class="nbd-av nbd-av2">B</div>
|
|
35
|
+
<div class="nbd-av nbd-av3">+1</div>
|
|
36
|
+
</div>
|
|
37
|
+
<span class="nbd-label">3 editors online</span>
|
|
38
|
+
<div class="nb-mode-switch" style="margin-left:auto;">
|
|
39
|
+
<button class="nb-mode-edit nb-on">edit</button>
|
|
40
|
+
<button class="nb-mode-view">view</button>
|
|
41
|
+
</div>
|
|
42
|
+
<button class="nb-btn nbd-history-btn">⟲ history</button>
|
|
43
|
+
<span class="nbd-servers-slot"></span>
|
|
44
|
+
</div>
|
|
45
|
+
<input class="nbd-title nb-doc-title" value="${escapeAttr(state.title)}">
|
|
46
|
+
<div class="nbd-meta">edited just now · saved ✓</div>
|
|
47
|
+
<div class="nb-history-panel nbd-history-panel"></div>
|
|
48
|
+
<div class="nbd-cells"></div>
|
|
49
|
+
<div class="nbd-footer">
|
|
50
|
+
<button class="nb-btn nb-add-cell" data-add="md">+ text</button>
|
|
51
|
+
<button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
|
|
52
|
+
<button class="nb-btn nb-add-cell" data-add="js">+ code</button>
|
|
53
|
+
<div class="nbd-spacer">
|
|
54
|
+
<span class="nbd-share-link nbd-share-btn">invite · share</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>`;
|
|
58
|
+
|
|
59
|
+
const shell = container.querySelector('.nbd-shell') as HTMLElement;
|
|
60
|
+
const cellsEl = shell.querySelector('.nbd-cells') as HTMLElement;
|
|
61
|
+
const historyPanel = shell.querySelector('.nbd-history-panel') as HTMLElement;
|
|
62
|
+
|
|
63
|
+
function renderCells() {
|
|
64
|
+
cellsEl.innerHTML = '';
|
|
65
|
+
state.cells.forEach((cell) => cellsEl.appendChild(renderCell(cell, state, rerender)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rerender() {
|
|
69
|
+
mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
|
|
70
|
+
renderCells();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
|
|
74
|
+
btn.addEventListener('click', () => {
|
|
75
|
+
const type = btn.dataset.add as any;
|
|
76
|
+
addCell(state, type);
|
|
77
|
+
rerender();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
(shell.querySelector('.nbd-history-btn') as HTMLElement).addEventListener('click', () => {
|
|
81
|
+
historyPanel.classList.toggle('nb-open');
|
|
82
|
+
});
|
|
83
|
+
(shell.querySelector('.nbd-share-btn') as HTMLElement).addEventListener('click', () => {
|
|
84
|
+
openShareModal(state, (fmt) => console.log('[notebook-document] share as', fmt, state));
|
|
85
|
+
});
|
|
86
|
+
(shell.querySelector('.nbd-title') as HTMLInputElement).addEventListener('input', (e) => {
|
|
87
|
+
state.title = (e.target as HTMLInputElement).value;
|
|
88
|
+
});
|
|
89
|
+
const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
|
|
90
|
+
const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
|
|
91
|
+
editBtn.addEventListener('click', () => {
|
|
92
|
+
state.mode = 'edit';
|
|
93
|
+
container.classList.remove('nb-view-mode');
|
|
94
|
+
editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
|
|
95
|
+
});
|
|
96
|
+
viewBtn.addEventListener('click', () => {
|
|
97
|
+
state.mode = 'view';
|
|
98
|
+
container.classList.add('nb-view-mode');
|
|
99
|
+
viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
buildServersButton(state, shell.querySelector('.nbd-servers-slot') as HTMLElement, data, rerender);
|
|
103
|
+
|
|
104
|
+
setupDnD(cellsEl, state, rerender);
|
|
105
|
+
const unsubHistory = registerHistoryObserver(() => mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); }));
|
|
106
|
+
|
|
107
|
+
rerender();
|
|
108
|
+
return () => { unsubHistory(); };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderCell(cell: NotebookCell, state: NotebookState, rerender: () => void): HTMLElement {
|
|
112
|
+
const wrap = document.createElement('div');
|
|
113
|
+
wrap.className = 'nb-cell-wrapper nbd-cell';
|
|
114
|
+
wrap.dataset.id = cell.id;
|
|
115
|
+
|
|
116
|
+
if (cell.type === 'md') {
|
|
117
|
+
const handle = document.createElement('span');
|
|
118
|
+
handle.className = 'nb-drag-handle nbd-md-handle';
|
|
119
|
+
handle.draggable = true;
|
|
120
|
+
handle.textContent = '⋮⋮';
|
|
121
|
+
wrap.appendChild(handle);
|
|
122
|
+
|
|
123
|
+
const p = document.createElement('div');
|
|
124
|
+
p.className = 'nbd-prose';
|
|
125
|
+
p.contentEditable = 'true';
|
|
126
|
+
p.innerHTML = cell.content;
|
|
127
|
+
p.addEventListener('input', () => { cell.content = p.innerHTML; });
|
|
128
|
+
wrap.appendChild(p);
|
|
129
|
+
|
|
130
|
+
const del = document.createElement('button');
|
|
131
|
+
del.className = 'nb-icon-btn nb-danger nbd-del-abs';
|
|
132
|
+
del.textContent = '✕';
|
|
133
|
+
del.addEventListener('click', () =>
|
|
134
|
+
deleteCellWithConfirm(state, cell, () => 'markdown block', rerender)
|
|
135
|
+
);
|
|
136
|
+
wrap.appendChild(del);
|
|
137
|
+
return wrap;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const row = document.createElement('div');
|
|
141
|
+
row.className = 'nbd-row' + (cell.comment ? '' : ' nbd-no-comment');
|
|
142
|
+
|
|
143
|
+
const codeCell = document.createElement('div');
|
|
144
|
+
codeCell.className = 'nb-code-cell nbd-code-cell';
|
|
145
|
+
|
|
146
|
+
const head = document.createElement('div');
|
|
147
|
+
head.className = 'nbd-cell-head';
|
|
148
|
+
head.innerHTML = `
|
|
149
|
+
<span class="nb-drag-handle" draggable="true" title="drag">⋮⋮</span>
|
|
150
|
+
<span class="nbd-run-controls"></span>
|
|
151
|
+
<span class="${cell.type === 'sql' ? 'nbd-type-sql' : 'nbd-type-js'}">${cell.type}</span>
|
|
152
|
+
<span class="nbd-meta-info">${cell.lastMs != null ? cell.lastMs + 'ms' : ''}</span>
|
|
153
|
+
<div class="nbd-actions">
|
|
154
|
+
<button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
|
|
155
|
+
<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>
|
|
156
|
+
<button class="nb-icon-btn nb-danger nbd-del">✕</button>
|
|
157
|
+
</div>`;
|
|
158
|
+
codeCell.appendChild(head);
|
|
159
|
+
mountRunControls(head.querySelector('.nbd-run-controls') as HTMLElement, cell, wrap, rerender);
|
|
160
|
+
|
|
161
|
+
const body = document.createElement('div');
|
|
162
|
+
body.className = 'nbd-code-body' + (cell.hideSource ? ' nbd-hidden' : '');
|
|
163
|
+
const ta = document.createElement('textarea');
|
|
164
|
+
ta.className = 'nb-code-edit';
|
|
165
|
+
ta.value = cell.content;
|
|
166
|
+
ta.rows = 1;
|
|
167
|
+
ta.spellcheck = false;
|
|
168
|
+
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
|
|
169
|
+
body.appendChild(ta);
|
|
170
|
+
codeCell.appendChild(body);
|
|
171
|
+
setTimeout(() => autosize(ta), 0);
|
|
172
|
+
|
|
173
|
+
if (cell.type === 'sql' && !cell.hideResult) {
|
|
174
|
+
const res = document.createElement('div');
|
|
175
|
+
res.className = 'nbd-result-inline';
|
|
176
|
+
res.innerHTML = `
|
|
177
|
+
<table>
|
|
178
|
+
<tr><td>row_1</td><td>42</td></tr>
|
|
179
|
+
<tr><td>row_2</td><td>29</td></tr>
|
|
180
|
+
<tr><td>row_3</td><td>22</td></tr>
|
|
181
|
+
<tr><td>row_4</td><td>9</td></tr>
|
|
182
|
+
</table>`;
|
|
183
|
+
codeCell.appendChild(res);
|
|
184
|
+
} else if (cell.type === 'js' && !cell.hideResult) {
|
|
185
|
+
const chart = document.createElement('div');
|
|
186
|
+
chart.className = 'nbd-chart';
|
|
187
|
+
chart.innerHTML = `
|
|
188
|
+
<div class="nbd-bar" style="height:100%"></div>
|
|
189
|
+
<div class="nbd-bar" style="height:68%"></div>
|
|
190
|
+
<div class="nbd-bar" style="height:52%"></div>
|
|
191
|
+
<div class="nbd-bar" style="height:22%"></div>`;
|
|
192
|
+
codeCell.appendChild(chart);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
row.appendChild(codeCell);
|
|
196
|
+
|
|
197
|
+
if (cell.comment) {
|
|
198
|
+
const c = document.createElement('div');
|
|
199
|
+
c.className = 'nbd-comment';
|
|
200
|
+
c.innerHTML = `
|
|
201
|
+
<div class="nbd-comment-who">
|
|
202
|
+
<div class="nbd-av-small">${escapeHtml(cell.comment.who.slice(0, 2).toUpperCase())}</div>
|
|
203
|
+
<span class="nbd-who-name">${escapeHtml(cell.comment.who)}</span>
|
|
204
|
+
<span class="nbd-when">${escapeHtml(cell.comment.when)}</span>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="nbd-comment-body">${escapeHtml(cell.comment.body)}</div>`;
|
|
207
|
+
row.appendChild(c);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
(head.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
|
|
211
|
+
(head.querySelector('.nb-toggle-res') as HTMLElement).addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
|
|
212
|
+
(head.querySelector('.nbd-del') as HTMLElement).addEventListener('click', () =>
|
|
213
|
+
deleteCellWithConfirm(state, cell, (c) => `${c.type} cell`, rerender)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
wrap.appendChild(row);
|
|
217
|
+
return wrap;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function escapeHtml(s: string): string {
|
|
221
|
+
return (s ?? '').replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]!));
|
|
222
|
+
}
|
|
223
|
+
function escapeAttr(s: string): string {
|
|
224
|
+
return (s ?? '').replace(/"/g, '"');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function injectLayoutStyles(): void {
|
|
228
|
+
if (document.getElementById('nbd-styles')) return;
|
|
229
|
+
const style = document.createElement('style');
|
|
230
|
+
style.id = 'nbd-styles';
|
|
231
|
+
style.textContent = `
|
|
232
|
+
.nbd-shell {
|
|
233
|
+
background: var(--color-surface);
|
|
234
|
+
border: 1px solid var(--color-border);
|
|
235
|
+
border-radius: 12px;
|
|
236
|
+
padding: 28px 32px;
|
|
237
|
+
}
|
|
238
|
+
.nbd-presence { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
|
|
239
|
+
.nbd-avatars { display: flex; }
|
|
240
|
+
.nbd-av {
|
|
241
|
+
width: 22px; height: 22px; border-radius: 50%;
|
|
242
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
243
|
+
font-size: 9px; font-weight: 600;
|
|
244
|
+
display: flex; align-items: center; justify-content: center;
|
|
245
|
+
border: 2px solid var(--color-surface);
|
|
246
|
+
}
|
|
247
|
+
.nbd-av + .nbd-av { margin-left: -6px; }
|
|
248
|
+
.nbd-av1 { background: rgba(124,109,250,0.25); color: var(--color-accent); }
|
|
249
|
+
.nbd-av2 { background: rgba(62,207,178,0.22); color: var(--color-teal); }
|
|
250
|
+
.nbd-av3 { background: rgba(240,160,80,0.22); color: var(--color-amber); }
|
|
251
|
+
.nbd-label {
|
|
252
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
253
|
+
font-size: 11px; color: var(--color-text2);
|
|
254
|
+
}
|
|
255
|
+
.nbd-title {
|
|
256
|
+
font-family: var(--font-sans, 'Syne', sans-serif);
|
|
257
|
+
font-size: 24px; font-weight: 600;
|
|
258
|
+
letter-spacing: -0.02em; margin: 6px 0 2px;
|
|
259
|
+
background: transparent; border: none; outline: none;
|
|
260
|
+
color: var(--color-text1);
|
|
261
|
+
width: 100%; padding: 2px 4px; border-radius: 3px;
|
|
262
|
+
}
|
|
263
|
+
.nbd-title:focus { background: var(--color-bg); }
|
|
264
|
+
.nbd-meta {
|
|
265
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
266
|
+
font-size: 11px; color: var(--color-text2);
|
|
267
|
+
margin-bottom: 20px;
|
|
268
|
+
}
|
|
269
|
+
.nbd-history-panel { margin-bottom: 12px; }
|
|
270
|
+
|
|
271
|
+
.nbd-cell { position: relative; margin-bottom: 18px; }
|
|
272
|
+
.nbd-prose {
|
|
273
|
+
font-size: 15px; line-height: 1.7;
|
|
274
|
+
color: var(--color-text1);
|
|
275
|
+
outline: none;
|
|
276
|
+
padding: 4px 6px;
|
|
277
|
+
border-radius: 3px;
|
|
278
|
+
border: 1px dashed transparent;
|
|
279
|
+
}
|
|
280
|
+
.nbd-prose:focus { border-color: var(--color-border); background: var(--color-bg); }
|
|
281
|
+
.nbd-prose mark {
|
|
282
|
+
background: rgba(240,160,80,0.18);
|
|
283
|
+
color: var(--color-amber);
|
|
284
|
+
padding: 0 4px; border-radius: 2px;
|
|
285
|
+
}
|
|
286
|
+
.nbd-md-handle { position: absolute; left: -20px; top: 6px; }
|
|
287
|
+
.nbd-del-abs {
|
|
288
|
+
position: absolute; top: 4px; right: 4px;
|
|
289
|
+
opacity: 0; transition: opacity 0.15s;
|
|
290
|
+
}
|
|
291
|
+
.nbd-cell:hover .nbd-del-abs { opacity: 0.5; }
|
|
292
|
+
.nbd-del-abs:hover { opacity: 1 !important; }
|
|
293
|
+
|
|
294
|
+
.nbd-row {
|
|
295
|
+
display: grid;
|
|
296
|
+
grid-template-columns: 1fr 150px;
|
|
297
|
+
gap: 16px; align-items: start;
|
|
298
|
+
}
|
|
299
|
+
.nbd-row.nbd-no-comment { grid-template-columns: 1fr; }
|
|
300
|
+
|
|
301
|
+
.nbd-code-cell {
|
|
302
|
+
border: 1px solid var(--color-border);
|
|
303
|
+
border-radius: 8px; overflow: hidden;
|
|
304
|
+
background: var(--color-bg);
|
|
305
|
+
}
|
|
306
|
+
.nbd-cell-head {
|
|
307
|
+
padding: 7px 12px;
|
|
308
|
+
display: flex; align-items: center; gap: 8px;
|
|
309
|
+
border-bottom: 1px solid var(--color-border);
|
|
310
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
311
|
+
font-size: 10.5px; color: var(--color-text2);
|
|
312
|
+
}
|
|
313
|
+
.nbd-type-sql { color: var(--color-accent); text-transform: uppercase; letter-spacing: 0.08em; font-size: 9.5px; }
|
|
314
|
+
.nbd-type-js { color: var(--color-teal); text-transform: uppercase; letter-spacing: 0.08em; font-size: 9.5px; }
|
|
315
|
+
.nbd-actions { margin-left: auto; display: flex; gap: 6px; }
|
|
316
|
+
.nbd-code-body { padding: 11px 12px; }
|
|
317
|
+
.nbd-hidden { display: none !important; }
|
|
318
|
+
.nbd-result-inline {
|
|
319
|
+
background: var(--color-surface2);
|
|
320
|
+
padding: 10px 12px;
|
|
321
|
+
border-top: 1px solid var(--color-border);
|
|
322
|
+
}
|
|
323
|
+
.nbd-result-inline table { width: 100%; border-collapse: collapse; font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
|
324
|
+
.nbd-result-inline table td { padding: 3px 0; color: var(--color-text1); }
|
|
325
|
+
.nbd-result-inline table td:first-child { color: var(--color-text2); font-variant-numeric: normal; }
|
|
326
|
+
.nbd-result-inline table td:last-child { text-align: right; }
|
|
327
|
+
|
|
328
|
+
.nbd-comment {
|
|
329
|
+
background: rgba(240,160,80,0.08);
|
|
330
|
+
border-left: 2px solid var(--color-amber);
|
|
331
|
+
border-radius: 0 6px 6px 0;
|
|
332
|
+
padding: 10px 12px;
|
|
333
|
+
font-size: 11.5px;
|
|
334
|
+
}
|
|
335
|
+
.nbd-comment-who { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|
336
|
+
.nbd-av-small {
|
|
337
|
+
width: 15px; height: 15px; border-radius: 50%;
|
|
338
|
+
background: rgba(240,160,80,0.25); color: var(--color-amber);
|
|
339
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
340
|
+
font-size: 8px; font-weight: 600;
|
|
341
|
+
display: flex; align-items: center; justify-content: center;
|
|
342
|
+
}
|
|
343
|
+
.nbd-who-name {
|
|
344
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
345
|
+
font-size: 10.5px; color: var(--color-amber); font-weight: 500;
|
|
346
|
+
}
|
|
347
|
+
.nbd-when {
|
|
348
|
+
margin-left: auto;
|
|
349
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
350
|
+
font-size: 10px; color: var(--color-text2);
|
|
351
|
+
}
|
|
352
|
+
.nbd-comment-body { color: var(--color-text1); line-height: 1.5; }
|
|
353
|
+
|
|
354
|
+
.nbd-chart { padding: 16px; display: flex; align-items: flex-end; gap: 10px; height: 95px; }
|
|
355
|
+
.nbd-bar { flex: 1; background: var(--color-accent); border-radius: 2px 2px 0 0; }
|
|
356
|
+
|
|
357
|
+
.nbd-footer {
|
|
358
|
+
display: flex; gap: 8px;
|
|
359
|
+
padding-top: 14px; margin-top: 20px;
|
|
360
|
+
border-top: 1px solid var(--color-border);
|
|
361
|
+
align-items: center;
|
|
362
|
+
}
|
|
363
|
+
.nbd-spacer { margin-left: auto; }
|
|
364
|
+
.nbd-share-link {
|
|
365
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
366
|
+
font-size: 11px; color: var(--color-text2);
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
}
|
|
369
|
+
.nbd-share-link:hover { color: var(--color-text1); }
|
|
370
|
+
`;
|
|
371
|
+
document.head.appendChild(style);
|
|
372
|
+
}
|