tokenrace 0.1.21 → 0.1.22
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/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>tokenrace</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-Dt3hqx3Z.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BD6TWWBL.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -15,7 +15,7 @@ import express from 'express'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { fileURLToPath } from 'node:url'
|
|
17
17
|
import { parseMetrics, parseEvents, parseTraces } from './otlp-parser.js'
|
|
18
|
-
import { processMetric, processEvent,
|
|
18
|
+
import { processMetric, processEvent, loadFromDisk, startAutoSave, saveSync, drainAutoDetectQueue, labelSession } from './store.js'
|
|
19
19
|
import { detectProjectBySessionId } from './git-detector.js'
|
|
20
20
|
import { createRouter, broadcast } from './api-routes.js'
|
|
21
21
|
|
|
@@ -92,9 +92,6 @@ export async function startServer({ port = 1337 } = {}) {
|
|
|
92
92
|
*/
|
|
93
93
|
app.post('/v1/traces', (req, res) => {
|
|
94
94
|
const spans = parseTraces(req.body)
|
|
95
|
-
for (const span of spans) {
|
|
96
|
-
processTrace(span)
|
|
97
|
-
}
|
|
98
95
|
broadcast('trace', { count: spans.length })
|
|
99
96
|
res.json({ partialSuccess: {} })
|
|
100
97
|
})
|
package/src/store.js
CHANGED
|
@@ -65,7 +65,6 @@ const state = {
|
|
|
65
65
|
eventIndex: 0,
|
|
66
66
|
projects: new Map(), // projectName → ProjectAggregates
|
|
67
67
|
tools: new Map(), // toolName → ToolStats
|
|
68
|
-
agents: new Map(), // agentId → AgentData
|
|
69
68
|
models: new Map(), // modelName → ModelStats
|
|
70
69
|
cumulativeValues: new Map(), // clave → último valor acumulativo (no persiste)
|
|
71
70
|
lastSeen: null,
|
|
@@ -97,7 +96,7 @@ function normalizeIncoming(name, value, labels) {
|
|
|
97
96
|
switch (name) {
|
|
98
97
|
case 'claude_code.token.usage': {
|
|
99
98
|
const type = labels.type
|
|
100
|
-
const key = `token:${sid}:${type}:${labels.model ?? ''}:${labels.query_source ?? ''}`
|
|
99
|
+
const key = `token:${sid}:${type}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels['agent.name'] ?? ''}`
|
|
101
100
|
const delta = cumulativeDelta(key, value)
|
|
102
101
|
const nameMap = {
|
|
103
102
|
'input': 'claude_code.tokens.input',
|
|
@@ -110,7 +109,7 @@ function normalizeIncoming(name, value, labels) {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
case 'claude_code.cost.usage': {
|
|
113
|
-
const key = `cost:${sid}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels.effort ?? ''}`
|
|
112
|
+
const key = `cost:${sid}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels.effort ?? ''}:${labels['agent.name'] ?? ''}`
|
|
114
113
|
const delta = cumulativeDelta(key, value)
|
|
115
114
|
return { name: 'claude_code.cost', value: delta }
|
|
116
115
|
}
|
|
@@ -432,55 +431,12 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
|
|
|
432
431
|
if (durMs > 0) tool.totalDurationMs += durMs
|
|
433
432
|
}
|
|
434
433
|
|
|
435
|
-
// ── Subagentes — Claude Code envía subagent_completed ──
|
|
436
|
-
if (eventName === 'subagent_completed') {
|
|
437
|
-
const seq = attributes['event.sequence'] ?? Date.now()
|
|
438
|
-
const agentId = `${sessionId ?? 'unknown'}:${seq}`
|
|
439
|
-
const totalTokens = Number(attributes['total_tokens'] ?? 0)
|
|
440
|
-
state.agents.set(agentId, {
|
|
441
|
-
agentId,
|
|
442
|
-
parentAgentId: null,
|
|
443
|
-
tokensInput: totalTokens,
|
|
444
|
-
tokensOutput: 0,
|
|
445
|
-
cost: estimateCost(attributes['model'] ?? null, totalTokens, 0, 0),
|
|
446
|
-
toolCalls: Number(attributes['total_tool_uses'] ?? 0),
|
|
447
|
-
durationMs: Number(attributes['duration_ms'] ?? 0)
|
|
448
|
-
})
|
|
449
|
-
}
|
|
450
|
-
|
|
451
434
|
state.lastSeen = timestamp
|
|
452
435
|
state.totalEvents++
|
|
453
436
|
|
|
454
437
|
return eventObj
|
|
455
438
|
}
|
|
456
439
|
|
|
457
|
-
/**
|
|
458
|
-
* Procesa un span normalizado.
|
|
459
|
-
* Solo actúa si el span tiene agentId.
|
|
460
|
-
* @param {Object} span
|
|
461
|
-
*/
|
|
462
|
-
export function processTrace(span) {
|
|
463
|
-
const attrs = span.attributes
|
|
464
|
-
const agentId = attrs['agent.id'] ?? attrs['gen_ai.agent.id'] ?? null
|
|
465
|
-
if (!agentId) return
|
|
466
|
-
|
|
467
|
-
if (!state.agents.has(agentId)) {
|
|
468
|
-
state.agents.set(agentId, {
|
|
469
|
-
agentId,
|
|
470
|
-
parentAgentId: attrs['agent.parent_id'] ?? null,
|
|
471
|
-
tokensInput: 0,
|
|
472
|
-
tokensOutput: 0,
|
|
473
|
-
cost: 0,
|
|
474
|
-
toolCalls: 0,
|
|
475
|
-
durationMs: 0
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const agent = state.agents.get(agentId)
|
|
480
|
-
const duration = span.endTime - span.startTime
|
|
481
|
-
if (duration > 0) agent.durationMs += duration
|
|
482
|
-
}
|
|
483
|
-
|
|
484
440
|
/**
|
|
485
441
|
* Asigna un proyecto a una sesión, con aplicación retroactiva.
|
|
486
442
|
* @param {string} sessionId
|
|
@@ -522,7 +478,6 @@ export function reset() {
|
|
|
522
478
|
state.eventIndex = 0
|
|
523
479
|
state.projects.clear()
|
|
524
480
|
state.tools.clear()
|
|
525
|
-
state.agents.clear()
|
|
526
481
|
state.models.clear()
|
|
527
482
|
state.cumulativeValues.clear()
|
|
528
483
|
state.lastSeen = null
|
|
@@ -915,10 +870,37 @@ export function getTools(from) {
|
|
|
915
870
|
}
|
|
916
871
|
|
|
917
872
|
/**
|
|
918
|
-
*
|
|
873
|
+
* Estadísticas de subagentes (Task tool), derivadas de las métricas de tokens
|
|
874
|
+
* y coste cuyas labels traen query_source: "subagent" y agent.name.
|
|
875
|
+
* Claude Code no emite eventos/spans con identidad de agente individual, así
|
|
876
|
+
* que se agrega por nombre de agente (p. ej. "Explore", "general-purpose").
|
|
919
877
|
*/
|
|
920
878
|
export function getAgents() {
|
|
921
|
-
|
|
879
|
+
const byAgent = new Map() // agentName → { tokensInput, tokensOutput, cost }
|
|
880
|
+
|
|
881
|
+
const metricField = {
|
|
882
|
+
'claude_code.tokens.input': 'tokensInput',
|
|
883
|
+
'claude_code.tokens.output': 'tokensOutput',
|
|
884
|
+
'claude_code.cost': 'cost'
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
for (const [metricName, field] of Object.entries(metricField)) {
|
|
888
|
+
for (const point of state.timeseries.get(metricName) ?? []) {
|
|
889
|
+
const labels = point.labels
|
|
890
|
+
if (labels.query_source !== 'subagent') continue
|
|
891
|
+
const name = labels['agent.name']
|
|
892
|
+
if (!name) continue
|
|
893
|
+
|
|
894
|
+
if (!byAgent.has(name)) {
|
|
895
|
+
byAgent.set(name, { tokensInput: 0, tokensOutput: 0, cost: 0 })
|
|
896
|
+
}
|
|
897
|
+
byAgent.get(name)[field] += point.value
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return Array.from(byAgent.entries())
|
|
902
|
+
.map(([name, stats]) => ({ name, ...stats }))
|
|
903
|
+
.sort((a, b) => b.cost - a.cost)
|
|
922
904
|
}
|
|
923
905
|
|
|
924
906
|
/**
|
|
@@ -964,8 +946,7 @@ export function saveSync() {
|
|
|
964
946
|
eventIndex: state.eventIndex,
|
|
965
947
|
totalEvents: state.totalEvents,
|
|
966
948
|
startTime: state.startTime,
|
|
967
|
-
tools: Array.from(state.tools.entries())
|
|
968
|
-
agents: Array.from(state.agents.values())
|
|
949
|
+
tools: Array.from(state.tools.entries())
|
|
969
950
|
}
|
|
970
951
|
|
|
971
952
|
fs.writeFileSync(dataFile, JSON.stringify(data), { mode: 0o600 })
|
|
@@ -1043,13 +1024,6 @@ export function loadFromDisk() {
|
|
|
1043
1024
|
}
|
|
1044
1025
|
}
|
|
1045
1026
|
|
|
1046
|
-
// Restaurar agentes
|
|
1047
|
-
if (Array.isArray(data.agents)) {
|
|
1048
|
-
for (const agent of data.agents) {
|
|
1049
|
-
state.agents.set(agent.agentId, agent)
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
1027
|
// Re-aplicar mappings a sesiones (timeseries no necesitan propagación:
|
|
1054
1028
|
// resolveProject() consulta sessionMappings primero en todos los lectores)
|
|
1055
1029
|
for (const [sessionId, project] of state.sessionMappings.entries()) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,Menlo,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-2{right:.5rem}.top-0{top:0}.top-10{top:2.5rem}.top-2{top:.5rem}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-48{height:12rem}.h-\[600px\]{height:600px}.h-full{height:100%}.max-h-40{max-height:10rem}.min-h-\[60vh\]{min-height:60vh}.min-h-screen{min-height:100vh}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-32{width:8rem}.w-44{width:11rem}.w-6{width:1.5rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[640px\]{min-width:640px}.min-w-\[720px\]{min-width:720px}.max-w-screen-2xl{max-width:1536px}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-accent-green{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.border-bg-border{--tw-border-opacity: 1;border-color:rgb(26 26 26 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.bg-accent-green{--tw-bg-opacity: 1;background-color:rgb(255 107 53 / var(--tw-bg-opacity, 1))}.bg-accent-purple{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-accent-teal{--tw-bg-opacity: 1;background-color:rgb(0 212 170 / var(--tw-bg-opacity, 1))}.bg-accent-yellow{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-bg-base{--tw-bg-opacity: 1;background-color:rgb(20 25 31 / var(--tw-bg-opacity, 1))}.bg-bg-card,.bg-bg-subtle{--tw-bg-opacity: 1;background-color:rgb(33 38 43 / var(--tw-bg-opacity, 1))}.bg-text-muted{--tw-bg-opacity: 1;background-color:rgb(68 68 68 / var(--tw-bg-opacity, 1))}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Menlo,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-accent-blue{--tw-text-opacity: 1;color:rgb(77 166 255 / var(--tw-text-opacity, 1))}.text-accent-green,.text-accent-orange{--tw-text-opacity: 1;color:rgb(255 107 53 / var(--tw-text-opacity, 1))}.text-accent-purple{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-accent-teal{--tw-text-opacity: 1;color:rgb(0 212 170 / var(--tw-text-opacity, 1))}.text-accent-yellow{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-text-muted{--tw-text-opacity: 1;color:rgb(68 68 68 / var(--tw-text-opacity, 1))}.text-text-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-text-secondary{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg-base: #14191F;--bg-card: #21262B;--bg-card-hover: #272d33;--bg-border: #1a1a1a;--bg-subtle: #21262B;--text-primary: #ffffff;--text-secondary: #888888;--text-muted: #444444;--accent-green: #ff6b35;--accent-blue: #4da6ff;--accent-purple: #a855f7;--accent-orange: #ff6b35;--accent-teal: #00d4aa;--accent-yellow: #fbbf24}*{box-sizing:border-box}html,body,#root{height:100%;margin:0;padding:0;background-color:#14191f;color:#fff}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:#0d0d0d}::-webkit-scrollbar-thumb{background:#333;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#444}.hover\:border-accent-green:hover{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.hover\:bg-bg-base:hover{--tw-bg-opacity: 1;background-color:rgb(20 25 31 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-card:hover{--tw-bg-opacity: 1;background-color:rgb(33 38 43 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-card-hover:hover{--tw-bg-opacity: 1;background-color:rgb(39 45 51 / var(--tw-bg-opacity, 1))}.hover\:text-accent-orange:hover{--tw-text-opacity: 1;color:rgb(255 107 53 / var(--tw-text-opacity, 1))}.hover\:text-text-primary:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-text-secondary:hover{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-accent-green:focus{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.disabled\:opacity-40:disabled{opacity:.4}@media(min-width:768px){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}
|