pylogue 0.3__py3-none-any.whl → 0.3.30__py3-none-any.whl
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.
- pylogue/core.py +804 -0
- pylogue/embeds.py +32 -0
- pylogue/integrations/__init__.py +1 -0
- pylogue/integrations/pydantic_ai.py +417 -0
- pylogue/legacy/cards.py +112 -0
- pylogue/{chat.py → legacy/chat.py} +54 -27
- pylogue/{chatapp.py → legacy/chatapp.py} +65 -26
- pylogue/legacy/design_system.py +117 -0
- pylogue/legacy/renderer.py +284 -0
- pylogue/shell.py +342 -0
- pylogue/static/pylogue-core.css +372 -0
- pylogue/static/pylogue-core.js +199 -0
- pylogue/static/pylogue-markdown.js +745 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/METADATA +10 -1
- pylogue-0.3.30.dist-info/RECORD +26 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/WHEEL +1 -1
- pylogue/cards.py +0 -174
- pylogue/renderer.py +0 -139
- pylogue-0.3.dist-info/RECORD +0 -17
- /pylogue/{__init__.py → legacy/__init__.py} +0 -0
- /pylogue/{__pre_init__.py → legacy/__pre_init__.py} +0 -0
- /pylogue/{_modidx.py → legacy/_modidx.py} +0 -0
- /pylogue/{health.py → legacy/health.py} +0 -0
- /pylogue/{service.py → legacy/service.py} +0 -0
- /pylogue/{session.py → legacy/session.py} +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/entry_points.txt +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/licenses/AUTHORS.md +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/licenses/LICENSE +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
|
|
2
|
+
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
|
3
|
+
marked.setOptions({ gfm: true, breaks: true });
|
|
4
|
+
|
|
5
|
+
let markdownRendering = false;
|
|
6
|
+
let pendingScrollState = null;
|
|
7
|
+
const KATEX_DOLLAR_PLACEHOLDER = '@@PYLOGUE_DOLLAR@@';
|
|
8
|
+
|
|
9
|
+
const getScrollState = () => {
|
|
10
|
+
const scrollElement = document.scrollingElement || document.documentElement;
|
|
11
|
+
if (!scrollElement) return null;
|
|
12
|
+
const maxScrollTop = scrollElement.scrollHeight - scrollElement.clientHeight;
|
|
13
|
+
const atBottom = maxScrollTop - scrollElement.scrollTop < 24;
|
|
14
|
+
return { top: scrollElement.scrollTop, atBottom };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const restoreScrollState = (state) => {
|
|
18
|
+
if (!state) return;
|
|
19
|
+
const scrollElement = document.scrollingElement || document.documentElement;
|
|
20
|
+
if (!scrollElement) return;
|
|
21
|
+
if (state.atBottom) {
|
|
22
|
+
scrollElement.scrollTop = scrollElement.scrollHeight;
|
|
23
|
+
} else {
|
|
24
|
+
scrollElement.scrollTop = state.top;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const forceScrollToBottom = () => {
|
|
29
|
+
const scrollElement = document.scrollingElement || document.documentElement;
|
|
30
|
+
if (!scrollElement) return;
|
|
31
|
+
const anchor = document.getElementById('scroll-anchor');
|
|
32
|
+
const apply = () => {
|
|
33
|
+
if (anchor) {
|
|
34
|
+
anchor.scrollIntoView({ block: 'end' });
|
|
35
|
+
} else {
|
|
36
|
+
scrollElement.scrollTop = scrollElement.scrollHeight;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
requestAnimationFrame(() => {
|
|
40
|
+
apply();
|
|
41
|
+
setTimeout(apply, 0);
|
|
42
|
+
setTimeout(apply, 50);
|
|
43
|
+
setTimeout(apply, 150);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
window.__forceScrollToBottom = forceScrollToBottom;
|
|
47
|
+
|
|
48
|
+
const isNearBottom = (threshold = 32) => {
|
|
49
|
+
const scrollElement = document.scrollingElement || document.documentElement;
|
|
50
|
+
if (!scrollElement) return false;
|
|
51
|
+
const maxScrollTop = scrollElement.scrollHeight - scrollElement.clientHeight;
|
|
52
|
+
return maxScrollTop - scrollElement.scrollTop <= threshold;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let bottomLockUntil = 0;
|
|
56
|
+
let bottomLockRaf = null;
|
|
57
|
+
|
|
58
|
+
const tickBottomLock = () => {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (now > bottomLockUntil) {
|
|
61
|
+
bottomLockRaf = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const scrollElement = document.scrollingElement || document.documentElement;
|
|
65
|
+
if (scrollElement) {
|
|
66
|
+
scrollElement.scrollTop = scrollElement.scrollHeight;
|
|
67
|
+
}
|
|
68
|
+
bottomLockRaf = requestAnimationFrame(tickBottomLock);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const startBottomLock = (durationMs = 1200) => {
|
|
72
|
+
if (!isNearBottom()) return;
|
|
73
|
+
bottomLockUntil = Date.now() + durationMs;
|
|
74
|
+
if (!bottomLockRaf) {
|
|
75
|
+
bottomLockRaf = requestAnimationFrame(tickBottomLock);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const decodeB64 = (value) => {
|
|
80
|
+
if (!value) return '';
|
|
81
|
+
try {
|
|
82
|
+
const binary = atob(value);
|
|
83
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
84
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const protectEscapedDollars = (md) => {
|
|
91
|
+
if (!md) return md || '';
|
|
92
|
+
const blocks = [];
|
|
93
|
+
const replaceBlock = (match) => {
|
|
94
|
+
blocks.push(match);
|
|
95
|
+
return `__PYLOGUE_CODEBLOCK_${blocks.length - 1}__`;
|
|
96
|
+
};
|
|
97
|
+
md = md.replace(/(```+|~~~+)[\s\S]*?\1/g, replaceBlock);
|
|
98
|
+
md = md.replace(/(`+)([^`]*?)\1/g, replaceBlock);
|
|
99
|
+
md = md.replace(/(\\+)\$/g, (match, slashes) => {
|
|
100
|
+
return '\\'.repeat(slashes.length - 1) + KATEX_DOLLAR_PLACEHOLDER;
|
|
101
|
+
});
|
|
102
|
+
blocks.forEach((block, index) => {
|
|
103
|
+
md = md.replace(`__PYLOGUE_CODEBLOCK_${index}__`, block);
|
|
104
|
+
});
|
|
105
|
+
return md;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const looksLikeHtmlBlock = (text) => {
|
|
109
|
+
if (!text) return false;
|
|
110
|
+
const trimmed = text.trim();
|
|
111
|
+
if (!trimmed.startsWith('<') || !trimmed.endsWith('>')) return false;
|
|
112
|
+
return /<\/?[a-zA-Z][\s\S]*?>/.test(trimmed);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const dedentHtml = (text) => {
|
|
116
|
+
if (!looksLikeHtmlBlock(text)) return text;
|
|
117
|
+
const lines = text.split(/\r?\n/);
|
|
118
|
+
let minIndent = null;
|
|
119
|
+
lines.forEach((line) => {
|
|
120
|
+
if (!line.trim()) return;
|
|
121
|
+
const match = line.match(/^[ \t]+/);
|
|
122
|
+
if (!match) {
|
|
123
|
+
minIndent = 0;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const indent = match[0].length;
|
|
127
|
+
if (minIndent === null || indent < minIndent) {
|
|
128
|
+
minIndent = indent;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (!minIndent) return text;
|
|
132
|
+
const strip = new RegExp(`^[ \\t]{0,${minIndent}}`);
|
|
133
|
+
return lines.map((line) => line.replace(strip, '')).join('\n');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const splitDivHtmlBlock = (text) => {
|
|
137
|
+
if (!text) return null;
|
|
138
|
+
if (text.includes('```')) return null;
|
|
139
|
+
const start = text.indexOf('<div');
|
|
140
|
+
const end = text.lastIndexOf('</div>');
|
|
141
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
142
|
+
const htmlEnd = end + 6;
|
|
143
|
+
return {
|
|
144
|
+
prefix: text.slice(0, start),
|
|
145
|
+
html: text.slice(start, htmlEnd),
|
|
146
|
+
suffix: text.slice(htmlEnd),
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const replaceDollarPlaceholders = (root) => {
|
|
151
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
152
|
+
const nodes = [];
|
|
153
|
+
let node;
|
|
154
|
+
while ((node = walker.nextNode())) {
|
|
155
|
+
if (node.nodeValue && node.nodeValue.includes(KATEX_DOLLAR_PLACEHOLDER)) {
|
|
156
|
+
nodes.push(node);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
nodes.forEach((textNode) => {
|
|
160
|
+
textNode.nodeValue = textNode.nodeValue
|
|
161
|
+
.split(KATEX_DOLLAR_PLACEHOLDER)
|
|
162
|
+
.join('$');
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const renderMath = (root) => {
|
|
167
|
+
if (typeof renderMathInElement !== 'function') return;
|
|
168
|
+
renderMathInElement(root, {
|
|
169
|
+
delimiters: [
|
|
170
|
+
{ left: '$$', right: '$$', display: true },
|
|
171
|
+
{ left: '$', right: '$', display: false },
|
|
172
|
+
],
|
|
173
|
+
throwOnError: false,
|
|
174
|
+
ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
|
|
175
|
+
});
|
|
176
|
+
replaceDollarPlaceholders(root);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const highlightCode = (root) => {
|
|
180
|
+
if (!window.hljs || typeof window.hljs.highlightElement !== 'function') return;
|
|
181
|
+
const blocks = root.querySelectorAll('pre code');
|
|
182
|
+
blocks.forEach((block) => {
|
|
183
|
+
if (block.dataset.hljsApplied === 'true') return;
|
|
184
|
+
window.hljs.highlightElement(block);
|
|
185
|
+
block.dataset.hljsApplied = 'true';
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const addCopyButtons = (root) => {
|
|
190
|
+
const blocks = root.querySelectorAll('pre');
|
|
191
|
+
blocks.forEach((pre) => {
|
|
192
|
+
if (pre.dataset.copyBound === 'true') return;
|
|
193
|
+
const code = pre.querySelector('code');
|
|
194
|
+
if (!code) return;
|
|
195
|
+
pre.dataset.copyBound = 'true';
|
|
196
|
+
pre.classList.add('codeblock');
|
|
197
|
+
const btn = document.createElement('button');
|
|
198
|
+
btn.type = 'button';
|
|
199
|
+
btn.className = 'code-copy-btn';
|
|
200
|
+
btn.setAttribute('aria-label', 'Copy code');
|
|
201
|
+
btn.setAttribute('title', 'Copy code');
|
|
202
|
+
btn.innerHTML = `
|
|
203
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
204
|
+
<rect x="9" y="9" width="10" height="10" rx="2"></rect>
|
|
205
|
+
<rect x="5" y="5" width="10" height="10" rx="2"></rect>
|
|
206
|
+
</svg>
|
|
207
|
+
`;
|
|
208
|
+
btn.addEventListener('click', async (event) => {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
const text = code.innerText || code.textContent || '';
|
|
211
|
+
try {
|
|
212
|
+
await navigator.clipboard.writeText(text);
|
|
213
|
+
btn.dataset.copied = 'true';
|
|
214
|
+
setTimeout(() => { btn.dataset.copied = 'false'; }, 1200);
|
|
215
|
+
} catch {
|
|
216
|
+
const textarea = document.createElement('textarea');
|
|
217
|
+
textarea.value = text;
|
|
218
|
+
document.body.appendChild(textarea);
|
|
219
|
+
textarea.select();
|
|
220
|
+
document.execCommand('copy');
|
|
221
|
+
document.body.removeChild(textarea);
|
|
222
|
+
btn.dataset.copied = 'true';
|
|
223
|
+
setTimeout(() => { btn.dataset.copied = 'false'; }, 1200);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
pre.appendChild(btn);
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const renderMarkdown = (root = document) => {
|
|
231
|
+
const nodes = root.querySelectorAll('.marked');
|
|
232
|
+
if (!marked || typeof marked.parse !== 'function') return;
|
|
233
|
+
if (nodes.length === 0) return;
|
|
234
|
+
markdownRendering = true;
|
|
235
|
+
nodes.forEach((el) => {
|
|
236
|
+
const rawB64 = el.getAttribute('data-raw-b64');
|
|
237
|
+
const rawAttr = el.getAttribute('data-raw');
|
|
238
|
+
const source = rawB64 ? decodeB64(rawB64) : (rawAttr !== null ? rawAttr : el.textContent);
|
|
239
|
+
if (el.dataset.renderedSource === source) return;
|
|
240
|
+
if (el.dataset.mermaidDirty === 'true') return;
|
|
241
|
+
const normalizedSource = dedentHtml(source);
|
|
242
|
+
const split = splitDivHtmlBlock(normalizedSource);
|
|
243
|
+
if (split) {
|
|
244
|
+
const safePrefix = protectEscapedDollars(split.prefix);
|
|
245
|
+
const safeSuffix = protectEscapedDollars(split.suffix);
|
|
246
|
+
const prefixHtml = safePrefix ? marked.parse(safePrefix) : '';
|
|
247
|
+
const suffixHtml = safeSuffix ? marked.parse(safeSuffix) : '';
|
|
248
|
+
el.innerHTML = `${prefixHtml}${split.html}${suffixHtml}`;
|
|
249
|
+
renderMath(el);
|
|
250
|
+
highlightCode(el);
|
|
251
|
+
addCopyButtons(el);
|
|
252
|
+
el.dataset.renderedSource = source;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (looksLikeHtmlBlock(normalizedSource)) {
|
|
256
|
+
el.innerHTML = normalizedSource;
|
|
257
|
+
} else {
|
|
258
|
+
const safeSource = protectEscapedDollars(normalizedSource);
|
|
259
|
+
el.innerHTML = marked.parse(safeSource);
|
|
260
|
+
renderMath(el);
|
|
261
|
+
highlightCode(el);
|
|
262
|
+
addCopyButtons(el);
|
|
263
|
+
}
|
|
264
|
+
el.dataset.renderedSource = source;
|
|
265
|
+
});
|
|
266
|
+
markdownRendering = false;
|
|
267
|
+
if (window.__upgradeMermaidBlocks) {
|
|
268
|
+
window.__upgradeMermaidBlocks(root);
|
|
269
|
+
}
|
|
270
|
+
if (window.__applyToolStatusUpdates) {
|
|
271
|
+
window.__applyToolStatusUpdates(root);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const observeMarkdown = () => {
|
|
276
|
+
const target = document.body;
|
|
277
|
+
if (!target) return;
|
|
278
|
+
let renderTimer = null;
|
|
279
|
+
const scheduleRender = () => {
|
|
280
|
+
if (markdownRendering) return;
|
|
281
|
+
if (renderTimer) return;
|
|
282
|
+
renderTimer = requestAnimationFrame(() => {
|
|
283
|
+
renderTimer = null;
|
|
284
|
+
renderMarkdown(document);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
const observer = new MutationObserver((mutations) => {
|
|
288
|
+
for (const mutation of mutations) {
|
|
289
|
+
if (mutation.type !== 'characterData') continue;
|
|
290
|
+
const parent = mutation.target && mutation.target.parentElement;
|
|
291
|
+
if (!parent) continue;
|
|
292
|
+
const markedRoot = parent.closest('.marked');
|
|
293
|
+
if (!markedRoot) continue;
|
|
294
|
+
const rawText = markedRoot.getAttribute('data-raw') || '';
|
|
295
|
+
if (!isMermaidFenceClosed(rawText)) {
|
|
296
|
+
markedRoot.dataset.mermaidDirty = 'true';
|
|
297
|
+
} else if (markedRoot.dataset.mermaidDirty === 'true') {
|
|
298
|
+
markedRoot.dataset.mermaidDirty = 'false';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
scheduleRender();
|
|
302
|
+
});
|
|
303
|
+
observer.observe(target, {
|
|
304
|
+
childList: true,
|
|
305
|
+
subtree: true,
|
|
306
|
+
characterData: true,
|
|
307
|
+
});
|
|
308
|
+
renderMarkdown(document);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const applyToolStatusUpdates = (root = document) => {
|
|
312
|
+
const updates = root.querySelectorAll('.tool-status-update[data-target-id]');
|
|
313
|
+
updates.forEach((update) => {
|
|
314
|
+
const targetId = update.getAttribute('data-target-id');
|
|
315
|
+
if (!targetId) return;
|
|
316
|
+
const target = document.getElementById(targetId);
|
|
317
|
+
if (!target) {
|
|
318
|
+
update.remove();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const replacement = document.createElement('div');
|
|
322
|
+
replacement.className = 'tool-status tool-status--done';
|
|
323
|
+
replacement.textContent = update.textContent || 'Completed';
|
|
324
|
+
target.replaceWith(replacement);
|
|
325
|
+
update.remove();
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
window.__applyToolStatusUpdates = applyToolStatusUpdates;
|
|
329
|
+
|
|
330
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
331
|
+
observeMarkdown();
|
|
332
|
+
renderMarkdown(document);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
336
|
+
renderMarkdown(event.target || document);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
document.body.addEventListener('htmx:beforeSwap', (event) => {
|
|
340
|
+
const target = event.detail && event.detail.target;
|
|
341
|
+
const cardsRoot = target && (target.closest ? target.closest('#cards') : null);
|
|
342
|
+
if (cardsRoot) {
|
|
343
|
+
pendingScrollState = getScrollState();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
348
|
+
const target = event.detail && event.detail.target;
|
|
349
|
+
const cardsRoot = target && (target.closest ? target.closest('#cards') : null);
|
|
350
|
+
if (cardsRoot) {
|
|
351
|
+
const state = pendingScrollState;
|
|
352
|
+
pendingScrollState = null;
|
|
353
|
+
if (state && state.atBottom) {
|
|
354
|
+
restoreScrollState(state);
|
|
355
|
+
} else {
|
|
356
|
+
forceScrollToBottom();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
document.body.addEventListener('htmx:wsAfterMessage', () => {
|
|
362
|
+
startBottomLock();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
document.body.addEventListener('htmx:wsBeforeMessage', () => {
|
|
366
|
+
startBottomLock();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
document.addEventListener('scroll', () => {
|
|
370
|
+
if (!isNearBottom()) {
|
|
371
|
+
bottomLockUntil = 0;
|
|
372
|
+
bottomLockRaf = null;
|
|
373
|
+
}
|
|
374
|
+
}, { passive: true });
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
379
|
+
|
|
380
|
+
let mermaidReady = false;
|
|
381
|
+
let mermaidCounter = 0;
|
|
382
|
+
const mermaidStates = {};
|
|
383
|
+
const mermaidCache = new Map();
|
|
384
|
+
const mermaidRenderPromises = new Map();
|
|
385
|
+
|
|
386
|
+
const ensureMermaid = () => {
|
|
387
|
+
if (mermaidReady) return;
|
|
388
|
+
mermaid.initialize({
|
|
389
|
+
startOnLoad: false,
|
|
390
|
+
suppressErrorRendering: true,
|
|
391
|
+
});
|
|
392
|
+
mermaidReady = true;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const hashMermaidCode = (text) => {
|
|
396
|
+
let hash = 0;
|
|
397
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
398
|
+
hash = ((hash << 5) - hash) + text.charCodeAt(i);
|
|
399
|
+
hash |= 0;
|
|
400
|
+
}
|
|
401
|
+
return `m${Math.abs(hash)}`;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const applyMermaidState = (wrapper, state) => {
|
|
405
|
+
const svg = wrapper.querySelector('svg');
|
|
406
|
+
if (!svg) return;
|
|
407
|
+
svg.style.pointerEvents = 'none';
|
|
408
|
+
svg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
409
|
+
svg.style.transformOrigin = 'center center';
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const fitSvgToWrapper = (wrapper, state) => {
|
|
413
|
+
const svg = wrapper.querySelector('svg');
|
|
414
|
+
if (!svg) return;
|
|
415
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
416
|
+
const svgRect = svg.getBoundingClientRect();
|
|
417
|
+
if (!wrapperRect.width || !wrapperRect.height || !svgRect.width || !svgRect.height) return;
|
|
418
|
+
const padding = 16;
|
|
419
|
+
const scaleX = (wrapperRect.width - padding) / svgRect.width;
|
|
420
|
+
const scaleY = (wrapperRect.height - padding) / svgRect.height;
|
|
421
|
+
const initialScale = Math.min(scaleX, scaleY, 1);
|
|
422
|
+
state.scale = initialScale;
|
|
423
|
+
state.translateX = 0;
|
|
424
|
+
state.translateY = 0;
|
|
425
|
+
applyMermaidState(wrapper, state);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const initMermaidInteraction = (wrapper) => {
|
|
429
|
+
if (wrapper.dataset.mermaidInteractive === 'true') return;
|
|
430
|
+
const svg = wrapper.querySelector('svg');
|
|
431
|
+
if (!svg) return;
|
|
432
|
+
|
|
433
|
+
const state = {
|
|
434
|
+
scale: 1,
|
|
435
|
+
translateX: 0,
|
|
436
|
+
translateY: 0,
|
|
437
|
+
isPanning: false,
|
|
438
|
+
startX: 0,
|
|
439
|
+
startY: 0,
|
|
440
|
+
};
|
|
441
|
+
mermaidStates[wrapper.id] = state;
|
|
442
|
+
wrapper.dataset.mermaidInteractive = 'true';
|
|
443
|
+
|
|
444
|
+
fitSvgToWrapper(wrapper, state);
|
|
445
|
+
|
|
446
|
+
wrapper.style.cursor = 'grab';
|
|
447
|
+
wrapper.style.touchAction = 'none';
|
|
448
|
+
|
|
449
|
+
wrapper.addEventListener('wheel', (e) => {
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
const currentSvg = wrapper.querySelector('svg');
|
|
452
|
+
if (!currentSvg) return;
|
|
453
|
+
const rect = currentSvg.getBoundingClientRect();
|
|
454
|
+
const mouseX = e.clientX - rect.left - rect.width / 2;
|
|
455
|
+
const mouseY = e.clientY - rect.top - rect.height / 2;
|
|
456
|
+
const zoomIntensity = 0.01;
|
|
457
|
+
const delta = e.deltaY > 0 ? 1 - zoomIntensity : 1 + zoomIntensity;
|
|
458
|
+
const newScale = Math.min(Math.max(0.1, state.scale * delta), 12);
|
|
459
|
+
const scaleFactor = newScale / state.scale - 1;
|
|
460
|
+
state.translateX -= mouseX * scaleFactor;
|
|
461
|
+
state.translateY -= mouseY * scaleFactor;
|
|
462
|
+
state.scale = newScale;
|
|
463
|
+
applyMermaidState(wrapper, state);
|
|
464
|
+
}, { passive: false });
|
|
465
|
+
|
|
466
|
+
wrapper.addEventListener('pointerdown', (e) => {
|
|
467
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
468
|
+
state.isPanning = true;
|
|
469
|
+
state.startX = e.clientX - state.translateX;
|
|
470
|
+
state.startY = e.clientY - state.translateY;
|
|
471
|
+
wrapper.setPointerCapture(e.pointerId);
|
|
472
|
+
wrapper.style.cursor = 'grabbing';
|
|
473
|
+
e.preventDefault();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
wrapper.addEventListener('pointermove', (e) => {
|
|
477
|
+
if (!state.isPanning) return;
|
|
478
|
+
state.translateX = e.clientX - state.startX;
|
|
479
|
+
state.translateY = e.clientY - state.startY;
|
|
480
|
+
applyMermaidState(wrapper, state);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const stopPanning = (e) => {
|
|
484
|
+
if (!state.isPanning) return;
|
|
485
|
+
state.isPanning = false;
|
|
486
|
+
try {
|
|
487
|
+
wrapper.releasePointerCapture(e.pointerId);
|
|
488
|
+
} catch {
|
|
489
|
+
// Ignore if pointer capture is not active
|
|
490
|
+
}
|
|
491
|
+
wrapper.style.cursor = 'grab';
|
|
492
|
+
};
|
|
493
|
+
wrapper.addEventListener('pointerup', stopPanning);
|
|
494
|
+
wrapper.addEventListener('pointercancel', stopPanning);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const scheduleMermaidInteraction = (wrapper, { maxAttempts = 12, delayMs = 80 } = {}) => {
|
|
498
|
+
let attempt = 0;
|
|
499
|
+
const check = () => {
|
|
500
|
+
if (wrapper.querySelector('svg')) {
|
|
501
|
+
initMermaidInteraction(wrapper);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (attempt >= maxAttempts) return;
|
|
505
|
+
attempt += 1;
|
|
506
|
+
setTimeout(check, delayMs);
|
|
507
|
+
};
|
|
508
|
+
check();
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const ensureMermaidInteractions = (root = document) => {
|
|
512
|
+
const wrappers = root.querySelectorAll('.mermaid-wrapper');
|
|
513
|
+
wrappers.forEach((wrapper) => {
|
|
514
|
+
if (wrapper.dataset.mermaidInteractive === 'true') return;
|
|
515
|
+
if (wrapper.querySelector('svg')) {
|
|
516
|
+
initMermaidInteraction(wrapper);
|
|
517
|
+
} else {
|
|
518
|
+
scheduleMermaidInteraction(wrapper);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const createMermaidContainer = (codeText) => {
|
|
524
|
+
mermaidCounter += 1;
|
|
525
|
+
const diagramId = `chat-mermaid-${mermaidCounter}`;
|
|
526
|
+
const codeKey = String(codeText || '').slice(0, 120);
|
|
527
|
+
|
|
528
|
+
const container = document.createElement('div');
|
|
529
|
+
container.className = 'mermaid-container';
|
|
530
|
+
|
|
531
|
+
const controls = document.createElement('div');
|
|
532
|
+
controls.className = 'mermaid-controls';
|
|
533
|
+
controls.innerHTML = `
|
|
534
|
+
<button type="button" data-action="reset" title="Reset zoom">Reset</button>
|
|
535
|
+
<button type="button" data-action="zoom-in" title="Zoom in">+</button>
|
|
536
|
+
<button type="button" data-action="zoom-out" title="Zoom out">−</button>
|
|
537
|
+
`;
|
|
538
|
+
|
|
539
|
+
const wrapper = document.createElement('div');
|
|
540
|
+
wrapper.id = diagramId;
|
|
541
|
+
wrapper.className = 'mermaid-wrapper';
|
|
542
|
+
wrapper.dataset.mermaidCode = codeText;
|
|
543
|
+
wrapper.dataset.mermaidRendered = 'false';
|
|
544
|
+
|
|
545
|
+
const pre = document.createElement('pre');
|
|
546
|
+
pre.className = 'mermaid';
|
|
547
|
+
pre.textContent = codeText;
|
|
548
|
+
wrapper.appendChild(pre);
|
|
549
|
+
|
|
550
|
+
container.appendChild(controls);
|
|
551
|
+
container.appendChild(wrapper);
|
|
552
|
+
|
|
553
|
+
controls.addEventListener('click', (event) => {
|
|
554
|
+
const btn = event.target.closest('button');
|
|
555
|
+
if (!btn) return;
|
|
556
|
+
const action = btn.getAttribute('data-action');
|
|
557
|
+
if (action === 'reset') {
|
|
558
|
+
resetMermaidZoom(wrapper.id);
|
|
559
|
+
} else if (action === 'zoom-in') {
|
|
560
|
+
zoomMermaidIn(wrapper.id);
|
|
561
|
+
} else if (action === 'zoom-out') {
|
|
562
|
+
zoomMermaidOut(wrapper.id);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
return { container, wrapper };
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const renderMermaidWrapper = async (wrapper) => {
|
|
570
|
+
if (!wrapper || wrapper.dataset.mermaidRendering === 'true') return;
|
|
571
|
+
const codeText = wrapper.dataset.mermaidCode || '';
|
|
572
|
+
if (!codeText.trim()) return;
|
|
573
|
+
const cacheHit = mermaidCache.get(codeText);
|
|
574
|
+
if (cacheHit) {
|
|
575
|
+
wrapper.innerHTML = cacheHit;
|
|
576
|
+
wrapper.dataset.mermaidRendered = 'true';
|
|
577
|
+
wrapper.dataset.mermaidRendering = 'false';
|
|
578
|
+
scheduleMermaidInteraction(wrapper);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const renderId = `${hashMermaidCode(codeText)}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
582
|
+
wrapper.dataset.mermaidRendering = 'true';
|
|
583
|
+
ensureMermaid();
|
|
584
|
+
let promise = mermaidRenderPromises.get(codeText);
|
|
585
|
+
if (!promise) {
|
|
586
|
+
promise = mermaid.render(renderId, codeText);
|
|
587
|
+
mermaidRenderPromises.set(codeText, promise);
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const result = await promise;
|
|
591
|
+
const svg = result && result.svg ? result.svg : '';
|
|
592
|
+
if (!svg) {
|
|
593
|
+
throw new Error('Mermaid render returned empty svg');
|
|
594
|
+
}
|
|
595
|
+
wrapper.innerHTML = svg;
|
|
596
|
+
if (result && typeof result.bindFunctions === 'function') {
|
|
597
|
+
result.bindFunctions(wrapper);
|
|
598
|
+
}
|
|
599
|
+
mermaidCache.set(codeText, svg);
|
|
600
|
+
wrapper.dataset.mermaidRendered = 'true';
|
|
601
|
+
wrapper.dataset.mermaidError = 'false';
|
|
602
|
+
scheduleMermaidInteraction(wrapper);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
wrapper.innerHTML = '<div class="mermaid-error">Invalid Mermaid diagram</div>';
|
|
605
|
+
wrapper.dataset.mermaidError = 'true';
|
|
606
|
+
console.warn('[mermaid] render failed', err);
|
|
607
|
+
} finally {
|
|
608
|
+
wrapper.dataset.mermaidRendering = 'false';
|
|
609
|
+
mermaidRenderPromises.delete(codeText);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const resetMermaidZoom = (id) => {
|
|
614
|
+
const state = mermaidStates[id];
|
|
615
|
+
const wrapper = document.getElementById(id);
|
|
616
|
+
if (!state || !wrapper) return;
|
|
617
|
+
fitSvgToWrapper(wrapper, state);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const zoomMermaidIn = (id) => {
|
|
621
|
+
const state = mermaidStates[id];
|
|
622
|
+
const wrapper = document.getElementById(id);
|
|
623
|
+
if (!state || !wrapper) return;
|
|
624
|
+
state.scale = Math.min(state.scale * 1.1, 12);
|
|
625
|
+
applyMermaidState(wrapper, state);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const zoomMermaidOut = (id) => {
|
|
629
|
+
const state = mermaidStates[id];
|
|
630
|
+
const wrapper = document.getElementById(id);
|
|
631
|
+
if (!state || !wrapper) return;
|
|
632
|
+
state.scale = Math.max(state.scale * 0.9, 0.1);
|
|
633
|
+
applyMermaidState(wrapper, state);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const isMermaidFenceClosed = (rawText) => {
|
|
637
|
+
if (!rawText) return true;
|
|
638
|
+
const openIndex = rawText.lastIndexOf('```mermaid');
|
|
639
|
+
if (openIndex === -1) return true;
|
|
640
|
+
const closeIndex = rawText.indexOf('```', openIndex + 3);
|
|
641
|
+
return closeIndex !== -1;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
let mermaidRenderTimer = null;
|
|
645
|
+
|
|
646
|
+
const upgradeMermaidBlocks = (root = document) => {
|
|
647
|
+
const blocks = root.querySelectorAll('pre > code.language-mermaid');
|
|
648
|
+
const wrappers = [];
|
|
649
|
+
blocks.forEach((code) => {
|
|
650
|
+
if (code.dataset.mermaidProcessed === 'true') return;
|
|
651
|
+
const markedRoot = code.closest('.marked');
|
|
652
|
+
const rawSource = markedRoot ? markedRoot.getAttribute('data-raw') : null;
|
|
653
|
+
if (rawSource && !isMermaidFenceClosed(rawSource)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
code.dataset.mermaidProcessed = 'true';
|
|
657
|
+
const pre = code.parentElement;
|
|
658
|
+
if (!pre) return;
|
|
659
|
+
const codeText = code.textContent || '';
|
|
660
|
+
const { container, wrapper } = createMermaidContainer(codeText);
|
|
661
|
+
pre.replaceWith(container);
|
|
662
|
+
const cachedSvg = mermaidCache.get(codeText);
|
|
663
|
+
if (cachedSvg) {
|
|
664
|
+
wrapper.innerHTML = cachedSvg;
|
|
665
|
+
wrapper.dataset.mermaidRendered = 'true';
|
|
666
|
+
scheduleMermaidInteraction(wrapper);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (wrapper.dataset.mermaidRendered === 'true' || wrapper.dataset.mermaidRendering === 'true') {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
wrappers.push(wrapper);
|
|
673
|
+
});
|
|
674
|
+
if (wrappers.length === 0) return;
|
|
675
|
+
if (mermaidRenderTimer) {
|
|
676
|
+
clearTimeout(mermaidRenderTimer);
|
|
677
|
+
}
|
|
678
|
+
mermaidRenderTimer = setTimeout(() => {
|
|
679
|
+
Promise.allSettled(wrappers.map(renderMermaidWrapper)).then(() => {
|
|
680
|
+
let didScroll = false;
|
|
681
|
+
if (!didScroll && window.__forceScrollToBottom) {
|
|
682
|
+
didScroll = true;
|
|
683
|
+
window.__forceScrollToBottom();
|
|
684
|
+
}
|
|
685
|
+
ensureMermaidInteractions(document);
|
|
686
|
+
});
|
|
687
|
+
}, 250);
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const observeMermaid = () => {
|
|
691
|
+
const target = document.getElementById('cards');
|
|
692
|
+
if (!target) return;
|
|
693
|
+
let upgradeTimer = null;
|
|
694
|
+
const scheduleUpgrade = () => {
|
|
695
|
+
if (upgradeTimer) return;
|
|
696
|
+
upgradeTimer = setTimeout(() => {
|
|
697
|
+
upgradeTimer = null;
|
|
698
|
+
upgradeMermaidBlocks(target);
|
|
699
|
+
}, 120);
|
|
700
|
+
};
|
|
701
|
+
const observer = new MutationObserver((mutations) => {
|
|
702
|
+
for (const mutation of mutations) {
|
|
703
|
+
if (mutation.type === 'characterData') {
|
|
704
|
+
const parent = mutation.target && mutation.target.parentElement;
|
|
705
|
+
const markedRoot = parent ? parent.closest('.marked') : null;
|
|
706
|
+
if (markedRoot) {
|
|
707
|
+
const rawText = markedRoot.getAttribute('data-raw') || '';
|
|
708
|
+
if (!isMermaidFenceClosed(rawText)) {
|
|
709
|
+
markedRoot.dataset.mermaidDirty = 'true';
|
|
710
|
+
} else if (markedRoot.dataset.mermaidDirty === 'true') {
|
|
711
|
+
markedRoot.dataset.mermaidDirty = 'false';
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
scheduleUpgrade();
|
|
717
|
+
});
|
|
718
|
+
observer.observe(target, {
|
|
719
|
+
childList: true,
|
|
720
|
+
subtree: true,
|
|
721
|
+
characterData: true,
|
|
722
|
+
});
|
|
723
|
+
upgradeMermaidBlocks(target);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
window.__upgradeMermaidBlocks = upgradeMermaidBlocks;
|
|
727
|
+
|
|
728
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
729
|
+
observeMermaid();
|
|
730
|
+
setTimeout(() => upgradeMermaidBlocks(document), 0);
|
|
731
|
+
setTimeout(() => ensureMermaidInteractions(document), 0);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
735
|
+
upgradeMermaidBlocks(event.target || document);
|
|
736
|
+
ensureMermaidInteractions(event.target || document);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (window.htmx && typeof window.htmx.onLoad === 'function') {
|
|
740
|
+
window.htmx.onLoad((root) => {
|
|
741
|
+
upgradeMermaidBlocks(root || document);
|
|
742
|
+
ensureMermaidInteractions(root || document);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|