Skip to content

Commit a5309b6

Browse files
authored
feat(usage): improve usage overview styling and localization (#51951)
* feat(usage): add usage page styles and localization - Introduced a new `usage.css` file for styling the usage overview page. - Updated `en.ts` localization file to include new usage-related translations. - Refactored the usage rendering components to utilize the new localization strings for improved user experience. - Enhanced the `app-render-usage-tab.ts` to better structure the data passed to the rendering function. * feat(ui): enhance styling and functionality for usage overview and chat components - Updated `package.json` to include new built dependencies. - Refined CSS styles across various files to improve UI consistency and accessibility, including adjustments to color themes and layout structures. - Introduced new responsive grid layouts for usage overview and chat components, enhancing the user experience on different screen sizes. - Added functionality to hide context notices based on token freshness in chat view. - Implemented new rendering functions for usage statistics, improving data presentation and user interaction. * feat(usage): enhance usage overview styling and rendering options - Added new CSS classes for improved layout and styling of usage insight cards and error lists. - Updated rendering functions to support customizable class names for usage insight cards and error lists, enhancing flexibility in UI presentation. - Implemented a wide card layout and specific styling for error lists to improve visual clarity and user experience. * fix(ui): address review feedback on usage and chat layout * docs(changelog): add entry for usage UI improvements
1 parent 2b4c3c2 commit a5309b6

25 files changed

Lines changed: 3440 additions & 3198 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
6262
- Agents/compaction: notify users when followup auto-compaction starts and finishes, keeping those notices out of TTS and preserving reply threading for the real assistant reply. (#38805) Thanks @zidongdesign.
6363
- Models/OpenAI: switch the default OpenAI setup model to `openai/gpt-5.4`, keep Codex on `openai-codex/gpt-5.4`, and centralize OpenAI chat, image, TTS, transcription, and embedding defaults in one shared module so future default-model updates stay low-churn. Thanks @vincentkoc.
6464
- Memory/plugins: let the active memory plugin register its own system-prompt section while preserving cache-clear and snapshot-load prompt isolation. (#40126) Thanks @jarimustonen.
65+
- Control UI/usage: improve usage overview styling, localization, and responsive chat/context-notice presentation, including safer theme color handling and unclipped usage-header menus. (#51951) Thanks @BunsDev.
6566

6667
### Fixes
6768

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,14 +798,15 @@
798798
"yauzl": "3.2.1"
799799
},
800800
"onlyBuiltDependencies": [
801+
"@discordjs/opus",
801802
"@lydell/node-pty",
802803
"@matrix-org/matrix-sdk-crypto-nodejs",
803804
"@napi-rs/canvas",
804805
"@tloncorp/api",
806+
"@tloncorp/tlon-skill",
805807
"@whiskeysockets/baileys",
806808
"authenticate-pam",
807809
"esbuild",
808-
"koffi",
809810
"node-llama-cpp",
810811
"protobufjs",
811812
"sharp"

ui/src/i18n/locales/en.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,234 @@ export const en: TranslationMap = {
153153
noResults: "No results",
154154
},
155155
},
156+
usage: {
157+
page: {
158+
subtitle: "See where tokens go, when sessions spike, and what drives cost.",
159+
},
160+
common: {
161+
emptyValue: "—",
162+
unknown: "unknown",
163+
},
164+
loading: {
165+
title: "Usage Overview",
166+
badge: "Loading",
167+
},
168+
metrics: {
169+
tokens: "Tokens",
170+
cost: "Cost",
171+
session: "session",
172+
sessions: "sessions",
173+
},
174+
presets: {
175+
today: "Today",
176+
last7d: "7d",
177+
last30d: "30d",
178+
},
179+
filters: {
180+
title: "Filters",
181+
to: "to",
182+
startDate: "Start date",
183+
endDate: "End date",
184+
timeZone: "Time zone",
185+
timeZoneLocal: "Local",
186+
timeZoneUtc: "UTC",
187+
pin: "Pin",
188+
pinned: "Pinned",
189+
unpin: "Unpin filters",
190+
selectAll: "Select All",
191+
clear: "Clear",
192+
clearAll: "Clear All",
193+
remove: "Remove filter",
194+
all: "All",
195+
days: "Days",
196+
hours: "Hours",
197+
session: "Session",
198+
agent: "Agent",
199+
channel: "Channel",
200+
provider: "Provider",
201+
model: "Model",
202+
tool: "Tool",
203+
daysCount: "{count} days",
204+
hoursCount: "{count} hours",
205+
sessionsCount: "{count} sessions",
206+
},
207+
query: {
208+
placeholder:
209+
"Filter sessions (e.g. key:agent:main:cron* model:gpt-4o has:errors minTokens:2000)",
210+
apply: "Filter (client-side)",
211+
matching: "{shown} of {total} sessions match",
212+
inRange: "{total} sessions in range",
213+
tip: "Tip: use filters or click bars to refine days.",
214+
},
215+
export: {
216+
label: "Export",
217+
sessionsCsv: "Sessions CSV",
218+
dailyCsv: "Daily CSV",
219+
json: "JSON",
220+
},
221+
empty: {
222+
title: "Start with a date range",
223+
subtitle:
224+
"Load usage data to compare costs, inspect sessions, and drill into timelines without leaving the dashboard.",
225+
hint: "Select a date range and click Refresh to load usage.",
226+
noData: "No data",
227+
featureOverview: "Overview cards",
228+
featureSessions: "Session ranking",
229+
featureTimeline: "Timeline drilldown",
230+
},
231+
daily: {
232+
title: "Daily Usage",
233+
total: "Total",
234+
byType: "By Type",
235+
tokensTitle: "Daily Token Usage",
236+
costTitle: "Daily Cost",
237+
},
238+
breakdown: {
239+
output: "Output",
240+
input: "Input",
241+
cacheWrite: "Cache Write",
242+
cacheRead: "Cache Read",
243+
total: "Total",
244+
tokensByType: "Tokens by Type",
245+
costByType: "Cost by Type",
246+
},
247+
overview: {
248+
title: "Usage Overview",
249+
messages: "Messages",
250+
messagesHint: "Total user and assistant messages in range.",
251+
messagesAbbrev: "msgs",
252+
user: "user",
253+
assistant: "assistant",
254+
toolCalls: "Tool Calls",
255+
toolCallsHint: "Total tool call count across sessions.",
256+
toolsUsed: "tools used",
257+
errors: "Errors",
258+
errorsHint: "Total message and tool errors in range.",
259+
toolResults: "tool results",
260+
avgTokens: "Avg Tokens / Msg",
261+
avgTokensHint: "Average tokens per message in this range.",
262+
avgCost: "Avg Cost / Msg",
263+
avgCostHint: "Average cost per message when providers report costs.",
264+
avgCostHintMissing:
265+
"Average cost per message when providers report costs. Cost data is missing for some or all sessions in this range.",
266+
acrossMessages: "Across {count} messages",
267+
sessions: "Sessions",
268+
sessionsHint: "Distinct sessions in the range.",
269+
sessionsInRange: "of {count} in range",
270+
throughput: "Throughput",
271+
throughputHint: "Throughput shows tokens per minute over active time. Higher is better.",
272+
tokensPerMinute: "tok/min",
273+
perMinute: "/ min",
274+
errorRate: "Error Rate",
275+
errorHint: "Error rate = errors / total messages. Lower is better.",
276+
avgSession: "avg session",
277+
cacheHitRate: "Cache Hit Rate",
278+
cacheHint: "Cache hit rate = cache read / (input + cache read). Higher is better.",
279+
cached: "cached",
280+
prompt: "prompt",
281+
calls: "calls",
282+
topModels: "Top Models",
283+
topProviders: "Top Providers",
284+
topTools: "Top Tools",
285+
topAgents: "Top Agents",
286+
topChannels: "Top Channels",
287+
peakErrorDays: "Peak Error Days",
288+
peakErrorHours: "Peak Error Hours",
289+
noModelData: "No model data",
290+
noProviderData: "No provider data",
291+
noToolCalls: "No tool calls",
292+
noAgentData: "No agent data",
293+
noChannelData: "No channel data",
294+
noErrorData: "No error data",
295+
},
296+
sessions: {
297+
title: "Sessions",
298+
shown: "{count} shown",
299+
total: "{count} total",
300+
avg: "avg",
301+
all: "All",
302+
recent: "Recently viewed",
303+
recentShort: "Recent",
304+
sort: "Sort",
305+
ascending: "Ascending",
306+
descending: "Descending",
307+
clearSelection: "Clear Selection",
308+
noRecent: "No recent sessions",
309+
noneInRange: "No sessions in range",
310+
more: "+{count} more",
311+
selected: "Selected ({count})",
312+
copy: "Copy",
313+
copyName: "Copy session name",
314+
limitReached: "Showing first 1,000 sessions. Narrow date range for complete results.",
315+
},
316+
details: {
317+
emptyTitle: "Select a session",
318+
emptySubtitle:
319+
"Pick a session to inspect the timeline, conversation, and prompt context for a single run.",
320+
emptyTimeline: "Turn-by-turn timeline",
321+
emptyConversation: "Conversation logs",
322+
emptyContext: "Prompt breakdown",
323+
noUsageData: "No usage data for this session.",
324+
duration: "Duration",
325+
modelMix: "Model Mix",
326+
filtered: "(filtered)",
327+
close: "Close session details",
328+
noTimeline: "No timeline data",
329+
noDataInRange: "No data in range",
330+
usageOverTime: "Usage Over Time",
331+
reset: "Reset",
332+
perTurn: "Per Turn",
333+
cumulative: "Cumulative",
334+
turnRange: "Turns {start}–{end} of {total}",
335+
assistantOutputTokens: "Assistant output tokens",
336+
userToolInputTokens: "User + tool input tokens",
337+
tokensWrittenToCache: "Tokens written to cache",
338+
tokensReadFromCache: "Tokens read from cache",
339+
noContextData: "No context data",
340+
systemPromptBreakdown: "System Prompt Breakdown",
341+
collapse: "Collapse",
342+
collapseAll: "Collapse All",
343+
expandAll: "Expand All",
344+
baseContextPerMessage: "Base context per message",
345+
system: "System",
346+
systemShort: "Sys",
347+
skills: "Skills",
348+
tools: "Tools",
349+
files: "Files",
350+
ofInput: "of input",
351+
of: "of",
352+
timelineFiltered: "timeline filtered",
353+
conversation: "Conversation",
354+
noMessages: "No messages",
355+
tool: "Tool",
356+
toolResult: "Tool result",
357+
hasTools: "Has tools",
358+
searchConversation: "Search conversation",
359+
you: "You",
360+
noMessagesMatch: "No messages match the filters.",
361+
},
362+
mosaic: {
363+
title: "Activity by Time",
364+
subtitleEmpty: "Estimates require session timestamps.",
365+
subtitle: "Estimated from session spans (first/last activity). Time zone: {zone}.",
366+
noTimelineData: "No timeline data yet.",
367+
dayOfWeek: "Day of Week",
368+
midnight: "Midnight",
369+
fourAm: "4am",
370+
eightAm: "8am",
371+
noon: "Noon",
372+
fourPm: "4pm",
373+
eightPm: "8pm",
374+
legend: "Low → High token density",
375+
sun: "Sun",
376+
mon: "Mon",
377+
tue: "Tue",
378+
wed: "Wed",
379+
thu: "Thu",
380+
fri: "Fri",
381+
sat: "Sat",
382+
},
383+
},
156384
login: {
157385
subtitle: "Gateway Dashboard",
158386
passwordPlaceholder: "optional",

ui/src/styles.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
@import "./styles/components.css";
55
@import "./styles/chat.css";
66
@import "./styles/config.css";
7+
@import "./styles/usage.css";

ui/src/styles/base.css

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,29 @@
218218
}
219219

220220
:root[data-theme="dash"] {
221-
/* Accent — warm amber on chocolate */
222-
--ring: #d4915c;
223-
--accent: #d4915c;
224-
--accent-hover: #e0a876;
225-
--accent-muted: #d4915c;
226-
--accent-subtle: rgba(212, 145, 92, 0.14);
227-
--accent-glow: rgba(212, 145, 92, 0.22);
228-
--primary: #d4915c;
221+
/*
222+
* Accent — chocolate brown on deep cocoa
223+
*
224+
* WCAG 2.1 AA audit (bg = #1a1210, relative luminance ≈ 0.0069):
225+
* --accent #b47840 L ≈ 0.235 contrast ≈ 5.0:1 AA text ✓ (floor: #a8703c → 4.4:1, fails)
226+
* --accent-hover #c8885a L ≈ 0.306 contrast ≈ 6.3:1 ✓
227+
* --primary-foreground #1a1210 on #b47840 button: 5.0:1 AA ✓
228+
* focus-ring at 80% opacity: ≈ 3.5:1 against bg ✓ (non-text 3:1 required)
229+
*/
230+
--ring: #b47840;
231+
--accent: #b47840;
232+
--accent-hover: #c8885a;
233+
--accent-muted: #b47840;
234+
--accent-subtle: rgba(180, 120, 64, 0.16);
235+
--accent-glow: rgba(180, 120, 64, 0.24);
236+
--primary: #b47840;
229237
--primary-foreground: #1a1210;
230238

239+
/* Focus — match chocolate accent, slightly higher ring opacity for dark bg visibility */
240+
--focus: rgba(180, 120, 64, 0.2);
241+
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent);
242+
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow);
243+
231244
/* Surfaces — deep cocoa tones */
232245
--bg: #1a1210;
233246
--bg-accent: #201816;
@@ -261,9 +274,9 @@
261274

262275
--secondary: #221a16;
263276
--secondary-foreground: #ece0d8;
264-
--accent-2: #c8a06e;
265-
--accent-2-muted: rgba(200, 160, 110, 0.7);
266-
--accent-2-subtle: rgba(200, 160, 110, 0.1);
277+
--accent-2: #a88050;
278+
--accent-2-muted: rgba(168, 128, 80, 0.7);
279+
--accent-2-subtle: rgba(168, 128, 80, 0.12);
267280

268281
--shadow-sm: 0 1px 2px rgba(10, 6, 4, 0.35);
269282
--shadow-md: 0 4px 16px rgba(10, 6, 4, 0.45);
@@ -273,14 +286,23 @@
273286
}
274287

275288
:root[data-theme="dash-light"] {
276-
/* Accent — rich brown on parchment */
277-
--ring: #7a522e;
278-
--accent: #7a522e;
279-
--accent-hover: #6b4526;
280-
--accent-muted: #7a522e;
281-
--accent-subtle: rgba(122, 82, 46, 0.1);
282-
--accent-glow: rgba(122, 82, 46, 0.14);
283-
--primary: #7a522e;
289+
/*
290+
* Accent — deep chocolate on warm parchment
291+
*
292+
* WCAG 2.1 AA audit (bg = #f7f2ec, relative luminance ≈ 0.893):
293+
* --accent #6e4828 L ≈ 0.081 contrast ≈ 7.2:1 AAA text ✓
294+
* --accent-hover #5c3c20 L ≈ 0.054 contrast ≈ 8.9:1 ✓
295+
* --primary-foreground #ffffff on #6e4828 button: 8.0:1 AAA ✓
296+
* focus-ring at 70% opacity: ≈ 3.4:1 against parchment bg ✓
297+
*/
298+
--ring: #6e4828;
299+
--accent: #6e4828;
300+
--accent-hover: #5c3c20;
301+
--accent-muted: #6e4828;
302+
--accent-subtle: rgba(110, 72, 40, 0.12);
303+
--accent-glow: rgba(110, 72, 40, 0.16);
304+
--primary: #6e4828;
305+
--primary-foreground: #ffffff;
284306

285307
/* Surfaces — warm parchment tones */
286308
--bg: #f7f2ec;
@@ -316,9 +338,9 @@
316338

317339
--secondary: #f0e8e0;
318340
--secondary-foreground: #4a3828;
319-
--accent-2: #7a5c38;
320-
--accent-2-muted: rgba(122, 92, 56, 0.75);
321-
--accent-2-subtle: rgba(122, 92, 56, 0.08);
341+
--accent-2: #6a4e30;
342+
--accent-2-muted: rgba(106, 78, 48, 0.75);
343+
--accent-2-subtle: rgba(106, 78, 48, 0.1);
322344

323345
--shadow-sm: 0 1px 2px rgba(60, 40, 20, 0.06);
324346
--shadow-md: 0 4px 12px rgba(60, 40, 20, 0.08);
@@ -528,13 +550,20 @@ select {
528550
@keyframes glow-pulse {
529551
0%,
530552
100% {
531-
box-shadow: 0 0 0 rgba(255, 92, 92, 0);
553+
box-shadow: 0 0 0 transparent;
532554
}
533555
50% {
534556
box-shadow: 0 0 20px var(--accent-glow);
535557
}
536558
}
537559

560+
@media (prefers-reduced-motion: reduce) {
561+
.glow-pulse,
562+
[class*="glow-pulse"] {
563+
animation: none !important;
564+
}
565+
}
566+
538567
/* Stagger animation delays for grouped elements */
539568
.stagger-1 {
540569
animation-delay: 0ms;

0 commit comments

Comments
 (0)