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,147 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Internationalization
|
|
3
|
+
// Lightweight runtime i18n: loads JSON translations at boot,
|
|
4
|
+
// exposes t() / tp() for all modules.
|
|
5
|
+
// ═══════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { storage } from './storage.js';
|
|
8
|
+
|
|
9
|
+
let _messages = {};
|
|
10
|
+
let _locale = 'en';
|
|
11
|
+
let _ready = false;
|
|
12
|
+
|
|
13
|
+
// ── Supported locales (grows as translations are added) ──
|
|
14
|
+
export const SUPPORTED_LOCALES = ['en'];
|
|
15
|
+
|
|
16
|
+
// ── Locale metadata (for language switcher UI) ──
|
|
17
|
+
export const LOCALE_NAMES = {
|
|
18
|
+
en: 'English',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════
|
|
22
|
+
// CORE API
|
|
23
|
+
// ═══════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a dot-separated key path against a nested object.
|
|
27
|
+
* t('settings.server.status') → messages.settings.server.status
|
|
28
|
+
*/
|
|
29
|
+
function resolve(obj, path) {
|
|
30
|
+
return path.split('.').reduce((o, k) => o?.[k], obj);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Translate a key, with optional parameter interpolation.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} key Dot-separated key path (e.g. 'nav.view')
|
|
37
|
+
* @param {Object} [params] Interpolation values: "Hello {name}" + { name: 'Dan' }
|
|
38
|
+
* @returns {string} Translated string, or the key itself if missing
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* t('common.save') // "Save"
|
|
42
|
+
* t('explorer.importanceTooltip', { n: 8 }) // "Importance: 8/10"
|
|
43
|
+
*/
|
|
44
|
+
export function t(key, params) {
|
|
45
|
+
let val = resolve(_messages, key);
|
|
46
|
+
if (val === undefined) {
|
|
47
|
+
if (_ready) console.warn(`[i18n] Missing key: ${key}`);
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
if (params && typeof val === 'string') {
|
|
51
|
+
val = val.replace(/\{(\w+)\}/g, (_, k) => (params[k] != null ? params[k] : `{${k}}`));
|
|
52
|
+
}
|
|
53
|
+
return val;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pluralization helper. Resolves to key.one or key.other based on count.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} key Base key path (e.g. 'search.resultCount')
|
|
60
|
+
* @param {number} count The number to pluralize on
|
|
61
|
+
* @param {Object} [params] Extra interpolation values (count is auto-included)
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // en.json: { "search": { "resultCount": { "one": "1 result", "other": "{count} results" } } }
|
|
66
|
+
* tp('search.resultCount', 1) // "1 result"
|
|
67
|
+
* tp('search.resultCount', 42) // "42 results"
|
|
68
|
+
*/
|
|
69
|
+
export function tp(key, count, params) {
|
|
70
|
+
const plural = count === 1 ? 'one' : 'other';
|
|
71
|
+
return t(`${key}.${plural}`, { count, ...params });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the current locale code (e.g. 'en', 'es').
|
|
76
|
+
* Use this for Intl APIs: date.toLocaleDateString(getLocale(), ...)
|
|
77
|
+
*/
|
|
78
|
+
export function getLocale() {
|
|
79
|
+
return _locale;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if translations have been loaded.
|
|
84
|
+
*/
|
|
85
|
+
export function isReady() {
|
|
86
|
+
return _ready;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ═══════════════════════════════════════════
|
|
90
|
+
// INITIALIZATION
|
|
91
|
+
// ═══════════════════════════════════════════
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect the user's preferred locale.
|
|
95
|
+
* Priority: localStorage explicit choice → browser language → 'en'
|
|
96
|
+
*/
|
|
97
|
+
function detectLocale() {
|
|
98
|
+
// 1. Explicit user preference
|
|
99
|
+
const saved = storage.getItem('synabun-locale');
|
|
100
|
+
if (saved && SUPPORTED_LOCALES.includes(saved)) return saved;
|
|
101
|
+
// 2. Browser language
|
|
102
|
+
const browser = (navigator.language || 'en').slice(0, 2).toLowerCase();
|
|
103
|
+
if (SUPPORTED_LOCALES.includes(browser)) return browser;
|
|
104
|
+
// 3. Default
|
|
105
|
+
return 'en';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Load translations. Call this ONCE at app boot, BEFORE any UI modules init.
|
|
110
|
+
* Typically the first thing in the entry point's top-level await.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} [locale] Force a specific locale (skips detection)
|
|
113
|
+
*/
|
|
114
|
+
export async function initI18n(locale) {
|
|
115
|
+
_locale = locale || detectLocale();
|
|
116
|
+
try {
|
|
117
|
+
const resp = await fetch(`/i18n/${_locale}.json`);
|
|
118
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
119
|
+
_messages = await resp.json();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// Fallback to English if the requested locale file fails
|
|
122
|
+
if (_locale !== 'en') {
|
|
123
|
+
console.warn(`[i18n] Failed to load '${_locale}', falling back to 'en'`);
|
|
124
|
+
_locale = 'en';
|
|
125
|
+
try {
|
|
126
|
+
const resp = await fetch('/i18n/en.json');
|
|
127
|
+
if (resp.ok) _messages = await resp.json();
|
|
128
|
+
} catch {
|
|
129
|
+
console.error('[i18n] Failed to load English fallback');
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.error('[i18n] Failed to load English translations:', err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
_ready = true;
|
|
136
|
+
document.documentElement.lang = _locale;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Switch to a different locale. Persists to localStorage and reloads.
|
|
141
|
+
* @param {string} locale
|
|
142
|
+
*/
|
|
143
|
+
export function setLocale(locale) {
|
|
144
|
+
if (!SUPPORTED_LOCALES.includes(locale)) return;
|
|
145
|
+
storage.setItem('synabun-locale', locale);
|
|
146
|
+
window.location.reload();
|
|
147
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// REGISTRY — feature registry for variant capabilities
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Each variant (3D / 2D) registers its identity, capabilities, and UI
|
|
6
|
+
// extensions on boot. Shared UI reads the registry to decide what to render.
|
|
7
|
+
|
|
8
|
+
const _registry = {
|
|
9
|
+
variant: null, // '3d' | '2d' | future variants
|
|
10
|
+
capabilities: new Set(), // e.g. 'camera-hud', 'minimap', 'hulls', 'lasso', 'controls-panel'
|
|
11
|
+
navbarButtons: [], // { zone, order, html, init } (legacy, kept for compat)
|
|
12
|
+
menuItems: [], // { menu, order, type, id, label, shortcut?, init }
|
|
13
|
+
helpSections: [], // { order, html }
|
|
14
|
+
settingsTabs: [], // { id, label, icon, builder, afterRender }
|
|
15
|
+
keyboardShortcuts: [], // { key, description, handler }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ── Registration API ──
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register the active variant and its capabilities.
|
|
22
|
+
* Called once at variant boot time.
|
|
23
|
+
* @param {{ variant: string, capabilities?: string[] }} config
|
|
24
|
+
*/
|
|
25
|
+
export function registerVariant(config) {
|
|
26
|
+
_registry.variant = config.variant;
|
|
27
|
+
_registry.capabilities = new Set(config.capabilities || []);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register an extra button for the top navbar.
|
|
32
|
+
* @param {{ zone: string, order: number, html: string, init: Function }} spec
|
|
33
|
+
*/
|
|
34
|
+
export function registerNavbarButton(spec) {
|
|
35
|
+
_registry.navbarButtons.push(spec);
|
|
36
|
+
_registry.navbarButtons.sort((a, b) => a.order - b.order);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register a menu item for the OS-style menubar.
|
|
41
|
+
* @param {{ menu: string, order: number, type: string, id: string, label: string, shortcut?: string, init?: Function }} spec
|
|
42
|
+
*/
|
|
43
|
+
export function registerMenuItem(spec) {
|
|
44
|
+
_registry.menuItems.push(spec);
|
|
45
|
+
_registry.menuItems.sort((a, b) => a.order - b.order);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register an extra section for the help modal.
|
|
50
|
+
* @param {{ order: number, html: string }} spec
|
|
51
|
+
*/
|
|
52
|
+
export function registerHelpSection(spec) {
|
|
53
|
+
_registry.helpSections.push(spec);
|
|
54
|
+
_registry.helpSections.sort((a, b) => a.order - b.order);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register an extra tab for the settings panel.
|
|
59
|
+
* @param {{ id: string, label: string, icon: string, builder: Function, afterRender?: Function }} spec
|
|
60
|
+
*/
|
|
61
|
+
export function registerSettingsTab(spec) {
|
|
62
|
+
_registry.settingsTabs.push(spec);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a keyboard shortcut (for help display and central handler).
|
|
67
|
+
* @param {{ key: string, description: string, handler: Function }} spec
|
|
68
|
+
*/
|
|
69
|
+
export function registerKeyboardShortcut(spec) {
|
|
70
|
+
_registry.keyboardShortcuts.push(spec);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Query API ──
|
|
74
|
+
|
|
75
|
+
export function getVariant() { return _registry.variant; }
|
|
76
|
+
export function hasCapability(name) { return _registry.capabilities.has(name); }
|
|
77
|
+
export function getNavbarButtons() { return _registry.navbarButtons; }
|
|
78
|
+
export function getMenuItems() { return _registry.menuItems; }
|
|
79
|
+
export function getHelpSections() { return _registry.helpSections; }
|
|
80
|
+
export function getSettingsTabs() { return _registry.settingsTabs; }
|
|
81
|
+
export function getKeyboardShortcuts() { return _registry.keyboardShortcuts; }
|
|
82
|
+
export function getRegistry() { return _registry; }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// STATE — shared reactive state store + event bus
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// All UI components read/write this object.
|
|
6
|
+
// Variants (3D / 2D) listen for changes via the event bus.
|
|
7
|
+
// Variant-specific state (graph, bloom, frustum, etc.) stays in the variant.
|
|
8
|
+
|
|
9
|
+
export const state = {
|
|
10
|
+
// ── Data (loaded from API) ──
|
|
11
|
+
allNodes: [],
|
|
12
|
+
allLinks: [],
|
|
13
|
+
categoryMetadata: {}, // { name: { description, color, parent, is_parent, logo } }
|
|
14
|
+
allCategoryNames: [], // ordered list of category names from server
|
|
15
|
+
categoryDescriptions: {},// { name: description }
|
|
16
|
+
|
|
17
|
+
// ── Selection ──
|
|
18
|
+
selectedNodeId: null,
|
|
19
|
+
hoveredNodeId: null,
|
|
20
|
+
focusedNodeId: null,
|
|
21
|
+
|
|
22
|
+
// ── Search ──
|
|
23
|
+
searchResults: null, // null = no active search, Set of IDs = active search
|
|
24
|
+
searchQuery: '',
|
|
25
|
+
|
|
26
|
+
// ── Category visibility ──
|
|
27
|
+
activeCategories: new Set(),
|
|
28
|
+
|
|
29
|
+
// ── Multi-select ──
|
|
30
|
+
multiSelected: new Set(),
|
|
31
|
+
|
|
32
|
+
// ── UI toggles ──
|
|
33
|
+
labelsVisible: true,
|
|
34
|
+
labelSizeMultiplier: 1.0,
|
|
35
|
+
|
|
36
|
+
// ── Link mode ──
|
|
37
|
+
linkMode: 'off', // 'off' | 'intra' | 'all'
|
|
38
|
+
linkTypeFilter: 'all',
|
|
39
|
+
|
|
40
|
+
// ── Grid snap ──
|
|
41
|
+
gridSnap: false,
|
|
42
|
+
gridSize: 20,
|
|
43
|
+
|
|
44
|
+
// ── Terminal links ──
|
|
45
|
+
linkedSessionIds: new Set(), // session IDs currently in an active link
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
// ═══════════════════════════════════════════
|
|
50
|
+
// EVENT BUS — lightweight pub/sub
|
|
51
|
+
// ═══════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
const _listeners = {};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emit an event to all registered listeners.
|
|
57
|
+
* @param {string} event Event name (e.g. 'selection-changed', 'data-loaded')
|
|
58
|
+
* @param {*} [data] Payload passed to each listener
|
|
59
|
+
*/
|
|
60
|
+
export function emit(event, data) {
|
|
61
|
+
const fns = _listeners[event];
|
|
62
|
+
if (fns) fns.forEach(fn => fn(data));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
67
|
+
* @param {string} event Event name
|
|
68
|
+
* @param {Function} fn Listener
|
|
69
|
+
* @returns {Function} Unsubscribe function
|
|
70
|
+
*/
|
|
71
|
+
export function on(event, fn) {
|
|
72
|
+
if (!_listeners[event]) _listeners[event] = [];
|
|
73
|
+
_listeners[event].push(fn);
|
|
74
|
+
return () => { _listeners[event] = _listeners[event].filter(f => f !== fn); };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Remove a specific listener for an event.
|
|
79
|
+
* @param {string} event Event name
|
|
80
|
+
* @param {Function} fn Listener to remove
|
|
81
|
+
*/
|
|
82
|
+
export function off(event, fn) {
|
|
83
|
+
if (_listeners[event]) {
|
|
84
|
+
_listeners[event] = _listeners[event].filter(f => f !== fn);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// STORAGE — Server-synced persistent state
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Drop-in replacement for localStorage calls.
|
|
6
|
+
// Data persists to data/ui-state.json on the server.
|
|
7
|
+
//
|
|
8
|
+
// Self-hydrating: the module-level `await` ensures any module that
|
|
9
|
+
// imports from storage.js gets a populated cache before its own
|
|
10
|
+
// body executes. This is critical for modules like gfx.js that call
|
|
11
|
+
// loadGfxConfig() at import time.
|
|
12
|
+
//
|
|
13
|
+
// The in-memory cache ensures synchronous reads (zero latency).
|
|
14
|
+
// Writes are async but invisible to callers.
|
|
15
|
+
|
|
16
|
+
const _cache = {};
|
|
17
|
+
let _dirty = {};
|
|
18
|
+
let _flushTimer = null;
|
|
19
|
+
let _hydrated = false;
|
|
20
|
+
const DEBOUNCE_MS = 500;
|
|
21
|
+
|
|
22
|
+
// ── Hydrate from server ──────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function _hydrate() {
|
|
25
|
+
let serverData = null;
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch('/api/ui-state');
|
|
28
|
+
if (res.ok) serverData = await res.json();
|
|
29
|
+
} catch { /* server unreachable */ }
|
|
30
|
+
|
|
31
|
+
if (serverData && Object.keys(serverData).filter(k => !k.startsWith('_')).length > 0) {
|
|
32
|
+
// Normal boot: hydrate from server file
|
|
33
|
+
for (const [key, value] of Object.entries(serverData)) {
|
|
34
|
+
if (key.startsWith('_')) continue;
|
|
35
|
+
_cache[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// First run or server down: import from localStorage
|
|
39
|
+
console.info('[storage] Migrating localStorage to server...');
|
|
40
|
+
_migrateFromLocalStorage();
|
|
41
|
+
// Build dirty set from everything we imported
|
|
42
|
+
for (const [key, val] of Object.entries(_cache)) {
|
|
43
|
+
try { _dirty[key] = JSON.parse(val); } catch { _dirty[key] = val; }
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(_dirty).length > 0) {
|
|
46
|
+
await _flushToServer();
|
|
47
|
+
console.info('[storage] Migration complete. Data saved to data/ui-state.json');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
_hydrated = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Self-hydrate at module load — blocks dependents until cache is ready
|
|
54
|
+
await _hydrate();
|
|
55
|
+
|
|
56
|
+
// ── Drop-in API (mirrors localStorage) ───────────────
|
|
57
|
+
|
|
58
|
+
export const storage = {
|
|
59
|
+
getItem(key) {
|
|
60
|
+
return _cache[key] ?? null;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
setItem(key, value) {
|
|
64
|
+
const strValue = String(value);
|
|
65
|
+
if (_cache[key] === strValue) return; // no-op if unchanged
|
|
66
|
+
_cache[key] = strValue;
|
|
67
|
+
// Parse back to native type for the server (avoids double-encoding JSON)
|
|
68
|
+
try { _dirty[key] = JSON.parse(strValue); } catch { _dirty[key] = strValue; }
|
|
69
|
+
_scheduleDebouncedFlush();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
removeItem(key) {
|
|
73
|
+
if (!(key in _cache)) return;
|
|
74
|
+
delete _cache[key];
|
|
75
|
+
_dirty[key] = null; // null signals deletion to the server
|
|
76
|
+
_scheduleDebouncedFlush();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ── Debounced flush to server ────────────────────────
|
|
81
|
+
|
|
82
|
+
function _scheduleDebouncedFlush() {
|
|
83
|
+
if (_flushTimer) clearTimeout(_flushTimer);
|
|
84
|
+
_flushTimer = setTimeout(_flushToServer, DEBOUNCE_MS);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function _flushToServer() {
|
|
88
|
+
const payload = { ..._dirty };
|
|
89
|
+
_dirty = {};
|
|
90
|
+
_flushTimer = null;
|
|
91
|
+
if (Object.keys(payload).length === 0) return;
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch('/api/ui-state', {
|
|
94
|
+
method: 'PATCH',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify(payload),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[storage] Flush failed:', err.message, '— will retry');
|
|
101
|
+
// Re-queue failed keys (don't overwrite newer dirty values)
|
|
102
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
103
|
+
if (!(k in _dirty)) _dirty[k] = v;
|
|
104
|
+
}
|
|
105
|
+
_scheduleDebouncedFlush();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Flush on page unload (sendBeacon for reliability) ──
|
|
110
|
+
|
|
111
|
+
window.addEventListener('beforeunload', () => {
|
|
112
|
+
if (_flushTimer) {
|
|
113
|
+
clearTimeout(_flushTimer);
|
|
114
|
+
_flushTimer = null;
|
|
115
|
+
}
|
|
116
|
+
// Merge any pending dirty keys
|
|
117
|
+
const payload = { ..._dirty };
|
|
118
|
+
_dirty = {};
|
|
119
|
+
if (Object.keys(payload).length === 0) return;
|
|
120
|
+
navigator.sendBeacon(
|
|
121
|
+
'/api/ui-state',
|
|
122
|
+
new Blob([JSON.stringify(payload)], { type: 'application/json' })
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── Migration helper ─────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function _migrateFromLocalStorage() {
|
|
129
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
130
|
+
const key = localStorage.key(i);
|
|
131
|
+
if (key && (key.startsWith('neural-') || key.startsWith('synabun-'))) {
|
|
132
|
+
_cache[key] = localStorage.getItem(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Utilities ────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** Force an immediate flush (useful for testing or explicit save actions) */
|
|
140
|
+
export async function flushStorage() {
|
|
141
|
+
if (_flushTimer) {
|
|
142
|
+
clearTimeout(_flushTimer);
|
|
143
|
+
_flushTimer = null;
|
|
144
|
+
}
|
|
145
|
+
await _flushToServer();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isHydrated() {
|
|
149
|
+
return _hydrated;
|
|
150
|
+
}
|