Replace the monolithic single-file dashboards (dashboard.html,
dashboard-preact.html) with a proper modular directory structure:
dashboard/
index.html - Entry point, loads main.js
main.js - App bootstrap, mounts <App> to #root
styles.css - Global styles (dark theme, typography)
components/
App.js - Root component, state management, polling
Header.js - Top bar with refresh/timing info
Sidebar.js - Project tree navigation
SessionCard.js - Individual session card with status/actions
SessionGroup.js - Group sessions by project path
Modal.js - Full conversation viewer overlay
ChatMessages.js - Message list with role styling
MessageBubble.js - Individual message with markdown
QuestionBlock.js - User question input with quick options
EmptyState.js - "No sessions" placeholder
OptionButton.js - Quick response button component
SimpleInput.js - Text input with send button
lib/
preact.js - Preact + htm ESM bundle (CDN shim)
markdown.js - Lightweight markdown-to-HTML renderer
utils/
api.js - fetch wrappers for /api/* endpoints
formatting.js - Time formatting, truncation helpers
status.js - Session status logic, action availability
This structure enables:
- Browser-native ES modules (no build step required)
- Component reuse and isolation
- Easier styling and theming
- IDE support for component navigation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
67 lines
2.3 KiB
JavaScript
67 lines
2.3 KiB
JavaScript
// Formatting utilities
|
|
|
|
export function formatDuration(isoStart) {
|
|
if (!isoStart) return '';
|
|
const start = new Date(isoStart);
|
|
const now = new Date();
|
|
const mins = Math.max(0, Math.floor((now - start) / 60000));
|
|
if (mins < 60) return mins + 'm';
|
|
const hrs = Math.floor(mins / 60);
|
|
const remainMins = mins % 60;
|
|
return hrs + 'h ' + remainMins + 'm';
|
|
}
|
|
|
|
export function formatTime(isoTime) {
|
|
if (!isoTime) return '';
|
|
const date = new Date(isoTime);
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
}
|
|
|
|
export function formatTokenCount(value) {
|
|
if (!Number.isFinite(value)) return '';
|
|
return Math.round(value).toLocaleString('en-US');
|
|
}
|
|
|
|
export function getContextUsageSummary(usage) {
|
|
if (!usage || typeof usage !== 'object') return null;
|
|
|
|
const current = Number(usage.current_tokens);
|
|
const windowTokens = Number(usage.window_tokens);
|
|
const sessionTotal = usage.session_total_tokens != null ? Number(usage.session_total_tokens) : null;
|
|
const hasCurrent = Number.isFinite(current) && current > 0;
|
|
const hasWindow = Number.isFinite(windowTokens) && windowTokens > 0;
|
|
const hasSessionTotal = sessionTotal != null && Number.isFinite(sessionTotal) && sessionTotal > 0;
|
|
|
|
if (hasCurrent && hasWindow) {
|
|
const percent = (current / windowTokens) * 100;
|
|
return {
|
|
headline: `${percent >= 10 ? percent.toFixed(0) : percent.toFixed(1)}% ctx`,
|
|
detail: `${formatTokenCount(current)} / ${formatTokenCount(windowTokens)}`,
|
|
trail: hasSessionTotal ? `Σ ${formatTokenCount(sessionTotal)}` : '',
|
|
title: `Context window usage: ${formatTokenCount(current)} / ${formatTokenCount(windowTokens)} tokens`,
|
|
};
|
|
}
|
|
|
|
if (hasCurrent) {
|
|
const inputTokens = Number(usage.input_tokens);
|
|
const outputTokens = Number(usage.output_tokens);
|
|
const hasInput = Number.isFinite(inputTokens);
|
|
const hasOutput = Number.isFinite(outputTokens);
|
|
const ioDetail = hasInput || hasOutput
|
|
? ` • in ${formatTokenCount(hasInput ? inputTokens : 0)} out ${formatTokenCount(hasOutput ? outputTokens : 0)}`
|
|
: '';
|
|
return {
|
|
headline: 'Ctx usage',
|
|
detail: `${formatTokenCount(current)} tok${ioDetail}`,
|
|
trail: '',
|
|
title: `Token usage: ${formatTokenCount(current)} tokens`,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|