Skip to content

Commit 80018ab

Browse files
committed
🤖 Extract base TokenMeter component for consistency
Refactors token visualization to use a shared base component: - TokenMeter.tsx: Base component supporting both orientations - HorizontalTokenMeter.tsx: Wrapper for horizontal use (future CostsTab use) - VerticalTokenMeter: Now uses base TokenMeter (-21 lines) - CostsTab: Uses shared TOKEN_COMPONENT_COLORS Benefits: - Single source of truth for token bar rendering logic - Consistent styling between horizontal and vertical views - Easier to maintain - changes propagate automatically - Reduced duplication (VerticalTokenMeter: 97→76 lines) _Generated with `cmux`_
1 parent 196dfa1 commit 80018ab

File tree

4 files changed

+68
-40
lines changed

4 files changed

+68
-40
lines changed

src/components/ChatMetaSidebar/CostsTab.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { usePersistedState } from "@/hooks/usePersistedState";
88
import { ToggleGroup, type ToggleOption } from "../ToggleGroup";
99
import { use1MContext } from "@/hooks/use1MContext";
1010
import { supports1MContext } from "@/utils/ai/models";
11+
import { TOKEN_COMPONENT_COLORS } from "@/utils/tokens/tokenMeterUtils";
1112

1213
const Container = styled.div`
1314
color: #d4d4d4;
@@ -86,14 +87,6 @@ interface SegmentProps {
8687
percentage: number;
8788
}
8889

89-
// Component color mapping - single source of truth for all cost component colors
90-
const COMPONENT_COLORS = {
91-
cached: "var(--color-token-cached)",
92-
input: "var(--color-token-input)",
93-
output: "var(--color-token-output)",
94-
thinking: "var(--color-thinking-mode)",
95-
} as const;
96-
9790
const FixedSegment = styled.div<SegmentProps>`
9891
height: 100%;
9992
width: ${(props) => props.percentage}%;
@@ -111,28 +104,28 @@ const VariableSegment = styled.div<SegmentProps>`
111104
const InputSegment = styled.div<SegmentProps>`
112105
height: 100%;
113106
width: ${(props) => props.percentage}%;
114-
background: ${COMPONENT_COLORS.input};
107+
background: ${TOKEN_COMPONENT_COLORS.input};
115108
transition: width 0.3s ease;
116109
`;
117110

118111
const OutputSegment = styled.div<SegmentProps>`
119112
height: 100%;
120113
width: ${(props) => props.percentage}%;
121-
background: ${COMPONENT_COLORS.output};
114+
background: ${TOKEN_COMPONENT_COLORS.output};
122115
transition: width 0.3s ease;
123116
`;
124117

125118
const ThinkingSegment = styled.div<SegmentProps>`
126119
height: 100%;
127120
width: ${(props) => props.percentage}%;
128-
background: ${COMPONENT_COLORS.thinking};
121+
background: ${TOKEN_COMPONENT_COLORS.thinking};
129122
transition: width 0.3s ease;
130123
`;
131124

132125
const CachedSegment = styled.div<SegmentProps>`
133126
height: 100%;
134127
width: ${(props) => props.percentage}%;
135-
background: ${COMPONENT_COLORS.cached};
128+
background: ${TOKEN_COMPONENT_COLORS.cached};
136129
transition: width 0.3s ease;
137130
`;
138131

@@ -452,35 +445,35 @@ export const CostsTab: React.FC = () => {
452445
name: "Cache Read",
453446
tokens: displayUsage.cached.tokens,
454447
cost: displayUsage.cached.cost_usd,
455-
color: COMPONENT_COLORS.cached,
448+
color: TOKEN_COMPONENT_COLORS.cached,
456449
show: displayUsage.cached.tokens > 0,
457450
},
458451
{
459452
name: "Cache Create",
460453
tokens: displayUsage.cacheCreate.tokens,
461454
cost: displayUsage.cacheCreate.cost_usd,
462-
color: COMPONENT_COLORS.cached,
455+
color: TOKEN_COMPONENT_COLORS.cached,
463456
show: displayUsage.cacheCreate.tokens > 0,
464457
},
465458
{
466459
name: "Input",
467460
tokens: displayUsage.input.tokens,
468461
cost: adjustedInputCost,
469-
color: COMPONENT_COLORS.input,
462+
color: TOKEN_COMPONENT_COLORS.input,
470463
show: true,
471464
},
472465
{
473466
name: "Output",
474467
tokens: displayUsage.output.tokens,
475468
cost: adjustedOutputCost,
476-
color: COMPONENT_COLORS.output,
469+
color: TOKEN_COMPONENT_COLORS.output,
477470
show: true,
478471
},
479472
{
480473
name: "Thinking",
481474
tokens: displayUsage.reasoning.tokens,
482475
cost: adjustedReasoningCost,
483-
color: COMPONENT_COLORS.thinking,
476+
color: TOKEN_COMPONENT_COLORS.thinking,
484477
show: displayUsage.reasoning.tokens > 0,
485478
},
486479
].filter((c) => c.show)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react";
2+
import { TokenMeter } from "./TokenMeter";
3+
import type { TokenMeterData } from "@/utils/tokens/tokenMeterUtils";
4+
5+
interface HorizontalTokenMeterProps {
6+
data: TokenMeterData;
7+
}
8+
9+
export const HorizontalTokenMeter: React.FC<HorizontalTokenMeterProps> = ({ data }) => {
10+
if (data.segments.length === 0) return null;
11+
return <TokenMeter segments={data.segments} orientation="horizontal" />;
12+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
import styled from "@emotion/styled";
3+
import type { TokenSegment } from "@/utils/tokens/tokenMeterUtils";
4+
5+
interface TokenMeterProps {
6+
segments: TokenSegment[];
7+
orientation: "horizontal" | "vertical";
8+
className?: string;
9+
}
10+
11+
const Bar = styled.div<{ orientation: "horizontal" | "vertical" }>`
12+
background: #3e3e42;
13+
border-radius: ${(props) => (props.orientation === "horizontal" ? "3px" : "4px")};
14+
overflow: hidden;
15+
display: flex;
16+
flex-direction: ${(props) => (props.orientation === "horizontal" ? "row" : "column")};
17+
${(props) =>
18+
props.orientation === "horizontal"
19+
? "width: 100%; height: 6px;"
20+
: "width: 8px; flex: 1; min-height: 100px;"}
21+
`;
22+
23+
const Segment = styled.div<{
24+
percentage: number;
25+
color: string;
26+
orientation: "horizontal" | "vertical";
27+
}>`
28+
background: ${(props) => props.color};
29+
transition: ${(props) => (props.orientation === "horizontal" ? "width" : "height")} 0.3s ease;
30+
${(props) =>
31+
props.orientation === "horizontal"
32+
? `width: ${props.percentage}%; height: 100%;`
33+
: `height: ${props.percentage}%; width: 100%;`}
34+
`;
35+
36+
export const TokenMeter: React.FC<TokenMeterProps> = ({ segments, orientation, className }) => {
37+
return (
38+
<Bar orientation={orientation} className={className}>
39+
{segments.map((seg, i) => (
40+
<Segment key={i} percentage={seg.percentage} color={seg.color} orientation={orientation} />
41+
))}
42+
</Bar>
43+
);
44+
};

src/components/ChatMetaSidebar/VerticalTokenMeter.tsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import styled from "@emotion/styled";
33
import { TooltipWrapper, Tooltip } from "../Tooltip";
4+
import { TokenMeter } from "./TokenMeter";
45
import { type TokenMeterData, formatTokens, getSegmentLabel } from "@/utils/tokens/tokenMeterUtils";
56

67
const Container = styled.div`
@@ -14,24 +15,6 @@ const Container = styled.div`
1415
border-left: 1px solid #3e3e42;
1516
`;
1617

17-
const Bar = styled.div`
18-
width: 8px;
19-
flex: 1;
20-
background: #3e3e42;
21-
border-radius: 4px;
22-
overflow: hidden;
23-
display: flex;
24-
flex-direction: column;
25-
min-height: 100px;
26-
`;
27-
28-
const Segment = styled.div<{ percentage: number; color: string }>`
29-
width: 100%;
30-
height: ${(props) => props.percentage}%;
31-
background: ${(props) => props.color};
32-
transition: height 0.3s ease;
33-
`;
34-
3518
const Content = styled.div`
3619
display: flex;
3720
flex-direction: column;
@@ -65,11 +48,7 @@ export const VerticalTokenMeter: React.FC<{ data: TokenMeterData }> = ({ data })
6548
return (
6649
<TooltipWrapper>
6750
<Container>
68-
<Bar>
69-
{data.segments.map((seg, i) => (
70-
<Segment key={i} percentage={seg.percentage} color={seg.color} />
71-
))}
72-
</Bar>
51+
<TokenMeter segments={data.segments} orientation="vertical" />
7352
</Container>
7453
<Tooltip>
7554
<Content>

0 commit comments

Comments
 (0)