synabun 1.3.0
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/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Utilities
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
// Shared helper functions used across UI components.
|
|
5
|
+
|
|
6
|
+
import { t, getLocale } from './i18n.js';
|
|
7
|
+
|
|
8
|
+
// ─── Data Normalization ─────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ensure every node has a valid payload.category.
|
|
12
|
+
* Nodes with null/undefined/empty category get 'uncategorized'.
|
|
13
|
+
* Mutates in place and returns the array for chaining.
|
|
14
|
+
* @param {Array} nodes
|
|
15
|
+
* @returns {Array}
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeNodes(nodes) {
|
|
18
|
+
for (const n of nodes) {
|
|
19
|
+
if (n.payload && !n.payload.category) {
|
|
20
|
+
n.payload.category = 'uncategorized';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return nodes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── String Utilities ────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Truncate a string to a maximum length, adding ellipsis if truncated.
|
|
30
|
+
* @param {string} str
|
|
31
|
+
* @param {number} len
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function truncate(str, len) {
|
|
35
|
+
if (!str) return '';
|
|
36
|
+
return str.length > len ? str.slice(0, len) + '...' : str;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Escape HTML entities using a DOM element for safety.
|
|
41
|
+
* @param {string} str
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function escapeHtml(str) {
|
|
45
|
+
const div = document.createElement('div');
|
|
46
|
+
div.textContent = str;
|
|
47
|
+
return div.innerHTML;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Date / Time Formatting ──────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format a relative time string from an ISO date (e.g. "5m ago", "3 days ago").
|
|
54
|
+
* Used by the trash panel to show when items were trashed.
|
|
55
|
+
* @param {string} isoDate
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function formatTrashAge(isoDate) {
|
|
59
|
+
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
60
|
+
const mins = Math.floor(diffMs / 60000);
|
|
61
|
+
if (mins < 1) return t('time.justNow');
|
|
62
|
+
if (mins < 60) return t('time.minutesAgo', { n: mins });
|
|
63
|
+
const hours = Math.floor(mins / 60);
|
|
64
|
+
if (hours < 24) return t('time.hoursAgo', { n: hours });
|
|
65
|
+
const days = Math.floor(hours / 24);
|
|
66
|
+
if (days === 1) return t('time.dayAgo');
|
|
67
|
+
if (days < 30) return t('time.daysAgo', { n: days });
|
|
68
|
+
const months = Math.floor(days / 30);
|
|
69
|
+
return months === 1 ? t('time.monthAgo') : t('time.monthsAgo', { n: months });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Content Formatting ─────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert markdown-like memory content into styled HTML.
|
|
76
|
+
*
|
|
77
|
+
* Supports:
|
|
78
|
+
* - Headings (## / ### / ####)
|
|
79
|
+
* - Code blocks (```)
|
|
80
|
+
* - Lists (- / * / numbered)
|
|
81
|
+
* - Bold (**text**), italic (*text*), inline code (`code`)
|
|
82
|
+
* - Em-dash description splits ( — description)
|
|
83
|
+
* - Key: Value patterns (label followed by 2+ spaces then value)
|
|
84
|
+
* - Heading-like lines ending with colon
|
|
85
|
+
* - Separator lines (---, ===, ___)
|
|
86
|
+
*
|
|
87
|
+
* @param {string} text — raw memory content
|
|
88
|
+
* @returns {string} — formatted HTML string
|
|
89
|
+
*/
|
|
90
|
+
export function formatMemoryContent(text) {
|
|
91
|
+
if (!text) return '';
|
|
92
|
+
|
|
93
|
+
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
94
|
+
|
|
95
|
+
// Inline formatting: `code`, **bold**, *italic*, em-dash descriptions
|
|
96
|
+
function inlineFmt(s) {
|
|
97
|
+
let out = esc(s);
|
|
98
|
+
// Backtick code spans
|
|
99
|
+
out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
100
|
+
// Bold
|
|
101
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
102
|
+
// Italic
|
|
103
|
+
out = out.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
|
|
104
|
+
// Em-dash description split (` — ` becomes dimmed after)
|
|
105
|
+
out = out.replace(/ — (.+)$/, ' <span class="kv-desc">\u2014 $1</span>');
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = text.split('\n');
|
|
110
|
+
let html = '';
|
|
111
|
+
let inList = false;
|
|
112
|
+
let inCode = false;
|
|
113
|
+
|
|
114
|
+
function closeList() { if (inList) { html += '</ul>'; inList = false; } }
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
const raw = lines[i];
|
|
118
|
+
const trimmed = raw.trim();
|
|
119
|
+
|
|
120
|
+
// Empty line
|
|
121
|
+
if (!trimmed) {
|
|
122
|
+
if (inCode) { html += '\n'; continue; }
|
|
123
|
+
closeList();
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Code block fences
|
|
128
|
+
if (trimmed.startsWith('```')) {
|
|
129
|
+
if (inCode) { html += '</div>'; inCode = false; }
|
|
130
|
+
else { closeList(); inCode = true; html += '<div class="mc-codeblock">'; }
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (inCode) { html += esc(raw) + '\n'; continue; }
|
|
134
|
+
|
|
135
|
+
// Markdown headings ## / ### / ####
|
|
136
|
+
const headingMatch = trimmed.match(/^(#{1,4})\s+(.+)$/);
|
|
137
|
+
if (headingMatch) {
|
|
138
|
+
closeList();
|
|
139
|
+
const level = headingMatch[1].length;
|
|
140
|
+
const cls = level <= 2 ? 'mc-h2' : level === 3 ? 'mc-h3' : 'mc-h4';
|
|
141
|
+
html += `<div class="${cls}">${inlineFmt(headingMatch[2])}</div>`;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Separator lines
|
|
146
|
+
if (/^[-=_]{3,}$/.test(trimmed)) {
|
|
147
|
+
closeList();
|
|
148
|
+
html += '<div class="mc-divider"></div>';
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Standalone backtick line (full line is a code key pattern)
|
|
153
|
+
if (trimmed.startsWith('`') && trimmed.endsWith('`') && !trimmed.includes(' ')) {
|
|
154
|
+
closeList();
|
|
155
|
+
html += `<p>${inlineFmt(trimmed)}</p>`;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Backtick line followed by em-dash description
|
|
160
|
+
if (/^`[^`]+`\s+—\s+/.test(trimmed)) {
|
|
161
|
+
closeList();
|
|
162
|
+
html += `<p>${inlineFmt(trimmed)}</p>`;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// List items (- or * or numbered)
|
|
167
|
+
if (/^[-*\u2022]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) {
|
|
168
|
+
if (!inList) { html += '<ul>'; inList = true; }
|
|
169
|
+
const content = trimmed.replace(/^[-*\u2022]\s+/, '').replace(/^\d+[.)]\s+/, '');
|
|
170
|
+
html += `<li>${inlineFmt(content)}</li>`;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Heading-like: short line ending with colon (not containing backticks — those are code patterns)
|
|
175
|
+
if (trimmed.endsWith(':') && trimmed.length < 50 && !trimmed.includes('`') && !/\s{2,}/.test(trimmed)) {
|
|
176
|
+
closeList();
|
|
177
|
+
html += `<div class="mc-h3">${inlineFmt(trimmed.slice(0, -1))}</div>`;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Key: Value (label < 25 chars, then colon + space + value)
|
|
182
|
+
const kvMatch = trimmed.match(/^([A-Za-z][\w\s/().,-]{0,24}?)\s{2,}(.+)$/);
|
|
183
|
+
if (kvMatch) {
|
|
184
|
+
closeList();
|
|
185
|
+
html += `<div class="mc-kv"><span class="mc-kv-key">${esc(kvMatch[1])}</span> <span class="mc-kv-val">${inlineFmt(kvMatch[2])}</span></div>`;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Regular paragraph
|
|
190
|
+
closeList();
|
|
191
|
+
html += `<p>${inlineFmt(trimmed)}</p>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
closeList();
|
|
195
|
+
if (inCode) html += '</div>';
|
|
196
|
+
return html;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Export ──────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Convert a memory node to markdown and trigger a file download.
|
|
203
|
+
* @param {{ id: string, payload: Object }} node — graph node with payload
|
|
204
|
+
*/
|
|
205
|
+
export function exportMemoryAsMarkdown(node) {
|
|
206
|
+
const p = node.payload;
|
|
207
|
+
const lines = [];
|
|
208
|
+
|
|
209
|
+
// Title — first meaningful line of content, or category
|
|
210
|
+
const firstLine = (p.content || '').split('\n').find(l => l.trim()) || p.category || 'Memory';
|
|
211
|
+
const title = firstLine.length > 80 ? firstLine.slice(0, 80) + '...' : firstLine;
|
|
212
|
+
lines.push(`# ${title}`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
|
|
215
|
+
// Metadata table
|
|
216
|
+
const dateFmt = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
217
|
+
const locale = getLocale();
|
|
218
|
+
lines.push(`| ${t('export.fieldLabel')} | ${t('export.valueLabel')} |`);
|
|
219
|
+
lines.push('|-------|-------|');
|
|
220
|
+
lines.push(`| ${t('export.categoryLabel')} | ${p.category}${p.subcategory ? ' / ' + p.subcategory : ''} |`);
|
|
221
|
+
lines.push(`| ${t('export.importanceLabel')} | ${p.importance || 5}/10 |`);
|
|
222
|
+
if (p.project) lines.push(`| ${t('export.projectLabel')} | ${p.project} |`);
|
|
223
|
+
if (p.source) lines.push(`| ${t('export.sourceLabel')} | ${p.source} |`);
|
|
224
|
+
if (p.created_at) lines.push(`| ${t('export.createdLabel')} | ${new Date(p.created_at).toLocaleDateString(locale, dateFmt)} |`);
|
|
225
|
+
if (p.updated_at) lines.push(`| ${t('export.updatedLabel')} | ${new Date(p.updated_at).toLocaleDateString(locale, dateFmt)} |`);
|
|
226
|
+
lines.push(`| ${t('export.idLabel')} | \`${node.id}\` |`);
|
|
227
|
+
lines.push('');
|
|
228
|
+
|
|
229
|
+
// Tags
|
|
230
|
+
const tags = (p.tags && Array.isArray(p.tags) && p.tags.length) ? p.tags : null;
|
|
231
|
+
if (tags) {
|
|
232
|
+
lines.push(`**${t('export.tagsLabel')}** ${tags.map(tg => '`' + tg + '`').join(' ')}`);
|
|
233
|
+
lines.push('');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Content
|
|
237
|
+
lines.push(`## ${t('export.contentHeading')}`);
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(p.content || t('export.emptyContent'));
|
|
240
|
+
lines.push('');
|
|
241
|
+
|
|
242
|
+
// Related files
|
|
243
|
+
if (p.related_files && p.related_files.length) {
|
|
244
|
+
lines.push(`## ${t('export.relatedFilesHeading')}`);
|
|
245
|
+
lines.push('');
|
|
246
|
+
p.related_files.forEach(f => lines.push(`- \`${f}\``));
|
|
247
|
+
lines.push('');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const md = lines.join('\n');
|
|
251
|
+
|
|
252
|
+
// Build filename from category + truncated content
|
|
253
|
+
const slug = (p.category + (p.subcategory ? '-' + p.subcategory : '')).replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
|
254
|
+
const contentSlug = (p.content || 'memory').split('\n')[0].trim().slice(0, 40).replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase();
|
|
255
|
+
const filename = `${slug}--${contentSlug || 'memory'}.md`;
|
|
256
|
+
|
|
257
|
+
// Trigger download
|
|
258
|
+
const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
|
|
259
|
+
const url = URL.createObjectURL(blob);
|
|
260
|
+
const a = document.createElement('a');
|
|
261
|
+
a.href = url;
|
|
262
|
+
a.download = filename;
|
|
263
|
+
document.body.appendChild(a);
|
|
264
|
+
a.click();
|
|
265
|
+
document.body.removeChild(a);
|
|
266
|
+
URL.revokeObjectURL(url);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Browser Frame Renderer ─────────────
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Creates a serialized frame renderer that prevents decode backlog.
|
|
273
|
+
* Accepts raw Blob/ArrayBuffer (binary WS) or base64 string (legacy fallback).
|
|
274
|
+
* Only one decode runs at a time; newer frames replace queued ones.
|
|
275
|
+
* Draws are gated to requestAnimationFrame for vsync-aligned rendering.
|
|
276
|
+
*
|
|
277
|
+
* @param {HTMLCanvasElement} canvas
|
|
278
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
279
|
+
* @returns {{ render(data: Blob|ArrayBuffer|string): void, destroy(): void }}
|
|
280
|
+
*/
|
|
281
|
+
export function createFrameRenderer(canvas, ctx) {
|
|
282
|
+
let pending = null;
|
|
283
|
+
let rendering = false;
|
|
284
|
+
let destroyed = false;
|
|
285
|
+
const useBitmap = typeof createImageBitmap === 'function';
|
|
286
|
+
const FRAME_INTERVAL = 1000 / 30; // 30fps cap
|
|
287
|
+
let lastFrameTime = 0;
|
|
288
|
+
|
|
289
|
+
function flush() {
|
|
290
|
+
if (destroyed || rendering || !pending) return;
|
|
291
|
+
const now = performance.now();
|
|
292
|
+
if (now - lastFrameTime < FRAME_INTERVAL) {
|
|
293
|
+
// Drop frame — too soon since last render
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
lastFrameTime = now;
|
|
297
|
+
rendering = true;
|
|
298
|
+
const data = pending;
|
|
299
|
+
pending = null;
|
|
300
|
+
|
|
301
|
+
// Convert input to a Blob for createImageBitmap
|
|
302
|
+
let blob;
|
|
303
|
+
if (data instanceof Blob) {
|
|
304
|
+
blob = data;
|
|
305
|
+
} else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
306
|
+
blob = new Blob([data], { type: 'image/jpeg' });
|
|
307
|
+
} else {
|
|
308
|
+
// Legacy base64 string fallback
|
|
309
|
+
const bin = atob(data);
|
|
310
|
+
const bytes = new Uint8Array(bin.length);
|
|
311
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
312
|
+
blob = new Blob([bytes], { type: 'image/jpeg' });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (useBitmap) {
|
|
316
|
+
createImageBitmap(blob).then(bmp => {
|
|
317
|
+
if (destroyed) { bmp.close(); return; }
|
|
318
|
+
if (canvas.width !== bmp.width || canvas.height !== bmp.height) {
|
|
319
|
+
canvas.width = bmp.width;
|
|
320
|
+
canvas.height = bmp.height;
|
|
321
|
+
}
|
|
322
|
+
ctx.drawImage(bmp, 0, 0);
|
|
323
|
+
bmp.close();
|
|
324
|
+
rendering = false;
|
|
325
|
+
if (pending) flush();
|
|
326
|
+
}).catch(() => {
|
|
327
|
+
if (destroyed) return;
|
|
328
|
+
rendering = false;
|
|
329
|
+
if (pending) flush();
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
// Fallback for browsers without createImageBitmap
|
|
333
|
+
const url = URL.createObjectURL(blob);
|
|
334
|
+
const img = new Image();
|
|
335
|
+
img.onload = () => {
|
|
336
|
+
if (destroyed) { URL.revokeObjectURL(url); return; }
|
|
337
|
+
if (canvas.width !== img.naturalWidth || canvas.height !== img.naturalHeight) {
|
|
338
|
+
canvas.width = img.naturalWidth;
|
|
339
|
+
canvas.height = img.naturalHeight;
|
|
340
|
+
}
|
|
341
|
+
ctx.drawImage(img, 0, 0);
|
|
342
|
+
URL.revokeObjectURL(url);
|
|
343
|
+
rendering = false;
|
|
344
|
+
if (pending) flush();
|
|
345
|
+
};
|
|
346
|
+
img.onerror = () => {
|
|
347
|
+
URL.revokeObjectURL(url);
|
|
348
|
+
if (destroyed) return;
|
|
349
|
+
rendering = false;
|
|
350
|
+
if (pending) flush();
|
|
351
|
+
};
|
|
352
|
+
img.src = url;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
render(data) { pending = data; flush(); },
|
|
358
|
+
destroy() { destroyed = true; pending = null; },
|
|
359
|
+
};
|
|
360
|
+
}
|