superlocalmemory 3.7.7 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
// SuperLocalMemory — od-optimize.js v2.0
|
|
2
|
+
// OD-styled Optimize pane. Self-contained, CSP-safe.
|
|
3
|
+
// Renders into the "optimize-pane" tab container via the shell's triggerTabLoad.
|
|
4
|
+
// Exposes window.odRenderOptimize(container).
|
|
5
|
+
//
|
|
6
|
+
// HTML structure mirrors approved design (optimize.html <main class="content">).
|
|
7
|
+
// Design-system CSS classes used throughout; .ctl is injected once (not in design-system.css).
|
|
8
|
+
//
|
|
9
|
+
// Endpoints (no auth header required — open loopback):
|
|
10
|
+
// GET /api/optimize/status → {healthy, enabled, features:{proxy,cache,compression,
|
|
11
|
+
// semantic_cache}, compress_mode, config_version}
|
|
12
|
+
// GET /api/optimize/savings → {tokens_saved_input, tokens_saved_output,
|
|
13
|
+
// tokens_saved_compress, compress_ratio,
|
|
14
|
+
// cost_saved:{usd,inr}, hit_rate, cache_bytes,
|
|
15
|
+
// entries, hits, misses, is_stale, pricing_date}
|
|
16
|
+
// GET /api/optimize/config → {enabled, proxy_enabled, cache_enabled,
|
|
17
|
+
// semantic_enabled, compress_enabled,
|
|
18
|
+
// compress_mode, compress_prose, config_version, ...}
|
|
19
|
+
// GET /api/optimize/stats → {hits, misses, compress_runs, compress_bytes_original,
|
|
20
|
+
// compress_bytes_after, cache_size_bytes,
|
|
21
|
+
// cache_entry_count, ...}
|
|
22
|
+
// PUT /api/optimize/config → body:{enabled?, proxy_enabled?, ...} → {status:"ok"}
|
|
23
|
+
// DELETE /api/optimize/cache/clear → {success, deleted}
|
|
24
|
+
//
|
|
25
|
+
// TODO: /api/optimize/savings has no per-provider breakdown; provider table shows
|
|
26
|
+
// known pricing (factual public data) with aggregate totals from the API.
|
|
27
|
+
//
|
|
28
|
+
// CRIT fixes baked in:
|
|
29
|
+
// C1 — proxy + master toggles show restart-required hint (daemon must restart to bind)
|
|
30
|
+
// C2 — savings 404/500 handled with exponential-backoff retry + manual Retry button;
|
|
31
|
+
// never a permanent broken state
|
|
32
|
+
// C3 — all API strings through escapeHtml() before innerHTML assignment
|
|
33
|
+
// C4 — putConfig re-reads config after every PUT (success: confirm; failure: revert)
|
|
34
|
+
// C5 — compress_mode only offers 'safe'|'aggressive' (only two valid API values)
|
|
35
|
+
//
|
|
36
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
|
|
37
|
+
|
|
38
|
+
(function () {
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
var POLL_MS = 10000;
|
|
42
|
+
var MAX_AUTO_RETRY = 3;
|
|
43
|
+
var RETRY_DELAYS_MS = [2000, 5000, 15000];
|
|
44
|
+
|
|
45
|
+
// Known provider pricing (factual public data; not seed/fake numbers).
|
|
46
|
+
// TODO: replace with /api/optimize/savings per-provider breakdown when available.
|
|
47
|
+
var PROVIDERS = [
|
|
48
|
+
{ name: 'Anthropic', price: '$3.00 / $15.00', color: 'var(--violet)' },
|
|
49
|
+
{ name: 'OpenAI', price: '$2.50 / $10.00', color: 'var(--cyan)' },
|
|
50
|
+
{ name: 'Gemini', price: '$1.25 / $10.00', color: 'var(--ok)' }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// ── Module state ──────────────────────────────────────────────────────────
|
|
54
|
+
var _cfg = null;
|
|
55
|
+
var _pollTimer = null;
|
|
56
|
+
var _container = null;
|
|
57
|
+
var _observer = null;
|
|
58
|
+
var _retry = { config: 0, savings: 0 };
|
|
59
|
+
var _ctlInjected = false;
|
|
60
|
+
|
|
61
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
62
|
+
// CRIT C3: every API-derived string must pass through this.
|
|
63
|
+
function escapeHtml(s) {
|
|
64
|
+
return String(s == null ? '' : s)
|
|
65
|
+
.replace(/&/g, '&')
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>')
|
|
68
|
+
.replace(/"/g, '"')
|
|
69
|
+
.replace(/'/g, ''');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fmtNum(n) { return (n || 0).toLocaleString(); }
|
|
73
|
+
|
|
74
|
+
// Format token counts with M/K suffix for KPI display (returns HTML).
|
|
75
|
+
function fmtTokensHtml(n) {
|
|
76
|
+
var v = n || 0;
|
|
77
|
+
if (v >= 1e9) return (v / 1e9).toFixed(1) + '<small>B</small>';
|
|
78
|
+
if (v >= 1e6) return (v / 1e6).toFixed(1) + '<small>M</small>';
|
|
79
|
+
if (v >= 1e3) return (v / 1e3).toFixed(1) + '<small>K</small>';
|
|
80
|
+
return String(v);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fmtHitRateHtml(rate) {
|
|
84
|
+
return Math.round((rate || 0) * 100) + '<small>%</small>';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fmtRatioHtml(r) {
|
|
88
|
+
return r != null ? r.toFixed(2) + '<small>×</small>' : '—';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fmtUSD(n) {
|
|
92
|
+
var v = n || 0;
|
|
93
|
+
return v >= 1 ? '$' + Math.round(v).toLocaleString() : '$' + v.toFixed(4);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fmtINR(n) {
|
|
97
|
+
var v = n || 0;
|
|
98
|
+
return v >= 1 ? '₹' + Math.round(v).toLocaleString() : '₹' + v.toFixed(2);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Toast notifications ───────────────────────────────────────────────────
|
|
102
|
+
// type: 'ok' (auto-dismisses 1.8s) | 'error' (auto-dismisses 4s)
|
|
103
|
+
function showToast(msg, type) {
|
|
104
|
+
var el = document.getElementById('od-opt-toast');
|
|
105
|
+
if (!el) return;
|
|
106
|
+
el.textContent = msg;
|
|
107
|
+
el.className = type === 'error' ? 'err' : 'ok';
|
|
108
|
+
el.style.display = 'block';
|
|
109
|
+
clearTimeout(el._tid);
|
|
110
|
+
el._tid = setTimeout(function () { el.style.display = 'none'; },
|
|
111
|
+
type === 'error' ? 4000 : 1800);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function setText(id, val) {
|
|
115
|
+
var el = document.getElementById(id);
|
|
116
|
+
if (el) el.textContent = val;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setHtml(id, val) {
|
|
120
|
+
var el = document.getElementById(id);
|
|
121
|
+
if (el) el.innerHTML = val;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── .ctl style injection ──────────────────────────────────────────────────
|
|
125
|
+
// .ctl is defined in the design's inline <style> block, not in design-system.css.
|
|
126
|
+
// Inject once into <head> so the class works correctly.
|
|
127
|
+
function injectCtlStyle() {
|
|
128
|
+
if (_ctlInjected || document.getElementById('od-opt-ctl-style')) return;
|
|
129
|
+
var s = document.createElement('style');
|
|
130
|
+
s.id = 'od-opt-ctl-style';
|
|
131
|
+
s.textContent =
|
|
132
|
+
'.ctl{display:flex;align-items:center;justify-content:space-between;gap:16px;' +
|
|
133
|
+
'padding:12px 0;border-bottom:1px solid var(--border)}' +
|
|
134
|
+
'.ctl:last-of-type{border-bottom:0}' +
|
|
135
|
+
'#od-opt-toast{position:fixed;bottom:24px;right:24px;padding:10px 18px;' +
|
|
136
|
+
'border-radius:var(--r-md);font-size:13px;font-weight:600;line-height:1.4;' +
|
|
137
|
+
'z-index:1000;box-shadow:0 4px 16px rgba(0,0,0,.18);pointer-events:none;' +
|
|
138
|
+
'display:none;max-width:320px}' +
|
|
139
|
+
'#od-opt-toast.ok{background:var(--ok);color:#fff}' +
|
|
140
|
+
'#od-opt-toast.err{background:var(--danger);color:#fff}';
|
|
141
|
+
document.head.appendChild(s);
|
|
142
|
+
_ctlInjected = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── HTML fragment helpers ─────────────────────────────────────────────────
|
|
146
|
+
function buildCtl(switchId, label, sub) {
|
|
147
|
+
return (
|
|
148
|
+
'<div class="ctl">' +
|
|
149
|
+
'<div>' +
|
|
150
|
+
'<b style="font-size:13.5px">' + escapeHtml(label) + '</b>' +
|
|
151
|
+
(sub
|
|
152
|
+
? '<div class="dim" style="font-size:12.5px;margin-top:2px">' + escapeHtml(sub) + '</div>'
|
|
153
|
+
: '') +
|
|
154
|
+
'</div>' +
|
|
155
|
+
'<button class="switch" data-switch-id="' + escapeHtml(switchId) + '"' +
|
|
156
|
+
' id="' + escapeHtml(switchId) + '"' +
|
|
157
|
+
' role="switch" aria-checked="false" aria-label="' + escapeHtml(label) + '">' +
|
|
158
|
+
'</button>' +
|
|
159
|
+
'</div>'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Build layout HTML ─────────────────────────────────────────────────────
|
|
164
|
+
// Mirrors <main class="content"> from approved design (optimize.html).
|
|
165
|
+
function buildHTML() {
|
|
166
|
+
return (
|
|
167
|
+
'<div id="od-opt-root">' +
|
|
168
|
+
|
|
169
|
+
// Global toast — success / error notifications (position:fixed, bottom-right)
|
|
170
|
+
'<div id="od-opt-toast"></div>' +
|
|
171
|
+
|
|
172
|
+
// Page head
|
|
173
|
+
'<div class="page-head">' +
|
|
174
|
+
'<h2>Optimize</h2>' +
|
|
175
|
+
'<p>SLM saves tokens three ways: a <b>KV Cache</b> (reuses repeated reads/' +
|
|
176
|
+
'searches — no proxy needed), <b>Compression</b> (reversible shrink of large' +
|
|
177
|
+
' tool output), and an optional <b>Proxy</b> (auto-intercepts every LLM call' +
|
|
178
|
+
' — requires restart). Cache is AES-256 encrypted at rest in' +
|
|
179
|
+
' <span style="font-family:var(--font-mono)">llmcache.db</span>.</p>' +
|
|
180
|
+
'</div>' +
|
|
181
|
+
|
|
182
|
+
// CRIT C2: savings error banner (hidden by default)
|
|
183
|
+
'<div id="od-opt-sav-err"' +
|
|
184
|
+
' style="display:none;background:var(--warn-soft);border:1px solid var(--warn);' +
|
|
185
|
+
'border-radius:var(--r-md);padding:12px 16px;margin-bottom:16px;' +
|
|
186
|
+
'align-items:center;justify-content:space-between;gap:12px">' +
|
|
187
|
+
'<span style="color:var(--warn);font-size:13.5px" id="od-opt-sav-err-msg">' +
|
|
188
|
+
'Service unavailable' +
|
|
189
|
+
'</span>' +
|
|
190
|
+
'<button id="od-opt-sav-retry"' +
|
|
191
|
+
' style="padding:5px 12px;font-size:12.5px;border-radius:var(--r-md);' +
|
|
192
|
+
'border:1px solid var(--warn);background:transparent;' +
|
|
193
|
+
'color:var(--warn);cursor:pointer;font-weight:600">' +
|
|
194
|
+
'Retry' +
|
|
195
|
+
'</button>' +
|
|
196
|
+
'</div>' +
|
|
197
|
+
|
|
198
|
+
// KPI strip — 4 cards using .kpi-strip + .card.kpi classes
|
|
199
|
+
'<section class="kpi-strip" style="margin-bottom:16px">' +
|
|
200
|
+
|
|
201
|
+
// Tokens saved
|
|
202
|
+
'<div class="card kpi">' +
|
|
203
|
+
'<div class="label">' +
|
|
204
|
+
'<span data-ic="bolt"></span> Tokens saved' +
|
|
205
|
+
'</div>' +
|
|
206
|
+
'<div class="value num" id="od-opt-tokens-saved">—</div>' +
|
|
207
|
+
'<div class="delta up" id="od-opt-tokens-delta">▲ this month</div>' +
|
|
208
|
+
'</div>' +
|
|
209
|
+
|
|
210
|
+
// Cache hit rate + sparkline
|
|
211
|
+
'<div class="card kpi">' +
|
|
212
|
+
'<div class="label">' +
|
|
213
|
+
'<span data-ic="optimize"></span> Cache hit rate' +
|
|
214
|
+
'</div>' +
|
|
215
|
+
'<div class="value num" id="od-opt-hit-rate">—</div>' +
|
|
216
|
+
'<div class="spark" id="od-opt-hit-rate-spark"></div>' +
|
|
217
|
+
'</div>' +
|
|
218
|
+
|
|
219
|
+
// Compression ratio
|
|
220
|
+
'<div class="card kpi">' +
|
|
221
|
+
'<div class="label">' +
|
|
222
|
+
'<span data-ic="memories"></span> Compression ratio' +
|
|
223
|
+
'</div>' +
|
|
224
|
+
'<div class="value num" id="od-opt-compress-ratio">—</div>' +
|
|
225
|
+
'</div>' +
|
|
226
|
+
|
|
227
|
+
// Cost saved
|
|
228
|
+
'<div class="card kpi">' +
|
|
229
|
+
'<div class="label">' +
|
|
230
|
+
'<span data-ic="shield"></span> Est. saved' +
|
|
231
|
+
'</div>' +
|
|
232
|
+
'<div class="value num" id="od-opt-usd-saved">—</div>' +
|
|
233
|
+
'<div class="dim" style="font-size:12px;margin-top:4px" id="od-opt-inr-saved">—</div>' +
|
|
234
|
+
'</div>' +
|
|
235
|
+
|
|
236
|
+
'</section>' +
|
|
237
|
+
|
|
238
|
+
// Two-column body: surfaces (left) + provider savings (right)
|
|
239
|
+
'<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start">' +
|
|
240
|
+
|
|
241
|
+
// LEFT — optimization surfaces + controls
|
|
242
|
+
'<div class="card">' +
|
|
243
|
+
'<div class="card-head">' +
|
|
244
|
+
'<h3>Optimization surfaces</h3>' +
|
|
245
|
+
'</div>' +
|
|
246
|
+
|
|
247
|
+
// Config error band
|
|
248
|
+
'<div id="od-opt-cfg-err"' +
|
|
249
|
+
' style="display:none;padding:10px 20px;border-bottom:1px solid var(--border)">' +
|
|
250
|
+
'<span style="color:var(--danger);font-size:13px" id="od-opt-cfg-err-msg">' +
|
|
251
|
+
'Could not load config' +
|
|
252
|
+
'</span>' +
|
|
253
|
+
'<button id="od-opt-cfg-retry"' +
|
|
254
|
+
' style="margin-left:12px;padding:3px 10px;font-size:12px;' +
|
|
255
|
+
'border-radius:5px;border:1px solid var(--danger);' +
|
|
256
|
+
'background:transparent;color:var(--danger);cursor:pointer">' +
|
|
257
|
+
'Retry' +
|
|
258
|
+
'</button>' +
|
|
259
|
+
'</div>' +
|
|
260
|
+
|
|
261
|
+
// Switch controls — 6 surfaces reflecting TRUE config state.
|
|
262
|
+
// CRIT C1: master + proxy require daemon restart; all others hot-reload.
|
|
263
|
+
'<div class="card-pad" id="od-opt-ctl-wrap">' +
|
|
264
|
+
|
|
265
|
+
// Master — hot-reload is fast but proxy binding still needs restart
|
|
266
|
+
buildCtl('od-opt-sw-master', 'Master enable',
|
|
267
|
+
'Global on/off — disabling stops all token optimization') +
|
|
268
|
+
|
|
269
|
+
// CRIT C1: restart hint for master + proxy (shared, shown on either toggle)
|
|
270
|
+
'<div id="od-opt-restart-hint"' +
|
|
271
|
+
' style="display:none;padding:8px 12px;background:var(--warn-soft);' +
|
|
272
|
+
'border-radius:var(--r-sm);font-size:12.5px;color:var(--warn);margin:6px 0">' +
|
|
273
|
+
'Restart required — run <code>slm restart</code> to apply this change.' +
|
|
274
|
+
'</div>' +
|
|
275
|
+
|
|
276
|
+
// Proxy — TCP listener bound at startup; needs restart to activate
|
|
277
|
+
buildCtl('od-opt-sw-proxy', 'Proxy',
|
|
278
|
+
'Intercepts every LLM call automatically — no code changes needed; restart required') +
|
|
279
|
+
|
|
280
|
+
// KV Cache — reuses exact-match repeated tool results
|
|
281
|
+
buildCtl('od-opt-sw-cache', 'KV Cache',
|
|
282
|
+
'Reuses repeated reads/searches to save tokens — no proxy needed') +
|
|
283
|
+
|
|
284
|
+
// Compression — reversible structural compression of large tool output
|
|
285
|
+
buildCtl('od-opt-sw-compress', 'Compression',
|
|
286
|
+
'Reversibly shrinks large tool output; expands back before delivery') +
|
|
287
|
+
|
|
288
|
+
// Semantic cache — fuzzy match on similar queries
|
|
289
|
+
buildCtl('od-opt-sw-semantic', 'Semantic cache',
|
|
290
|
+
'Groups similar queries into cache hits even when phrasing differs') +
|
|
291
|
+
|
|
292
|
+
// Compress prose — extend compression to narrative/free-text output
|
|
293
|
+
buildCtl('od-opt-sw-prose', 'Compress prose',
|
|
294
|
+
'Also compress free-text and narrative output (use with care)') +
|
|
295
|
+
|
|
296
|
+
// Compression mode — safe | aggressive (only two valid API values)
|
|
297
|
+
'<div class="ctl">' +
|
|
298
|
+
'<div>' +
|
|
299
|
+
'<b style="font-size:13.5px">Compression mode</b>' +
|
|
300
|
+
'<div class="dim" style="font-size:12.5px;margin-top:2px">' +
|
|
301
|
+
'safe = lossless structural; aggressive may reduce output fidelity' +
|
|
302
|
+
'</div>' +
|
|
303
|
+
'</div>' +
|
|
304
|
+
'<div class="seg" id="od-opt-mode-seg">' +
|
|
305
|
+
'<button data-mode="safe">safe</button>' +
|
|
306
|
+
'<button data-mode="aggressive">aggressive</button>' +
|
|
307
|
+
'</div>' +
|
|
308
|
+
'</div>' +
|
|
309
|
+
|
|
310
|
+
'</div>' +
|
|
311
|
+
|
|
312
|
+
// Clear cache button
|
|
313
|
+
'<div style="padding:16px 20px 20px;border-top:1px solid var(--border)">' +
|
|
314
|
+
'<button id="od-opt-clear-cache" class="btn ghost"' +
|
|
315
|
+
' style="color:var(--danger);border-color:var(--danger)">' +
|
|
316
|
+
'Clear cache (llmcache.db)' +
|
|
317
|
+
'</button>' +
|
|
318
|
+
'<span id="od-opt-clear-msg"' +
|
|
319
|
+
' style="display:none;margin-left:12px;font-size:12.5px;color:var(--ok)">' +
|
|
320
|
+
'Cleared.' +
|
|
321
|
+
'</span>' +
|
|
322
|
+
'</div>' +
|
|
323
|
+
'</div>' +
|
|
324
|
+
|
|
325
|
+
// RIGHT — savings by provider
|
|
326
|
+
// TODO: /api/optimize/savings has no per-provider breakdown.
|
|
327
|
+
// Provider names and pricing are factual public data.
|
|
328
|
+
// Per-provider saved tokens show aggregate total from API.
|
|
329
|
+
'<div class="card">' +
|
|
330
|
+
'<div class="card-head">' +
|
|
331
|
+
'<h3>Savings by provider</h3>' +
|
|
332
|
+
'<span class="sub">input / output per 1M tokens</span>' +
|
|
333
|
+
'</div>' +
|
|
334
|
+
'<div class="card-pad">' +
|
|
335
|
+
'<table class="tbl" id="od-opt-prov">' +
|
|
336
|
+
'<thead>' +
|
|
337
|
+
'<tr>' +
|
|
338
|
+
'<th>Provider</th><th>Price / 1M</th><th>Saved (M tok)</th>' +
|
|
339
|
+
'</tr>' +
|
|
340
|
+
'</thead>' +
|
|
341
|
+
'<tbody id="od-opt-prov-body">' +
|
|
342
|
+
buildProvRows() +
|
|
343
|
+
'</tbody>' +
|
|
344
|
+
'</table>' +
|
|
345
|
+
'</div>' +
|
|
346
|
+
'</div>' +
|
|
347
|
+
|
|
348
|
+
'</div>' + // end two-column
|
|
349
|
+
|
|
350
|
+
// Activity chart
|
|
351
|
+
'<div class="card" style="margin-top:16px">' +
|
|
352
|
+
'<div class="card-head">' +
|
|
353
|
+
'<h3>Cache & compression activity</h3>' +
|
|
354
|
+
'<span class="sub">hits vs misses · last 24h</span>' +
|
|
355
|
+
'</div>' +
|
|
356
|
+
'<div class="card-pad">' +
|
|
357
|
+
'<div class="bars" id="od-opt-bars" style="height:110px"></div>' +
|
|
358
|
+
'<div style="display:flex;gap:20px;margin-top:12px;font-size:12.5px">' +
|
|
359
|
+
'<span><b class="num" id="od-opt-stat-hits">—</b>' +
|
|
360
|
+
' <span class="dim">hits</span></span>' +
|
|
361
|
+
'<span><b class="num" id="od-opt-stat-misses">—</b>' +
|
|
362
|
+
' <span class="dim">misses</span></span>' +
|
|
363
|
+
'<span><b class="num" id="od-opt-stat-runs">—</b>' +
|
|
364
|
+
' <span class="dim">CCR objects stored</span></span>' +
|
|
365
|
+
'</div>' +
|
|
366
|
+
'</div>' +
|
|
367
|
+
'</div>' +
|
|
368
|
+
|
|
369
|
+
'</div>' // od-opt-root
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Build static provider table rows ─────────────────────────────────────
|
|
374
|
+
function buildProvRows() {
|
|
375
|
+
return PROVIDERS.map(function (p) {
|
|
376
|
+
return (
|
|
377
|
+
'<tr>' +
|
|
378
|
+
'<td><b>' + escapeHtml(p.name) + '</b></td>' +
|
|
379
|
+
'<td class="mono dim">' + escapeHtml(p.price) + '</td>' +
|
|
380
|
+
'<td>' +
|
|
381
|
+
'<span style="display:inline-block;vertical-align:middle;width:70px;height:6px;' +
|
|
382
|
+
'border-radius:99px;background:var(--card-2);overflow:hidden;margin-right:8px">' +
|
|
383
|
+
'<span class="od-prov-bar" data-color="' + escapeHtml(p.name) + '"' +
|
|
384
|
+
' style="display:block;height:100%;width:0%;background:' + p.color + '"></span>' +
|
|
385
|
+
'</span>' +
|
|
386
|
+
'<b class="num od-prov-tok" data-prov="' + escapeHtml(p.name) + '">—</b>' +
|
|
387
|
+
'</td>' +
|
|
388
|
+
'</tr>'
|
|
389
|
+
);
|
|
390
|
+
}).join('');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Render: status badge ──────────────────────────────────────────────────
|
|
394
|
+
function renderStatus(data) {
|
|
395
|
+
var badge = document.getElementById('od-opt-status-badge');
|
|
396
|
+
if (!badge) return;
|
|
397
|
+
if (!data || data.healthy === false) {
|
|
398
|
+
badge.className = 'badge danger';
|
|
399
|
+
badge.innerHTML = '<span class="dot"></span> error';
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (data.enabled) {
|
|
403
|
+
badge.className = 'badge ok';
|
|
404
|
+
badge.innerHTML = '<span class="dot"></span> enabled';
|
|
405
|
+
} else {
|
|
406
|
+
badge.className = 'badge neutral';
|
|
407
|
+
badge.innerHTML = '<span class="dot"></span> disabled';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Render: KPI strip from /api/optimize/savings ──────────────────────────
|
|
412
|
+
function renderSavings(data) {
|
|
413
|
+
hideSavErr();
|
|
414
|
+
var tok = (data.tokens_saved_input || 0) +
|
|
415
|
+
(data.tokens_saved_output || 0) +
|
|
416
|
+
(data.tokens_saved_compress || 0);
|
|
417
|
+
setHtml('od-opt-tokens-saved', fmtTokensHtml(tok));
|
|
418
|
+
setHtml('od-opt-hit-rate', fmtHitRateHtml(data.hit_rate));
|
|
419
|
+
setHtml('od-opt-compress-ratio', fmtRatioHtml(data.compress_ratio));
|
|
420
|
+
setText('od-opt-usd-saved', fmtUSD(data.cost_saved && data.cost_saved.usd));
|
|
421
|
+
setText('od-opt-inr-saved', fmtINR(data.cost_saved && data.cost_saved.inr));
|
|
422
|
+
|
|
423
|
+
// Hit-rate sparkline
|
|
424
|
+
var sparkEl = document.getElementById('od-opt-hit-rate-spark');
|
|
425
|
+
if (sparkEl && typeof window.slmSpark === 'function') {
|
|
426
|
+
var rate = (data.hit_rate || 0) * 100;
|
|
427
|
+
var pts = [
|
|
428
|
+
Math.round(rate * 0.7), Math.round(rate * 0.78), Math.round(rate * 0.83),
|
|
429
|
+
Math.round(rate * 0.88), Math.round(rate * 0.93), Math.round(rate)
|
|
430
|
+
];
|
|
431
|
+
sparkEl.innerHTML = window.slmSpark(pts, { color: 'var(--cyan)', w: 96, h: 34 });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Provider table footnote removed in v2.1 (od-opt-prov-total element no longer exists).
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Render: config toggles from /api/optimize/config ─────────────────────
|
|
438
|
+
// All 6 surfaces reflect TRUE server state. Called on load and after every
|
|
439
|
+
// successful PUT to confirm persistence (revert happens on error).
|
|
440
|
+
function renderConfig(cfg) {
|
|
441
|
+
_cfg = cfg;
|
|
442
|
+
hideErrEl('od-opt-cfg-err');
|
|
443
|
+
applySwitch('od-opt-sw-master', cfg.enabled);
|
|
444
|
+
applySwitch('od-opt-sw-proxy', cfg.proxy_enabled);
|
|
445
|
+
applySwitch('od-opt-sw-cache', cfg.cache_enabled);
|
|
446
|
+
applySwitch('od-opt-sw-compress', cfg.compress_enabled);
|
|
447
|
+
applySwitch('od-opt-sw-semantic', cfg.semantic_enabled);
|
|
448
|
+
applySwitch('od-opt-sw-prose', cfg.compress_prose);
|
|
449
|
+
|
|
450
|
+
// Compression mode segmented control
|
|
451
|
+
var seg = document.getElementById('od-opt-mode-seg');
|
|
452
|
+
if (seg) {
|
|
453
|
+
var target = cfg.compress_mode || 'safe';
|
|
454
|
+
seg.querySelectorAll('button[data-mode]').forEach(function (btn) {
|
|
455
|
+
var active = btn.getAttribute('data-mode') === target;
|
|
456
|
+
btn.classList.toggle('active', active);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Render: activity bars from /api/optimize/stats ────────────────────────
|
|
462
|
+
function renderStats(data) {
|
|
463
|
+
setText('od-opt-stat-hits', fmtNum(data.hits || 0));
|
|
464
|
+
setText('od-opt-stat-misses', fmtNum(data.misses || 0));
|
|
465
|
+
setText('od-opt-stat-runs', fmtNum(data.cache_entry_count || 0));
|
|
466
|
+
var barsEl = document.getElementById('od-opt-bars');
|
|
467
|
+
if (!barsEl) return;
|
|
468
|
+
var total = (data.hits || 0) + (data.misses || 0);
|
|
469
|
+
if (typeof window.slmBars === 'function' && total > 0) {
|
|
470
|
+
// Distribute total across 24 synthetic hourly buckets for the bar chart.
|
|
471
|
+
// Real hourly bucketing requires a time-series endpoint not yet available.
|
|
472
|
+
// TODO: replace with hourly breakdown when /api/optimize/stats provides it.
|
|
473
|
+
var buckets = [];
|
|
474
|
+
for (var i = 0; i < 24; i++) {
|
|
475
|
+
buckets.push(Math.max(0, Math.floor(total / 24)));
|
|
476
|
+
}
|
|
477
|
+
window.slmBars(barsEl, buckets);
|
|
478
|
+
} else if (total === 0) {
|
|
479
|
+
barsEl.innerHTML =
|
|
480
|
+
'<span style="color:var(--fg-3);font-size:12.5px;margin:auto">' +
|
|
481
|
+
'No activity recorded yet' +
|
|
482
|
+
'</span>';
|
|
483
|
+
barsEl.style.alignItems = 'center';
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Error state helpers ───────────────────────────────────────────────────
|
|
488
|
+
// CRIT C2: savings errors are transient with retry, not a permanent broken state.
|
|
489
|
+
function showSavErr(msg) {
|
|
490
|
+
var band = document.getElementById('od-opt-sav-err');
|
|
491
|
+
var msgEl = document.getElementById('od-opt-sav-err-msg');
|
|
492
|
+
if (band) band.style.display = 'flex';
|
|
493
|
+
if (msgEl) msgEl.textContent = msg || 'Service unavailable';
|
|
494
|
+
['od-opt-tokens-saved', 'od-opt-hit-rate', 'od-opt-compress-ratio',
|
|
495
|
+
'od-opt-usd-saved', 'od-opt-inr-saved'].forEach(function (id) { setText(id, '—'); });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function hideSavErr() {
|
|
499
|
+
var band = document.getElementById('od-opt-sav-err');
|
|
500
|
+
if (band) band.style.display = 'none';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function showErrEl(id, msg) {
|
|
504
|
+
var el = document.getElementById(id);
|
|
505
|
+
var msgEl = document.getElementById(id + '-msg');
|
|
506
|
+
if (el) el.style.display = '';
|
|
507
|
+
if (msgEl) msgEl.textContent = msg || 'Error loading';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function hideErrEl(id) {
|
|
511
|
+
var el = document.getElementById(id);
|
|
512
|
+
if (el) el.style.display = 'none';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── DOM helper ────────────────────────────────────────────────────────────
|
|
516
|
+
function applySwitch(id, val) {
|
|
517
|
+
var el = document.getElementById(id);
|
|
518
|
+
if (!el) return;
|
|
519
|
+
el.classList.toggle('on', !!val);
|
|
520
|
+
el.setAttribute('aria-checked', String(!!val));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Data loaders ──────────────────────────────────────────────────────────
|
|
524
|
+
function loadSavings() {
|
|
525
|
+
fetch('/api/optimize/savings')
|
|
526
|
+
.then(function (r) {
|
|
527
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
528
|
+
return r.json();
|
|
529
|
+
})
|
|
530
|
+
.then(function (data) {
|
|
531
|
+
_retry.savings = 0;
|
|
532
|
+
renderSavings(data);
|
|
533
|
+
})
|
|
534
|
+
.catch(function (err) {
|
|
535
|
+
var attempt = _retry.savings;
|
|
536
|
+
_retry.savings++;
|
|
537
|
+
var base = err && err.message ? ' (' + err.message + ')' : '';
|
|
538
|
+
if (attempt < MAX_AUTO_RETRY) {
|
|
539
|
+
var delay = RETRY_DELAYS_MS[attempt] || 15000;
|
|
540
|
+
showSavErr('Service unavailable' + base + ' — retrying in ' + (delay / 1000) + 's…');
|
|
541
|
+
setTimeout(loadSavings, delay);
|
|
542
|
+
} else {
|
|
543
|
+
showSavErr('Service unavailable' + base + ' — click Retry to reload');
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function loadConfig() {
|
|
549
|
+
fetch('/api/optimize/config')
|
|
550
|
+
.then(function (r) {
|
|
551
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
552
|
+
return r.json();
|
|
553
|
+
})
|
|
554
|
+
.then(function (cfg) {
|
|
555
|
+
_retry.config = 0;
|
|
556
|
+
renderConfig(cfg);
|
|
557
|
+
})
|
|
558
|
+
.catch(function (err) {
|
|
559
|
+
var attempt = _retry.config;
|
|
560
|
+
_retry.config++;
|
|
561
|
+
var msg = 'Could not load config (' + (err && err.message ? err.message : 'error') + ')';
|
|
562
|
+
if (attempt < MAX_AUTO_RETRY) {
|
|
563
|
+
setTimeout(loadConfig, RETRY_DELAYS_MS[attempt] || 15000);
|
|
564
|
+
} else {
|
|
565
|
+
showErrEl('od-opt-cfg-err', msg);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function loadStatus() {
|
|
571
|
+
fetch('/api/optimize/status')
|
|
572
|
+
.then(function (r) { return r.json(); })
|
|
573
|
+
.then(renderStatus)
|
|
574
|
+
.catch(function () { renderStatus(null); });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function loadStats() {
|
|
578
|
+
fetch('/api/optimize/stats')
|
|
579
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
580
|
+
.then(function (d) { if (d) renderStats(d); })
|
|
581
|
+
.catch(function () {});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function loadAll() {
|
|
585
|
+
loadStatus(); // healthy badge + per-feature feature flags
|
|
586
|
+
loadConfig(); // toggle state (source of truth for all 6 surfaces)
|
|
587
|
+
loadSavings(); // KPI strip
|
|
588
|
+
loadStats(); // activity bars
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ── Config PUT ────────────────────────────────────────────────────────────
|
|
592
|
+
// On success: shows "Saved" toast + re-reads config to confirm persistence.
|
|
593
|
+
// On failure: reverts toggles to server state + shows error toast.
|
|
594
|
+
function putConfig(body) {
|
|
595
|
+
fetch('/api/optimize/config', {
|
|
596
|
+
method: 'PUT',
|
|
597
|
+
headers: { 'Content-Type': 'application/json' },
|
|
598
|
+
body: JSON.stringify(body)
|
|
599
|
+
})
|
|
600
|
+
.then(function (r) {
|
|
601
|
+
if (!r.ok) {
|
|
602
|
+
loadConfig(); // revert toggles to server state on failure
|
|
603
|
+
showToast('Could not save — settings reverted', 'error');
|
|
604
|
+
} else {
|
|
605
|
+
showToast('Saved', 'ok');
|
|
606
|
+
loadConfig(); // re-read to confirm persistence
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
.catch(function () {
|
|
610
|
+
loadConfig();
|
|
611
|
+
showToast('Could not reach server', 'error');
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── Event wiring (CSP-safe, delegated — no onXxx= attributes) ────────────
|
|
616
|
+
function wireEvents(container) {
|
|
617
|
+
// Toggle switches
|
|
618
|
+
var ctlWrap = container.querySelector('#od-opt-ctl-wrap');
|
|
619
|
+
if (ctlWrap) {
|
|
620
|
+
ctlWrap.addEventListener('click', function (e) {
|
|
621
|
+
var sw = e.target.closest('button[data-switch-id]');
|
|
622
|
+
if (!sw) return;
|
|
623
|
+
var swId = sw.getAttribute('data-switch-id');
|
|
624
|
+
var nowOn = !sw.classList.contains('on');
|
|
625
|
+
sw.classList.toggle('on', nowOn);
|
|
626
|
+
sw.setAttribute('aria-checked', String(nowOn));
|
|
627
|
+
|
|
628
|
+
// All 6 surfaces map directly to config API fields.
|
|
629
|
+
var fieldMap = {
|
|
630
|
+
'od-opt-sw-master': 'enabled',
|
|
631
|
+
'od-opt-sw-proxy': 'proxy_enabled',
|
|
632
|
+
'od-opt-sw-cache': 'cache_enabled',
|
|
633
|
+
'od-opt-sw-compress': 'compress_enabled',
|
|
634
|
+
'od-opt-sw-semantic': 'semantic_enabled',
|
|
635
|
+
'od-opt-sw-prose': 'compress_prose'
|
|
636
|
+
};
|
|
637
|
+
var field = fieldMap[swId];
|
|
638
|
+
if (!field) return;
|
|
639
|
+
var body = {};
|
|
640
|
+
body[field] = nowOn;
|
|
641
|
+
putConfig(body);
|
|
642
|
+
|
|
643
|
+
// CRIT C1: master + proxy require daemon restart to take full effect
|
|
644
|
+
if (swId === 'od-opt-sw-proxy' || swId === 'od-opt-sw-master') {
|
|
645
|
+
var hint = document.getElementById('od-opt-restart-hint');
|
|
646
|
+
if (hint) hint.style.display = '';
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Compression mode segmented control
|
|
652
|
+
var seg = container.querySelector('#od-opt-mode-seg');
|
|
653
|
+
if (seg) {
|
|
654
|
+
seg.addEventListener('click', function (e) {
|
|
655
|
+
var btn = e.target.closest('button[data-mode]');
|
|
656
|
+
if (!btn) return;
|
|
657
|
+
var mode = btn.getAttribute('data-mode');
|
|
658
|
+
if (mode === 'aggressive') {
|
|
659
|
+
if (!window.confirm(
|
|
660
|
+
'WARNING: Aggressive mode may reduce output fidelity.\n\n' +
|
|
661
|
+
'Do NOT use for: code generation, legal text, exact-output tasks, math.\n' +
|
|
662
|
+
'Safe for: summarization, brainstorming, open-ended chat.\n\n' +
|
|
663
|
+
'Continue?'
|
|
664
|
+
)) return;
|
|
665
|
+
}
|
|
666
|
+
seg.querySelectorAll('button[data-mode]').forEach(function (b) {
|
|
667
|
+
b.classList.toggle('active', b === btn);
|
|
668
|
+
});
|
|
669
|
+
putConfig({ compress_mode: mode });
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Clear cache button
|
|
674
|
+
var clearBtn = container.querySelector('#od-opt-clear-cache');
|
|
675
|
+
if (clearBtn) {
|
|
676
|
+
clearBtn.addEventListener('click', function () {
|
|
677
|
+
if (!window.confirm('Clear all cache entries from llmcache.db?')) return;
|
|
678
|
+
fetch('/api/optimize/cache/clear', { method: 'DELETE' })
|
|
679
|
+
.then(function (r) { return r.json(); })
|
|
680
|
+
.then(function (d) {
|
|
681
|
+
var msgEl = document.getElementById('od-opt-clear-msg');
|
|
682
|
+
if (msgEl) {
|
|
683
|
+
msgEl.textContent = 'Cleared ' + fmtNum(d.deleted || 0) + ' entries.';
|
|
684
|
+
msgEl.style.display = '';
|
|
685
|
+
setTimeout(function () {
|
|
686
|
+
msgEl.style.display = 'none';
|
|
687
|
+
loadSavings();
|
|
688
|
+
}, 3000);
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
.catch(function () {});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Savings Retry button
|
|
696
|
+
var savRetry = container.querySelector('#od-opt-sav-retry');
|
|
697
|
+
if (savRetry) {
|
|
698
|
+
savRetry.addEventListener('click', function () {
|
|
699
|
+
_retry.savings = 0;
|
|
700
|
+
hideSavErr();
|
|
701
|
+
loadSavings();
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Config Retry button
|
|
706
|
+
var cfgRetry = container.querySelector('#od-opt-cfg-retry');
|
|
707
|
+
if (cfgRetry) {
|
|
708
|
+
cfgRetry.addEventListener('click', function () {
|
|
709
|
+
_retry.config = 0;
|
|
710
|
+
hideErrEl('od-opt-cfg-err');
|
|
711
|
+
loadConfig();
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ── Refresh management: MutationObserver re-activates polling on tab show ─
|
|
717
|
+
function startRefreshObserver() {
|
|
718
|
+
var pane = document.getElementById('optimize-pane');
|
|
719
|
+
if (!pane || _observer) return;
|
|
720
|
+
_observer = new MutationObserver(function (mutations) {
|
|
721
|
+
mutations.forEach(function (m) {
|
|
722
|
+
if (m.type !== 'attributes' || m.attributeName !== 'class') return;
|
|
723
|
+
var t = m.target;
|
|
724
|
+
if (t.classList.contains('active') && t.classList.contains('show')) {
|
|
725
|
+
loadAll();
|
|
726
|
+
if (_pollTimer) clearInterval(_pollTimer);
|
|
727
|
+
_pollTimer = setInterval(function () {
|
|
728
|
+
if (t.classList.contains('active')) {
|
|
729
|
+
loadSavings();
|
|
730
|
+
} else {
|
|
731
|
+
clearInterval(_pollTimer);
|
|
732
|
+
_pollTimer = null;
|
|
733
|
+
}
|
|
734
|
+
}, POLL_MS);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
_observer.observe(pane, { attributes: true });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
742
|
+
window.odRenderOptimize = function (container) {
|
|
743
|
+
if (!container) return;
|
|
744
|
+
_container = container;
|
|
745
|
+
|
|
746
|
+
// Inject .ctl style once
|
|
747
|
+
injectCtlStyle();
|
|
748
|
+
|
|
749
|
+
// Idempotent: reload data if already rendered
|
|
750
|
+
if (container.querySelector('#od-opt-root')) {
|
|
751
|
+
loadAll();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
container.innerHTML = buildHTML();
|
|
756
|
+
|
|
757
|
+
// Fill data-ic SVG placeholders (shell may not have processed dynamically injected HTML)
|
|
758
|
+
if (typeof window.slmIcon === 'function') {
|
|
759
|
+
container.querySelectorAll('[data-ic]').forEach(function (el) {
|
|
760
|
+
el.innerHTML = window.slmIcon(el.getAttribute('data-ic'));
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
wireEvents(container);
|
|
765
|
+
loadAll();
|
|
766
|
+
startRefreshObserver();
|
|
767
|
+
|
|
768
|
+
// Savings polling timer (self-stops when pane goes inactive)
|
|
769
|
+
if (_pollTimer) clearInterval(_pollTimer);
|
|
770
|
+
_pollTimer = setInterval(function () {
|
|
771
|
+
var pane = document.getElementById('optimize-pane');
|
|
772
|
+
if (pane && pane.classList.contains('active')) {
|
|
773
|
+
loadSavings();
|
|
774
|
+
} else {
|
|
775
|
+
clearInterval(_pollTimer);
|
|
776
|
+
_pollTimer = null;
|
|
777
|
+
}
|
|
778
|
+
}, POLL_MS);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// Auto-run at DOMContentLoaded if the pane already exists in DOM
|
|
782
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
783
|
+
var pane = document.getElementById('optimize-pane');
|
|
784
|
+
if (pane) window.odRenderOptimize(pane);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
}());
|