openclaw-agent-dashboard 1.0.4

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.
Files changed (111) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/README.md +302 -0
  3. package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
  4. package/docs/RELEASE-LATEST.md +189 -0
  5. package/docs/RELEASE-MODEL-CONFIG.md +95 -0
  6. package/docs/release-guide.md +259 -0
  7. package/docs/release-operations-manual.md +167 -0
  8. package/docs/specs/tr3-install-system.md +580 -0
  9. package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
  10. package/frontend/index.html +12 -0
  11. package/frontend/package-lock.json +1240 -0
  12. package/frontend/package.json +19 -0
  13. package/frontend/src/App.vue +331 -0
  14. package/frontend/src/components/AgentCard.vue +796 -0
  15. package/frontend/src/components/AgentConfigPanel.vue +539 -0
  16. package/frontend/src/components/AgentDetailPanel.vue +738 -0
  17. package/frontend/src/components/ErrorAnalysisView.vue +546 -0
  18. package/frontend/src/components/ErrorCenterPanel.vue +844 -0
  19. package/frontend/src/components/PerformanceMonitor.vue +515 -0
  20. package/frontend/src/components/SettingsPanel.vue +236 -0
  21. package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
  22. package/frontend/src/components/chain/ChainEdge.vue +85 -0
  23. package/frontend/src/components/chain/ChainNode.vue +166 -0
  24. package/frontend/src/components/chain/TaskChainView.vue +425 -0
  25. package/frontend/src/components/chain/index.ts +3 -0
  26. package/frontend/src/components/chain/types.ts +70 -0
  27. package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
  28. package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
  29. package/frontend/src/components/performance/PerformancePanel.vue +119 -0
  30. package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
  31. package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
  32. package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
  33. package/frontend/src/components/timeline/TimelineRound.vue +135 -0
  34. package/frontend/src/components/timeline/TimelineStep.vue +691 -0
  35. package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
  36. package/frontend/src/components/timeline/TimelineView.vue +540 -0
  37. package/frontend/src/components/timeline/index.ts +5 -0
  38. package/frontend/src/components/timeline/types.ts +120 -0
  39. package/frontend/src/composables/index.ts +7 -0
  40. package/frontend/src/composables/useDebounce.ts +48 -0
  41. package/frontend/src/composables/useRealtime.ts +52 -0
  42. package/frontend/src/composables/useState.ts +52 -0
  43. package/frontend/src/composables/useThrottle.ts +46 -0
  44. package/frontend/src/composables/useVirtualScroll.ts +106 -0
  45. package/frontend/src/main.ts +4 -0
  46. package/frontend/src/managers/EventDispatcher.ts +127 -0
  47. package/frontend/src/managers/RealtimeDataManager.ts +293 -0
  48. package/frontend/src/managers/StateManager.ts +128 -0
  49. package/frontend/src/managers/index.ts +5 -0
  50. package/frontend/src/types/collaboration.ts +135 -0
  51. package/frontend/src/types/index.ts +20 -0
  52. package/frontend/src/types/performance.ts +105 -0
  53. package/frontend/src/types/task.ts +38 -0
  54. package/frontend/vite.config.ts +18 -0
  55. package/package.json +22 -0
  56. package/plugin/README.md +99 -0
  57. package/plugin/config.json.example +1 -0
  58. package/plugin/index.js +250 -0
  59. package/plugin/openclaw.plugin.json +17 -0
  60. package/plugin/package.json +21 -0
  61. package/scripts/build-plugin.js +67 -0
  62. package/scripts/bundle.sh +62 -0
  63. package/scripts/install-plugin.sh +162 -0
  64. package/scripts/install-python-deps.js +346 -0
  65. package/scripts/install-python-deps.sh +226 -0
  66. package/scripts/install.js +512 -0
  67. package/scripts/install.sh +367 -0
  68. package/scripts/lib/common.js +490 -0
  69. package/scripts/lib/common.sh +137 -0
  70. package/scripts/release-pack.sh +110 -0
  71. package/scripts/start.js +50 -0
  72. package/scripts/test_available_models.py +284 -0
  73. package/scripts/test_websocket_ping.py +44 -0
  74. package/src/backend/agents.py +73 -0
  75. package/src/backend/api/__init__.py +1 -0
  76. package/src/backend/api/agent_config_api.py +90 -0
  77. package/src/backend/api/agents.py +73 -0
  78. package/src/backend/api/agents_config.py +75 -0
  79. package/src/backend/api/chains.py +126 -0
  80. package/src/backend/api/collaboration.py +902 -0
  81. package/src/backend/api/debug_paths.py +39 -0
  82. package/src/backend/api/error_analysis.py +146 -0
  83. package/src/backend/api/errors.py +281 -0
  84. package/src/backend/api/performance.py +784 -0
  85. package/src/backend/api/subagents.py +770 -0
  86. package/src/backend/api/timeline.py +144 -0
  87. package/src/backend/api/websocket.py +251 -0
  88. package/src/backend/collaboration.py +405 -0
  89. package/src/backend/data/__init__.py +1 -0
  90. package/src/backend/data/agent_config_manager.py +270 -0
  91. package/src/backend/data/chain_reader.py +299 -0
  92. package/src/backend/data/config_reader.py +153 -0
  93. package/src/backend/data/error_analyzer.py +430 -0
  94. package/src/backend/data/session_reader.py +445 -0
  95. package/src/backend/data/subagent_reader.py +244 -0
  96. package/src/backend/data/task_history.py +118 -0
  97. package/src/backend/data/timeline_reader.py +981 -0
  98. package/src/backend/errors.py +63 -0
  99. package/src/backend/main.py +89 -0
  100. package/src/backend/mechanism_reader.py +131 -0
  101. package/src/backend/mechanisms.py +32 -0
  102. package/src/backend/performance.py +474 -0
  103. package/src/backend/requirements.txt +5 -0
  104. package/src/backend/session_reader.py +238 -0
  105. package/src/backend/status/__init__.py +1 -0
  106. package/src/backend/status/error_detector.py +122 -0
  107. package/src/backend/status/status_calculator.py +301 -0
  108. package/src/backend/status_calculator.py +121 -0
  109. package/src/backend/subagent_reader.py +229 -0
  110. package/src/backend/watchers/__init__.py +4 -0
  111. package/src/backend/watchers/file_watcher.py +159 -0
@@ -0,0 +1,683 @@
1
+ <template>
2
+ <section class="token-analysis">
3
+ <div class="section-header">
4
+ <h2>Token 分析</h2>
5
+ <div class="time-range-selector">
6
+ <button
7
+ v-for="range in timeRanges"
8
+ :key="range.value"
9
+ :class="['range-btn', { active: selectedRange === range.value }]"
10
+ @click="selectedRange = range.value"
11
+ >
12
+ {{ range.label }}
13
+ </button>
14
+ </div>
15
+ </div>
16
+
17
+ <div v-if="loading" class="loading-state">
18
+ <div class="spinner"></div>
19
+ <span>加载 Token 数据...</span>
20
+ </div>
21
+
22
+ <div v-else-if="error" class="error-state">
23
+ <span class="error-icon">⚠️</span>
24
+ <span>{{ error }}</span>
25
+ <button @click="load" class="retry-btn">重试</button>
26
+ </div>
27
+
28
+ <template v-else>
29
+ <!-- Summary Cards -->
30
+ <div class="summary-cards">
31
+ <div class="summary-card">
32
+ <div class="card-icon">📥</div>
33
+ <div class="card-content">
34
+ <div class="card-label">Input</div>
35
+ <div class="card-value">{{ formatNum(data.summary?.input) }}</div>
36
+ </div>
37
+ </div>
38
+ <div class="summary-card">
39
+ <div class="card-icon">📤</div>
40
+ <div class="card-content">
41
+ <div class="card-label">Output</div>
42
+ <div class="card-value">{{ formatNum(data.summary?.output) }}</div>
43
+ </div>
44
+ </div>
45
+ <div class="summary-card highlight">
46
+ <div class="card-icon">💾</div>
47
+ <div class="card-content">
48
+ <div class="card-label">Cache Read</div>
49
+ <div class="card-value">{{ formatNum(data.summary?.cacheRead) }}</div>
50
+ <div class="card-sub">命中率 {{ formatPercent(data.summary?.cacheHitRate) }}</div>
51
+ </div>
52
+ </div>
53
+ <div class="summary-card">
54
+ <div class="card-icon">📝</div>
55
+ <div class="card-content">
56
+ <div class="card-label">Cache Write</div>
57
+ <div class="card-value">{{ formatNum(data.summary?.cacheWrite) }}</div>
58
+ </div>
59
+ </div>
60
+ <div class="summary-card cost">
61
+ <div class="card-icon">💰</div>
62
+ <div class="card-content">
63
+ <div class="card-label">估算成本</div>
64
+ <div class="card-value">${{ formatCost(data.cost?.total) }}</div>
65
+ <div v-if="data.cost?.saved > 0" class="card-sub saved">节省 ${{ formatCost(data.cost?.saved) }}</div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Trend Chart (for 20m/1h/24h range) -->
71
+ <div v-if="data.trend && showTrend" class="trend-section">
72
+ <h3>Token 消耗趋势</h3>
73
+ <div class="trend-chart">
74
+ <div class="trend-bars">
75
+ <div
76
+ v-for="(timestamp, i) in data.trend.timestamps"
77
+ :key="i"
78
+ class="trend-bar-group"
79
+ >
80
+ <div class="trend-bar input" :style="{ height: getBarHeight(data.trend.input[i], maxTrendValue) + '%' }">
81
+ <span v-if="data.trend.input[i] > 0" class="bar-tooltip">In: {{ formatNum(data.trend.input[i]) }}</span>
82
+ </div>
83
+ <div class="trend-bar output" :style="{ height: getBarHeight(data.trend.output[i], maxTrendValue) + '%' }">
84
+ <span v-if="data.trend.output[i] > 0" class="bar-tooltip">Out: {{ formatNum(data.trend.output[i]) }}</span>
85
+ </div>
86
+ <span class="trend-time">{{ formatTrendTime(timestamp) }}</span>
87
+ </div>
88
+ </div>
89
+ <div class="trend-legend">
90
+ <span class="legend-item"><span class="legend-color input"></span> Input</span>
91
+ <span class="legend-item"><span class="legend-color output"></span> Output</span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- View Toggle -->
97
+ <div class="view-toggle">
98
+ <button :class="{ active: viewMode === 'table' }" @click="viewMode = 'table'">📊 表格</button>
99
+ <button :class="{ active: viewMode === 'chart' }" @click="viewMode = 'chart'">📈 图表</button>
100
+ </div>
101
+
102
+ <!-- Table View -->
103
+ <div v-if="viewMode === 'table'" class="by-agent-table">
104
+ <table>
105
+ <thead>
106
+ <tr>
107
+ <th>Agent</th>
108
+ <th>Input</th>
109
+ <th>Output</th>
110
+ <th>Cache</th>
111
+ <th>总计</th>
112
+ <th>占比</th>
113
+ </tr>
114
+ </thead>
115
+ <tbody>
116
+ <tr v-for="agent in sortedAgents" :key="agent.agent">
117
+ <td class="agent-name">{{ agent.agent }}</td>
118
+ <td>{{ formatNum(agent.input) }}</td>
119
+ <td>{{ formatNum(agent.output) }}</td>
120
+ <td>{{ formatNum(agent.cacheRead + agent.cacheWrite) }}</td>
121
+ <td class="total-col">{{ formatNum(agent.total) }}</td>
122
+ <td>
123
+ <div class="percent-bar">
124
+ <div class="percent-fill" :style="{ width: (agent.percent * 100) + '%' }"></div>
125
+ <span class="percent-text">{{ formatPercent(agent.percent) }}</span>
126
+ </div>
127
+ </td>
128
+ </tr>
129
+ </tbody>
130
+ <tfoot>
131
+ <tr>
132
+ <td><strong>合计</strong></td>
133
+ <td>{{ formatNum(data.summary?.input) }}</td>
134
+ <td>{{ formatNum(data.summary?.output) }}</td>
135
+ <td>{{ formatNum((data.summary?.cacheRead || 0) + (data.summary?.cacheWrite || 0)) }}</td>
136
+ <td class="total-col"><strong>{{ formatNum(data.summary?.total) }}</strong></td>
137
+ <td>100%</td>
138
+ </tr>
139
+ </tfoot>
140
+ </table>
141
+ </div>
142
+
143
+ <!-- Chart View -->
144
+ <div v-else class="by-agent-chart">
145
+ <h3>Token 消耗分布</h3>
146
+ <div class="bar-chart">
147
+ <div v-for="agent in sortedAgents" :key="agent.agent" class="bar-row">
148
+ <div class="bar-label">{{ agent.agent }}</div>
149
+ <div class="bar-container">
150
+ <div class="bar" :style="{ width: (agent.percent * 100) + '%' }">
151
+ <span class="bar-value">{{ formatNum(agent.total) }}</span>
152
+ </div>
153
+ </div>
154
+ <div class="bar-percent">{{ formatPercent(agent.percent) }}</div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </template>
159
+ </section>
160
+ </template>
161
+
162
+ <script setup lang="ts">
163
+ import { ref, computed, onMounted, watch } from 'vue'
164
+ import type { TokenAnalysisData, AgentTokenData } from '../types/performance'
165
+
166
+ const data = ref<TokenAnalysisData>({
167
+ summary: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cacheHitRate: 0 },
168
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, saved: 0, savedPercent: 0 },
169
+ byAgent: [],
170
+ trend: null
171
+ })
172
+ const loading = ref(false)
173
+ const error = ref('')
174
+ const viewMode = ref<'table' | 'chart'>('table')
175
+ const selectedRange = ref<string>('all')
176
+
177
+ const timeRanges = [
178
+ { value: 'all', label: '全部' },
179
+ { value: '24h', label: '24小时' },
180
+ { value: '1h', label: '1小时' },
181
+ { value: '20m', label: '20分钟' }
182
+ ]
183
+
184
+ const showTrend = computed(() => selectedRange.value !== 'all' && data.value.trend)
185
+
186
+ const maxTrendValue = computed(() => {
187
+ if (!data.value.trend) return 1
188
+ const allValues = [...data.value.trend.input, ...data.value.trend.output]
189
+ return Math.max(...allValues, 1)
190
+ })
191
+
192
+ const sortedAgents = computed(() => {
193
+ return [...(data.value.byAgent || [])].sort((a, b) => b.total - a.total)
194
+ })
195
+
196
+ function getBarHeight(value: number, max: number): number {
197
+ return Math.max((value / max) * 100, 2)
198
+ }
199
+
200
+ function formatTrendTime(timestamp: number): string {
201
+ const date = new Date(timestamp)
202
+ if (selectedRange.value === '24h') {
203
+ return date.toLocaleString('zh-CN', { hour: '2-digit', hour12: false }) + ':00'
204
+ }
205
+ return date.toLocaleString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false })
206
+ }
207
+
208
+ function formatNum(n: number | undefined) {
209
+ if (n == null) return '0'
210
+ if (n >= 1000000) return (n / 1000000).toFixed(2) + 'M'
211
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
212
+ return n.toLocaleString()
213
+ }
214
+
215
+ function formatPercent(n: number | undefined) {
216
+ if (n == null) return '0%'
217
+ return (n * 100).toFixed(1) + '%'
218
+ }
219
+
220
+ function formatCost(n: number | undefined) {
221
+ if (n == null) return '0.00'
222
+ if (n >= 1) return n.toFixed(2)
223
+ return n.toFixed(4)
224
+ }
225
+
226
+ async function load() {
227
+ loading.value = true
228
+ error.value = ''
229
+ try {
230
+ const res = await fetch(`/api/tokens/analysis?range=${selectedRange.value}`)
231
+ if (res.ok) {
232
+ data.value = await res.json()
233
+ } else {
234
+ error.value = '加载失败'
235
+ }
236
+ } catch (e) {
237
+ error.value = String(e)
238
+ } finally {
239
+ loading.value = false
240
+ }
241
+ }
242
+
243
+ onMounted(load)
244
+
245
+ watch(selectedRange, load)
246
+ </script>
247
+
248
+ <style scoped>
249
+ .token-analysis {
250
+ background: white;
251
+ border-radius: 8px;
252
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
253
+ padding: 1.5rem;
254
+ }
255
+
256
+ .section-header {
257
+ display: flex;
258
+ justify-content: space-between;
259
+ align-items: center;
260
+ margin-bottom: 1.5rem;
261
+ flex-wrap: wrap;
262
+ gap: 1rem;
263
+ }
264
+
265
+ .section-header h2 {
266
+ margin: 0;
267
+ font-size: 1.3rem;
268
+ color: #333;
269
+ }
270
+
271
+ .time-range-selector {
272
+ display: flex;
273
+ gap: 0.5rem;
274
+ }
275
+
276
+ .range-btn {
277
+ padding: 0.5rem 1rem;
278
+ border: 1px solid #e5e7eb;
279
+ border-radius: 6px;
280
+ background: white;
281
+ font-size: 0.85rem;
282
+ cursor: pointer;
283
+ transition: all 0.2s;
284
+ }
285
+
286
+ .range-btn:hover {
287
+ border-color: #4a9eff;
288
+ }
289
+
290
+ .range-btn.active {
291
+ background: #4a9eff;
292
+ color: white;
293
+ border-color: #4a9eff;
294
+ }
295
+
296
+ .loading-state,
297
+ .error-state {
298
+ display: flex;
299
+ flex-direction: column;
300
+ align-items: center;
301
+ justify-content: center;
302
+ height: 200px;
303
+ gap: 1rem;
304
+ color: #6b7280;
305
+ }
306
+
307
+ .spinner {
308
+ width: 32px;
309
+ height: 32px;
310
+ border: 3px solid #e5e7eb;
311
+ border-top-color: #4a9eff;
312
+ border-radius: 50%;
313
+ animation: spin 1s linear infinite;
314
+ }
315
+
316
+ @keyframes spin {
317
+ to { transform: rotate(360deg); }
318
+ }
319
+
320
+ .retry-btn {
321
+ padding: 0.5rem 1rem;
322
+ background: #4a9eff;
323
+ color: white;
324
+ border: none;
325
+ border-radius: 4px;
326
+ cursor: pointer;
327
+ }
328
+
329
+ .error-icon {
330
+ font-size: 2rem;
331
+ }
332
+
333
+ /* Summary Cards */
334
+ .summary-cards {
335
+ display: grid;
336
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
337
+ gap: 1rem;
338
+ margin-bottom: 1.5rem;
339
+ }
340
+
341
+ .summary-card {
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 0.75rem;
345
+ padding: 1rem;
346
+ background: #f9fafb;
347
+ border-radius: 8px;
348
+ border: 1px solid #e5e7eb;
349
+ }
350
+
351
+ .summary-card.highlight {
352
+ background: #f0fdf4;
353
+ border-color: #86efac;
354
+ }
355
+
356
+ .summary-card.cost {
357
+ background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
358
+ border-color: #fde047;
359
+ }
360
+
361
+ .card-icon {
362
+ font-size: 1.5rem;
363
+ }
364
+
365
+ .card-content {
366
+ flex: 1;
367
+ }
368
+
369
+ .card-label {
370
+ font-size: 0.8rem;
371
+ color: #6b7280;
372
+ margin-bottom: 0.25rem;
373
+ }
374
+
375
+ .card-value {
376
+ font-size: 1.25rem;
377
+ font-weight: 600;
378
+ color: #333;
379
+ }
380
+
381
+ .card-sub {
382
+ font-size: 0.75rem;
383
+ color: #6b7280;
384
+ margin-top: 0.25rem;
385
+ }
386
+
387
+ .card-sub.saved {
388
+ color: #16a34a;
389
+ font-weight: 500;
390
+ }
391
+
392
+ /* Trend Section */
393
+ .trend-section {
394
+ margin-bottom: 1.5rem;
395
+ padding: 1rem;
396
+ background: #f9fafb;
397
+ border-radius: 8px;
398
+ border: 1px solid #e5e7eb;
399
+ }
400
+
401
+ .trend-section h3 {
402
+ margin: 0 0 1rem 0;
403
+ font-size: 1rem;
404
+ color: #666;
405
+ }
406
+
407
+ .trend-chart {
408
+ position: relative;
409
+ }
410
+
411
+ .trend-bars {
412
+ display: flex;
413
+ align-items: flex-end;
414
+ gap: 2px;
415
+ height: 120px;
416
+ padding-bottom: 30px;
417
+ }
418
+
419
+ .trend-bar-group {
420
+ flex: 1;
421
+ display: flex;
422
+ align-items: flex-end;
423
+ gap: 1px;
424
+ min-width: 20px;
425
+ position: relative;
426
+ }
427
+
428
+ .trend-bar {
429
+ flex: 1;
430
+ min-width: 8px;
431
+ border-radius: 2px 2px 0 0;
432
+ min-height: 2px;
433
+ position: relative;
434
+ transition: height 0.3s ease;
435
+ }
436
+
437
+ .trend-bar.input {
438
+ background: linear-gradient(to top, #4a9eff, #6bb9ff);
439
+ }
440
+
441
+ .trend-bar.output {
442
+ background: linear-gradient(to top, #f59e0b, #fbbf24);
443
+ }
444
+
445
+ .bar-tooltip {
446
+ position: absolute;
447
+ bottom: 100%;
448
+ left: 50%;
449
+ transform: translateX(-50%);
450
+ background: #333;
451
+ color: white;
452
+ padding: 2px 6px;
453
+ border-radius: 4px;
454
+ font-size: 0.7rem;
455
+ white-space: nowrap;
456
+ opacity: 0;
457
+ transition: opacity 0.2s;
458
+ pointer-events: none;
459
+ }
460
+
461
+ .trend-bar:hover .bar-tooltip {
462
+ opacity: 1;
463
+ }
464
+
465
+ .trend-time {
466
+ position: absolute;
467
+ bottom: -25px;
468
+ left: 50%;
469
+ transform: translateX(-50%);
470
+ font-size: 0.65rem;
471
+ color: #6b7280;
472
+ white-space: nowrap;
473
+ }
474
+
475
+ .trend-legend {
476
+ display: flex;
477
+ justify-content: center;
478
+ gap: 1.5rem;
479
+ margin-top: 0.5rem;
480
+ }
481
+
482
+ .legend-item {
483
+ display: flex;
484
+ align-items: center;
485
+ gap: 0.5rem;
486
+ font-size: 0.8rem;
487
+ color: #6b7280;
488
+ }
489
+
490
+ .legend-color {
491
+ width: 12px;
492
+ height: 12px;
493
+ border-radius: 2px;
494
+ }
495
+
496
+ .legend-color.input {
497
+ background: #4a9eff;
498
+ }
499
+
500
+ .legend-color.output {
501
+ background: #f59e0b;
502
+ }
503
+
504
+ /* View Toggle */
505
+ .view-toggle {
506
+ display: flex;
507
+ gap: 0.5rem;
508
+ margin-bottom: 1rem;
509
+ }
510
+
511
+ .view-toggle button {
512
+ padding: 0.5rem 1rem;
513
+ border: 1px solid #e5e7eb;
514
+ border-radius: 6px;
515
+ background: white;
516
+ font-size: 0.85rem;
517
+ cursor: pointer;
518
+ transition: all 0.2s;
519
+ }
520
+
521
+ .view-toggle button:hover {
522
+ border-color: #4a9eff;
523
+ }
524
+
525
+ .view-toggle button.active {
526
+ background: #4a9eff;
527
+ color: white;
528
+ border-color: #4a9eff;
529
+ }
530
+
531
+ /* Table View */
532
+ .by-agent-table {
533
+ overflow-x: auto;
534
+ }
535
+
536
+ .by-agent-table table {
537
+ width: 100%;
538
+ border-collapse: collapse;
539
+ font-size: 0.9rem;
540
+ }
541
+
542
+ .by-agent-table th,
543
+ .by-agent-table td {
544
+ padding: 0.75rem;
545
+ text-align: left;
546
+ border-bottom: 1px solid #e5e7eb;
547
+ }
548
+
549
+ .by-agent-table th {
550
+ background: #f9fafb;
551
+ font-weight: 600;
552
+ color: #6b7280;
553
+ font-size: 0.8rem;
554
+ text-transform: uppercase;
555
+ }
556
+
557
+ .by-agent-table tfoot td {
558
+ background: #f9fafb;
559
+ font-weight: 500;
560
+ }
561
+
562
+ .agent-name {
563
+ font-weight: 500;
564
+ color: #333;
565
+ }
566
+
567
+ .total-col {
568
+ font-weight: 600;
569
+ color: #4a9eff;
570
+ }
571
+
572
+ .percent-bar {
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 0.5rem;
576
+ }
577
+
578
+ .percent-fill {
579
+ height: 8px;
580
+ background: #4a9eff;
581
+ border-radius: 4px;
582
+ min-width: 4px;
583
+ }
584
+
585
+ .percent-text {
586
+ font-size: 0.8rem;
587
+ color: #6b7280;
588
+ white-space: nowrap;
589
+ }
590
+
591
+ /* Chart View */
592
+ .by-agent-chart h3 {
593
+ margin: 0 0 1rem 0;
594
+ font-size: 1rem;
595
+ color: #666;
596
+ }
597
+
598
+ .bar-chart {
599
+ display: flex;
600
+ flex-direction: column;
601
+ gap: 0.75rem;
602
+ }
603
+
604
+ .bar-row {
605
+ display: flex;
606
+ align-items: center;
607
+ gap: 1rem;
608
+ }
609
+
610
+ .bar-label {
611
+ width: 120px;
612
+ font-size: 0.85rem;
613
+ font-weight: 500;
614
+ color: #333;
615
+ flex-shrink: 0;
616
+ }
617
+
618
+ .bar-container {
619
+ flex: 1;
620
+ height: 24px;
621
+ background: #f3f4f6;
622
+ border-radius: 4px;
623
+ overflow: hidden;
624
+ }
625
+
626
+ .bar {
627
+ height: 100%;
628
+ background: linear-gradient(90deg, #4a9eff, #6bb9ff);
629
+ border-radius: 4px;
630
+ display: flex;
631
+ align-items: center;
632
+ justify-content: flex-end;
633
+ padding-right: 0.5rem;
634
+ min-width: fit-content;
635
+ }
636
+
637
+ .bar-value {
638
+ font-size: 0.75rem;
639
+ color: white;
640
+ font-weight: 500;
641
+ white-space: nowrap;
642
+ }
643
+
644
+ .bar-percent {
645
+ width: 50px;
646
+ font-size: 0.8rem;
647
+ color: #6b7280;
648
+ text-align: right;
649
+ }
650
+
651
+ /* Responsive */
652
+ @media (max-width: 640px) {
653
+ .section-header {
654
+ flex-direction: column;
655
+ align-items: flex-start;
656
+ }
657
+
658
+ .time-range-selector {
659
+ width: 100%;
660
+ justify-content: space-between;
661
+ }
662
+
663
+ .range-btn {
664
+ flex: 1;
665
+ text-align: center;
666
+ padding: 0.5rem;
667
+ font-size: 0.75rem;
668
+ }
669
+
670
+ .summary-cards {
671
+ grid-template-columns: 1fr 1fr;
672
+ }
673
+
674
+ .bar-label {
675
+ width: 80px;
676
+ font-size: 0.75rem;
677
+ }
678
+
679
+ .bar-percent {
680
+ width: 40px;
681
+ }
682
+ }
683
+ </style>