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,602 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// UI-COST-WIDGET — Cost tracker (dockable + floating)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Glass panel showing cost breakdown with daily bar chart,
|
|
6
|
+
// filter tabs, and drag/collapse persistence.
|
|
7
|
+
// Can dock to navbar (next to ?) or float freely.
|
|
8
|
+
|
|
9
|
+
import { on, emit } from './state.js';
|
|
10
|
+
import { KEYS } from './constants.js';
|
|
11
|
+
import { storage } from './storage.js';
|
|
12
|
+
|
|
13
|
+
// ── State ──
|
|
14
|
+
|
|
15
|
+
let _widget = null; // Root DOM element
|
|
16
|
+
let _costData = null; // Cached API response
|
|
17
|
+
let _activeTab = 'today'; // 'today' | 'month' | 'last30' | 'all'
|
|
18
|
+
let _collapsed = false;
|
|
19
|
+
let _drag = null;
|
|
20
|
+
let _pinnedOpen = false; // Toggled open from Claude Panel (independent of focus mode)
|
|
21
|
+
let _docked = false; // Docked to navbar
|
|
22
|
+
|
|
23
|
+
// ── Init ──
|
|
24
|
+
|
|
25
|
+
export function initCostWidget() {
|
|
26
|
+
const container = document.getElementById('static-bg');
|
|
27
|
+
console.log('[cost-widget] init, container:', !!container);
|
|
28
|
+
if (!container) return;
|
|
29
|
+
|
|
30
|
+
_collapsed = storage.getItem(KEYS.COST_WIDGET_COLLAPSED) === 'true';
|
|
31
|
+
const _dockedPref = storage.getItem(KEYS.COST_WIDGET_DOCKED);
|
|
32
|
+
_docked = _dockedPref === null ? true : _dockedPref === 'true';
|
|
33
|
+
|
|
34
|
+
_widget = document.createElement('div');
|
|
35
|
+
_widget.id = 'cost-widget';
|
|
36
|
+
_widget.className = 'glass cost-widget' + (_collapsed ? ' collapsed' : '') + (_docked ? ' cw-docked' : '');
|
|
37
|
+
|
|
38
|
+
// Restore position or default to bottom-right (clamped to viewport)
|
|
39
|
+
if (!_docked) {
|
|
40
|
+
const savedPos = _loadPos();
|
|
41
|
+
if (savedPos) {
|
|
42
|
+
const maxLeft = window.innerWidth - 270;
|
|
43
|
+
const maxTop = window.innerHeight - 60;
|
|
44
|
+
_widget.style.left = Math.max(0, Math.min(savedPos.left, maxLeft)) + 'px';
|
|
45
|
+
_widget.style.top = Math.max(0, Math.min(savedPos.top, maxTop)) + 'px';
|
|
46
|
+
} else {
|
|
47
|
+
_widget.style.right = '24px';
|
|
48
|
+
_widget.style.bottom = '24px';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_widget.innerHTML = `
|
|
53
|
+
<div class="cw-header">
|
|
54
|
+
<span class="cw-drag" title="Drag to undock and move">⠿</span>
|
|
55
|
+
<span class="cw-total">$0.00</span>
|
|
56
|
+
<span class="cw-month-label"></span>
|
|
57
|
+
<button class="cw-scan" title="Scan CLI sessions">⟳</button>
|
|
58
|
+
<button class="cw-collapse" title="Collapse">${_collapsed ? '▸' : '▾'}</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="cw-body">
|
|
61
|
+
<div class="cw-tabs">
|
|
62
|
+
<button class="cw-tab active" data-tab="today">Today</button>
|
|
63
|
+
<button class="cw-tab" data-tab="month">Month</button>
|
|
64
|
+
<button class="cw-tab" data-tab="last30">30d</button>
|
|
65
|
+
<button class="cw-tab" data-tab="all">All</button>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="cw-chart"></div>
|
|
68
|
+
<div class="cw-list"></div>
|
|
69
|
+
<div class="cw-footer"></div>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
document.body.appendChild(_widget);
|
|
74
|
+
|
|
75
|
+
// Show/hide based on current state
|
|
76
|
+
const focusOn = container.classList.contains('visible');
|
|
77
|
+
if (_docked) {
|
|
78
|
+
_widget.style.display = 'none'; // starts hidden, toggled by navbar click
|
|
79
|
+
} else {
|
|
80
|
+
_widget.style.display = focusOn ? '' : 'none';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Events
|
|
84
|
+
_widget.querySelector('.cw-collapse').addEventListener('click', _toggleCollapse);
|
|
85
|
+
_widget.querySelector('.cw-scan').addEventListener('click', _scanCliSessions);
|
|
86
|
+
_widget.querySelector('.cw-tabs').addEventListener('click', _onTabClick);
|
|
87
|
+
_makeDraggable(_widget.querySelector('.cw-header'));
|
|
88
|
+
|
|
89
|
+
// Close docked widget when clicking outside
|
|
90
|
+
document.addEventListener('mousedown', (e) => {
|
|
91
|
+
if (!_docked || !_widget || _widget.style.display === 'none') return;
|
|
92
|
+
const costBtn = document.getElementById('titlebar-cost-btn');
|
|
93
|
+
if (_widget.contains(e.target) || costBtn?.contains(e.target)) return;
|
|
94
|
+
_widget.style.display = 'none';
|
|
95
|
+
_pinnedOpen = false;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Listen for focus mode changes
|
|
99
|
+
on('viz:toggle', (vizEnabled) => {
|
|
100
|
+
if (_docked) return; // dock mode ignores focus mode
|
|
101
|
+
// vizEnabled=false means focus mode is ON
|
|
102
|
+
if (_widget) {
|
|
103
|
+
if (!vizEnabled) { _widget.style.display = ''; _pinnedOpen = false; }
|
|
104
|
+
else if (!_pinnedOpen) _widget.style.display = 'none';
|
|
105
|
+
}
|
|
106
|
+
if (!vizEnabled) _fetchAndRender();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Toggle from Claude Panel cost label (floating mode)
|
|
110
|
+
on('cost:toggle', () => {
|
|
111
|
+
if (!_widget || _docked) return;
|
|
112
|
+
_pinnedOpen = !_pinnedOpen;
|
|
113
|
+
_widget.style.display = _pinnedOpen ? '' : 'none';
|
|
114
|
+
if (_pinnedOpen) _fetchAndRender();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Toggle from navbar button (dock mode)
|
|
118
|
+
on('cost:dock-toggle', () => {
|
|
119
|
+
if (!_widget) return;
|
|
120
|
+
if (!_docked) {
|
|
121
|
+
// First click docks it
|
|
122
|
+
_dock();
|
|
123
|
+
_pinnedOpen = true;
|
|
124
|
+
_widget.style.display = '';
|
|
125
|
+
_fetchAndRender();
|
|
126
|
+
} else {
|
|
127
|
+
// Already docked — toggle visibility
|
|
128
|
+
_pinnedOpen = !_pinnedOpen;
|
|
129
|
+
_widget.style.display = _pinnedOpen ? '' : 'none';
|
|
130
|
+
if (_pinnedOpen) {
|
|
131
|
+
_positionDocked();
|
|
132
|
+
_fetchAndRender();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Real-time cost updates from Claude Panel
|
|
138
|
+
on('cost:updated', (data) => {
|
|
139
|
+
if (!_costData || !data) return;
|
|
140
|
+
_optimisticUpdate(data.amount);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Emit initial dock state for navbar button
|
|
144
|
+
emit('cost:dock-state', _docked);
|
|
145
|
+
|
|
146
|
+
// Initial fetch if already in focus mode or docked
|
|
147
|
+
if (_docked) { _positionDocked(); _fetchAndRender(); }
|
|
148
|
+
else if (focusOn) _fetchAndRender();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Dock / Undock ──
|
|
152
|
+
|
|
153
|
+
function _dock() {
|
|
154
|
+
_docked = true;
|
|
155
|
+
storage.setItem(KEYS.COST_WIDGET_DOCKED, 'true');
|
|
156
|
+
_widget.classList.add('cw-docked');
|
|
157
|
+
// Clear inline position styles — CSS handles docked positioning
|
|
158
|
+
_widget.style.left = '';
|
|
159
|
+
_widget.style.top = '';
|
|
160
|
+
_widget.style.right = '';
|
|
161
|
+
_widget.style.bottom = '';
|
|
162
|
+
_positionDocked();
|
|
163
|
+
emit('cost:dock-state', true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _undock() {
|
|
167
|
+
_docked = false;
|
|
168
|
+
storage.setItem(KEYS.COST_WIDGET_DOCKED, 'false');
|
|
169
|
+
_widget.classList.remove('cw-docked');
|
|
170
|
+
emit('cost:dock-state', false);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _positionDocked() {
|
|
174
|
+
const anchor = document.getElementById('titlebar-cost-btn');
|
|
175
|
+
if (!anchor || !_widget) return;
|
|
176
|
+
const rect = anchor.getBoundingClientRect();
|
|
177
|
+
_widget.style.top = (rect.bottom + 6) + 'px';
|
|
178
|
+
_widget.style.right = (window.innerWidth - rect.right) + 'px';
|
|
179
|
+
_widget.style.left = '';
|
|
180
|
+
_widget.style.bottom = '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Data ──
|
|
184
|
+
|
|
185
|
+
async function _fetchAndRender() {
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch('/api/claude-skin/cost');
|
|
188
|
+
if (!res.ok) return;
|
|
189
|
+
_costData = await res.json();
|
|
190
|
+
_render();
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _optimisticUpdate(amount) {
|
|
195
|
+
if (!_costData || typeof amount !== 'number') return;
|
|
196
|
+
const month = _costData.month;
|
|
197
|
+
if (!month) return;
|
|
198
|
+
month.totalUsd = Math.round((month.totalUsd + amount) * 1e6) / 1e6;
|
|
199
|
+
month.queries = (month.queries || 0) + 1;
|
|
200
|
+
// Update daily
|
|
201
|
+
if (!month.days) month.days = {};
|
|
202
|
+
const day = new Date().getDate().toString().padStart(2, '0');
|
|
203
|
+
if (!month.days[day]) month.days[day] = { totalUsd: 0, queries: 0 };
|
|
204
|
+
month.days[day].totalUsd = Math.round((month.days[day].totalUsd + amount) * 1e6) / 1e6;
|
|
205
|
+
month.days[day].queries += 1;
|
|
206
|
+
// Also update history
|
|
207
|
+
if (_costData.history && _costData.currentMonth) {
|
|
208
|
+
_costData.history[_costData.currentMonth] = month;
|
|
209
|
+
}
|
|
210
|
+
_render();
|
|
211
|
+
// Flash the total
|
|
212
|
+
const total = _widget?.querySelector('.cw-total');
|
|
213
|
+
if (total) { total.classList.add('flash'); setTimeout(() => total.classList.remove('flash'), 800); }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Render ──
|
|
217
|
+
|
|
218
|
+
function _render() {
|
|
219
|
+
if (!_widget || !_costData) return;
|
|
220
|
+
|
|
221
|
+
const prepared = _prepareData();
|
|
222
|
+
const { dataPoints, listRows, totalUsd, label, avgPerDay, projected } = prepared;
|
|
223
|
+
|
|
224
|
+
// Header
|
|
225
|
+
_widget.querySelector('.cw-total').textContent = '$' + totalUsd.toFixed(2);
|
|
226
|
+
_widget.querySelector('.cw-month-label').textContent = label;
|
|
227
|
+
|
|
228
|
+
// Chart
|
|
229
|
+
_renderBarChart(_widget.querySelector('.cw-chart'), dataPoints);
|
|
230
|
+
|
|
231
|
+
// List
|
|
232
|
+
_renderList(_widget.querySelector('.cw-list'), listRows);
|
|
233
|
+
|
|
234
|
+
// Footer
|
|
235
|
+
const footer = _widget.querySelector('.cw-footer');
|
|
236
|
+
const parts = [];
|
|
237
|
+
if (prepared.queries != null) parts.push(`${prepared.queries} queries`);
|
|
238
|
+
if (avgPerDay != null) parts.push(`avg $${avgPerDay.toFixed(2)}/day`);
|
|
239
|
+
if (projected != null) parts.push(`~$${projected.toFixed(0)}/mo`);
|
|
240
|
+
footer.textContent = parts.join(' · ');
|
|
241
|
+
|
|
242
|
+
// Session count in header tooltip
|
|
243
|
+
const totalEl = _widget.querySelector('.cw-total');
|
|
244
|
+
if (totalEl && _costData) {
|
|
245
|
+
const currentMonth = _costData.currentMonth;
|
|
246
|
+
const month = _costData.history?.[currentMonth] || _costData.month;
|
|
247
|
+
const sessions = month?.sessions?.length || 0;
|
|
248
|
+
totalEl.title = `${sessions} sessions · ${month?.queries || 0} queries`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Navbar cost label is managed by ui-claude-panel._updateCostLabel() (session cost)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _prepareData() {
|
|
255
|
+
const history = _costData.history || {};
|
|
256
|
+
const currentMonth = _costData.currentMonth;
|
|
257
|
+
|
|
258
|
+
if (_activeTab === 'today') {
|
|
259
|
+
return _prepareTodayData(history[currentMonth] || _costData.month);
|
|
260
|
+
} else if (_activeTab === 'month') {
|
|
261
|
+
return _prepareMonthData(history[currentMonth] || _costData.month, currentMonth);
|
|
262
|
+
} else if (_activeTab === 'last30') {
|
|
263
|
+
return _prepareLast30Data(history);
|
|
264
|
+
} else {
|
|
265
|
+
return _prepareAllData(history);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function _prepareTodayData(month) {
|
|
270
|
+
const now = new Date();
|
|
271
|
+
const dayKey = now.getDate().toString().padStart(2, '0');
|
|
272
|
+
const today = month?.days?.[dayKey] || { totalUsd: 0, queries: 0 };
|
|
273
|
+
const totalUsd = today.totalUsd || 0;
|
|
274
|
+
const queries = today.queries || 0;
|
|
275
|
+
const dateLabel = now.toLocaleDateString('en', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
276
|
+
|
|
277
|
+
// Show last 7 days as bar chart for context
|
|
278
|
+
const dataPoints = [];
|
|
279
|
+
for (let i = 6; i >= 0; i--) {
|
|
280
|
+
const d = new Date(now);
|
|
281
|
+
d.setDate(d.getDate() - i);
|
|
282
|
+
const mk = d.toISOString().slice(0, 7);
|
|
283
|
+
const dk = d.getDate().toString().padStart(2, '0');
|
|
284
|
+
const mData = _costData.history?.[mk];
|
|
285
|
+
const val = mData?.days?.[dk]?.totalUsd || 0;
|
|
286
|
+
const q = mData?.days?.[dk]?.queries || 0;
|
|
287
|
+
const dayLabel = d.toLocaleDateString('en', { weekday: 'short' }).slice(0, 2);
|
|
288
|
+
dataPoints.push({
|
|
289
|
+
label: dayLabel,
|
|
290
|
+
value: val,
|
|
291
|
+
queries: q,
|
|
292
|
+
tooltip: `$${val.toFixed(2)} · ${d.toLocaleDateString('en', { month: 'short', day: 'numeric' })}`,
|
|
293
|
+
highlight: i === 0,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
dataPoints,
|
|
299
|
+
listRows: [],
|
|
300
|
+
totalUsd,
|
|
301
|
+
label: dateLabel,
|
|
302
|
+
queries,
|
|
303
|
+
avgPerDay: null,
|
|
304
|
+
projected: null,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function _prepareMonthData(month, monthKey) {
|
|
309
|
+
const days = month?.days || {};
|
|
310
|
+
const now = new Date();
|
|
311
|
+
const year = parseInt(monthKey?.slice(0, 4) || now.getFullYear());
|
|
312
|
+
const mo = parseInt(monthKey?.slice(5, 7) || (now.getMonth() + 1));
|
|
313
|
+
const daysInMonth = new Date(year, mo, 0).getDate();
|
|
314
|
+
const todayDay = now.getDate().toString().padStart(2, '0');
|
|
315
|
+
const monthLabel = new Date(year, mo - 1).toLocaleString('en', { month: 'short', year: 'numeric' });
|
|
316
|
+
|
|
317
|
+
const dataPoints = [];
|
|
318
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
319
|
+
const key = d.toString().padStart(2, '0');
|
|
320
|
+
const val = days[key]?.totalUsd || 0;
|
|
321
|
+
dataPoints.push({
|
|
322
|
+
label: d.toString(),
|
|
323
|
+
value: val,
|
|
324
|
+
queries: days[key]?.queries || 0,
|
|
325
|
+
tooltip: `$${val.toFixed(2)} on ${monthLabel.split(' ')[0]} ${d}`,
|
|
326
|
+
highlight: key === todayDay,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const totalUsd = month?.totalUsd || 0;
|
|
331
|
+
const activeDays = Object.keys(days).length || 1;
|
|
332
|
+
const avgPerDay = totalUsd / activeDays;
|
|
333
|
+
const dayOfMonth = now.getDate();
|
|
334
|
+
const projected = (totalUsd / dayOfMonth) * daysInMonth;
|
|
335
|
+
|
|
336
|
+
// List rows — most recent days first
|
|
337
|
+
const listRows = Object.entries(days)
|
|
338
|
+
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
339
|
+
.slice(0, 10)
|
|
340
|
+
.map(([d, data]) => ({
|
|
341
|
+
label: `${monthLabel.split(' ')[0]} ${parseInt(d)}`,
|
|
342
|
+
cost: data.totalUsd,
|
|
343
|
+
queries: data.queries,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
return { dataPoints, listRows, totalUsd, label: monthLabel, avgPerDay, projected };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function _prepareLast30Data(history) {
|
|
350
|
+
const now = new Date();
|
|
351
|
+
const dataPoints = [];
|
|
352
|
+
const listRows = [];
|
|
353
|
+
let total = 0;
|
|
354
|
+
|
|
355
|
+
for (let i = 29; i >= 0; i--) {
|
|
356
|
+
const d = new Date(now);
|
|
357
|
+
d.setDate(d.getDate() - i);
|
|
358
|
+
const monthKey = d.toISOString().slice(0, 7);
|
|
359
|
+
const dayKey = d.getDate().toString().padStart(2, '0');
|
|
360
|
+
const month = history[monthKey];
|
|
361
|
+
const val = month?.days?.[dayKey]?.totalUsd || 0;
|
|
362
|
+
const queries = month?.days?.[dayKey]?.queries || 0;
|
|
363
|
+
total += val;
|
|
364
|
+
|
|
365
|
+
const dayLabel = `${d.getMonth() + 1}/${d.getDate()}`;
|
|
366
|
+
dataPoints.push({
|
|
367
|
+
label: i % 5 === 0 ? dayLabel : '',
|
|
368
|
+
value: val,
|
|
369
|
+
queries,
|
|
370
|
+
tooltip: `$${val.toFixed(2)} on ${dayLabel}`,
|
|
371
|
+
highlight: i === 0,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (val > 0) {
|
|
375
|
+
listRows.push({
|
|
376
|
+
label: d.toLocaleDateString('en', { month: 'short', day: 'numeric' }),
|
|
377
|
+
cost: val,
|
|
378
|
+
queries,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const activeDays = dataPoints.filter(p => p.value > 0).length || 1;
|
|
384
|
+
const avgPerDay = total / activeDays;
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
dataPoints,
|
|
388
|
+
listRows: listRows.slice(0, 10),
|
|
389
|
+
totalUsd: Math.round(total * 1e6) / 1e6,
|
|
390
|
+
label: 'Last 30 days',
|
|
391
|
+
avgPerDay,
|
|
392
|
+
projected: avgPerDay * 30,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function _prepareAllData(history) {
|
|
397
|
+
const months = Object.entries(history).sort((a, b) => a[0].localeCompare(b[0]));
|
|
398
|
+
let total = 0;
|
|
399
|
+
let totalQueries = 0;
|
|
400
|
+
|
|
401
|
+
const dataPoints = months.map(([key, data]) => {
|
|
402
|
+
const val = data.totalUsd || 0;
|
|
403
|
+
total += val;
|
|
404
|
+
totalQueries += data.queries || 0;
|
|
405
|
+
const [y, m] = key.split('-');
|
|
406
|
+
const label = new Date(parseInt(y), parseInt(m) - 1).toLocaleString('en', { month: 'short' });
|
|
407
|
+
return {
|
|
408
|
+
label,
|
|
409
|
+
value: val,
|
|
410
|
+
queries: data.queries || 0,
|
|
411
|
+
tooltip: `$${val.toFixed(2)} in ${label} ${y}`,
|
|
412
|
+
highlight: key === new Date().toISOString().slice(0, 7),
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const listRows = months.reverse().slice(0, 10).map(([key, data]) => {
|
|
417
|
+
const [y, m] = key.split('-');
|
|
418
|
+
return {
|
|
419
|
+
label: new Date(parseInt(y), parseInt(m) - 1).toLocaleString('en', { month: 'short', year: 'numeric' }),
|
|
420
|
+
cost: data.totalUsd || 0,
|
|
421
|
+
queries: data.queries || 0,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
dataPoints,
|
|
427
|
+
listRows,
|
|
428
|
+
totalUsd: Math.round(total * 1e6) / 1e6,
|
|
429
|
+
label: 'All time',
|
|
430
|
+
avgPerDay: null,
|
|
431
|
+
projected: null,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Bar Chart (SVG) ──
|
|
436
|
+
|
|
437
|
+
function _renderBarChart(container, dataPoints) {
|
|
438
|
+
if (!container) return;
|
|
439
|
+
const W = 230, H = 72, PAD_BOTTOM = 14, PAD_TOP = 4;
|
|
440
|
+
const chartH = H - PAD_BOTTOM - PAD_TOP;
|
|
441
|
+
const barCount = dataPoints.length;
|
|
442
|
+
if (barCount === 0) { container.innerHTML = ''; return; }
|
|
443
|
+
|
|
444
|
+
const maxVal = Math.max(...dataPoints.map(p => p.value), 0.01);
|
|
445
|
+
const barW = Math.max(2, Math.min(8, (W - 8) / barCount - 1));
|
|
446
|
+
const gap = Math.max(1, ((W - 8) - barW * barCount) / barCount);
|
|
447
|
+
const totalW = barCount * (barW + gap);
|
|
448
|
+
const offsetX = (W - totalW) / 2;
|
|
449
|
+
|
|
450
|
+
let svg = `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg">`;
|
|
451
|
+
|
|
452
|
+
// Bars
|
|
453
|
+
dataPoints.forEach((p, i) => {
|
|
454
|
+
const x = offsetX + i * (barW + gap);
|
|
455
|
+
const h = Math.max(1, (p.value / maxVal) * chartH);
|
|
456
|
+
const y = PAD_TOP + chartH - h;
|
|
457
|
+
const fill = p.highlight ? 'var(--accent-gold)' : 'var(--accent-blue)';
|
|
458
|
+
const opacity = p.value > 0 ? (p.highlight ? 0.9 : 0.6) : 0.15;
|
|
459
|
+
const rx = Math.min(barW / 2, 2);
|
|
460
|
+
svg += `<rect class="cw-bar" x="${x}" y="${y}" width="${barW}" height="${h}" rx="${rx}" fill="${fill}" opacity="${opacity}" data-idx="${i}"><title>${p.tooltip}</title></rect>`;
|
|
461
|
+
|
|
462
|
+
// X-axis labels (sparse)
|
|
463
|
+
if (p.label) {
|
|
464
|
+
svg += `<text x="${x + barW / 2}" y="${H - 1}" text-anchor="middle" fill="var(--t-faint)" font-size="7" font-family="'JetBrains Mono', monospace">${p.label}</text>`;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
svg += '</svg>';
|
|
469
|
+
container.innerHTML = svg;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── List ──
|
|
473
|
+
|
|
474
|
+
function _renderList(container, rows) {
|
|
475
|
+
if (!container) return;
|
|
476
|
+
if (rows.length === 0) {
|
|
477
|
+
container.innerHTML = '<div class="cw-empty">No data yet</div>';
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
container.innerHTML = rows.map(r =>
|
|
481
|
+
`<div class="cw-row">
|
|
482
|
+
<span class="cw-row-label">${r.label}</span>
|
|
483
|
+
<span class="cw-row-cost">$${r.cost.toFixed(2)}</span>
|
|
484
|
+
<span class="cw-row-queries">${r.queries}q</span>
|
|
485
|
+
</div>`
|
|
486
|
+
).join('');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── CLI Scan ──
|
|
490
|
+
|
|
491
|
+
async function _scanCliSessions() {
|
|
492
|
+
const btn = _widget?.querySelector('.cw-scan');
|
|
493
|
+
if (!btn) return;
|
|
494
|
+
btn.classList.add('spinning');
|
|
495
|
+
btn.disabled = true;
|
|
496
|
+
try {
|
|
497
|
+
const res = await fetch('/api/claude-skin/cost/scan', { method: 'POST' });
|
|
498
|
+
const data = await res.json();
|
|
499
|
+
if (data.added > 0) {
|
|
500
|
+
// Refetch to get updated totals
|
|
501
|
+
await _fetchAndRender();
|
|
502
|
+
const total = _widget?.querySelector('.cw-total');
|
|
503
|
+
if (total) { total.classList.add('flash'); setTimeout(() => total.classList.remove('flash'), 800); }
|
|
504
|
+
}
|
|
505
|
+
} catch {} finally {
|
|
506
|
+
btn.classList.remove('spinning');
|
|
507
|
+
btn.disabled = false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Tabs ──
|
|
512
|
+
|
|
513
|
+
function _onTabClick(e) {
|
|
514
|
+
const btn = e.target.closest('.cw-tab');
|
|
515
|
+
if (!btn) return;
|
|
516
|
+
const tab = btn.dataset.tab;
|
|
517
|
+
if (tab === _activeTab) return;
|
|
518
|
+
_activeTab = tab;
|
|
519
|
+
_widget.querySelectorAll('.cw-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
|
|
520
|
+
_render();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Collapse ──
|
|
524
|
+
|
|
525
|
+
function _toggleCollapse() {
|
|
526
|
+
_collapsed = !_collapsed;
|
|
527
|
+
_widget.classList.toggle('collapsed', _collapsed);
|
|
528
|
+
_widget.querySelector('.cw-collapse').textContent = _collapsed ? '▸' : '▾';
|
|
529
|
+
storage.setItem(KEYS.COST_WIDGET_COLLAPSED, _collapsed ? 'true' : 'false');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── Drag ──
|
|
533
|
+
|
|
534
|
+
function _makeDraggable(header) {
|
|
535
|
+
header.style.cursor = 'grab';
|
|
536
|
+
|
|
537
|
+
header.addEventListener('mousedown', (e) => {
|
|
538
|
+
if (e.target.closest('.cw-collapse') || e.target.closest('.cw-scan')) return;
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
|
|
541
|
+
// If docked, undock on drag
|
|
542
|
+
if (_docked) _undock();
|
|
543
|
+
|
|
544
|
+
const rect = _widget.getBoundingClientRect();
|
|
545
|
+
_drag = {
|
|
546
|
+
startX: e.clientX,
|
|
547
|
+
startY: e.clientY,
|
|
548
|
+
origLeft: rect.left,
|
|
549
|
+
origTop: rect.top,
|
|
550
|
+
};
|
|
551
|
+
header.style.cursor = 'grabbing';
|
|
552
|
+
|
|
553
|
+
// Reset right/bottom positioning and switch to left/top
|
|
554
|
+
_widget.style.right = 'auto';
|
|
555
|
+
_widget.style.bottom = 'auto';
|
|
556
|
+
_widget.style.left = _drag.origLeft + 'px';
|
|
557
|
+
_widget.style.top = _drag.origTop + 'px';
|
|
558
|
+
|
|
559
|
+
const onMove = (ev) => {
|
|
560
|
+
if (!_drag) return;
|
|
561
|
+
const dx = ev.clientX - _drag.startX;
|
|
562
|
+
const dy = ev.clientY - _drag.startY;
|
|
563
|
+
let newLeft = _drag.origLeft + dx;
|
|
564
|
+
let newTop = _drag.origTop + dy;
|
|
565
|
+
// Constrain to viewport
|
|
566
|
+
const ww = _widget.offsetWidth;
|
|
567
|
+
const wh = _widget.offsetHeight;
|
|
568
|
+
newLeft = Math.max(0, Math.min(window.innerWidth - ww, newLeft));
|
|
569
|
+
newTop = Math.max(0, Math.min(window.innerHeight - wh, newTop));
|
|
570
|
+
_widget.style.left = newLeft + 'px';
|
|
571
|
+
_widget.style.top = newTop + 'px';
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const onUp = () => {
|
|
575
|
+
document.removeEventListener('mousemove', onMove);
|
|
576
|
+
document.removeEventListener('mouseup', onUp);
|
|
577
|
+
header.style.cursor = 'grab';
|
|
578
|
+
if (_drag) {
|
|
579
|
+
_savePos(parseInt(_widget.style.left), parseInt(_widget.style.top));
|
|
580
|
+
_drag = null;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
document.addEventListener('mousemove', onMove);
|
|
585
|
+
document.addEventListener('mouseup', onUp);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ── Persistence ──
|
|
590
|
+
|
|
591
|
+
function _loadPos() {
|
|
592
|
+
try {
|
|
593
|
+
const raw = storage.getItem(KEYS.COST_WIDGET_POS);
|
|
594
|
+
if (!raw) return null;
|
|
595
|
+
const pos = JSON.parse(raw);
|
|
596
|
+
return (typeof pos.left === 'number' && typeof pos.top === 'number') ? pos : null;
|
|
597
|
+
} catch { return null; }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function _savePos(left, top) {
|
|
601
|
+
storage.setItem(KEYS.COST_WIDGET_POS, JSON.stringify({ left, top }));
|
|
602
|
+
}
|