@tokscale/cli 1.0.4 → 1.0.6
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/cli.js +11 -3
- package/dist/cli.js.map +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +3 -2
- package/dist/native.js.map +1 -1
- package/dist/tui/{App.jsx → App.js} +3 -35
- package/dist/tui/{App.jsx.map → App.js.map} +1 -1
- package/dist/tui/components/{BarChart.jsx → BarChart.js} +14 -37
- package/dist/tui/components/{BarChart.jsx.map → BarChart.js.map} +1 -1
- package/dist/tui/components/{DailyView.jsx → DailyView.js} +7 -23
- package/dist/tui/components/{DailyView.jsx.map → DailyView.js.map} +1 -1
- package/dist/tui/components/DateBreakdownPanel.js +36 -0
- package/dist/tui/components/DateBreakdownPanel.js.map +1 -0
- package/dist/tui/components/Footer.js +87 -0
- package/dist/tui/components/Footer.js.map +1 -0
- package/dist/tui/components/Header.js +20 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/Legend.js +16 -0
- package/dist/tui/components/Legend.js.map +1 -0
- package/dist/tui/components/{LoadingSpinner.jsx → LoadingSpinner.js} +6 -12
- package/dist/tui/components/{LoadingSpinner.jsx.map → LoadingSpinner.js.map} +1 -1
- package/dist/tui/components/ModelRow.js +15 -0
- package/dist/tui/components/ModelRow.js.map +1 -0
- package/dist/tui/components/{ModelView.jsx → ModelView.js} +7 -24
- package/dist/tui/components/{ModelView.jsx.map → ModelView.js.map} +1 -1
- package/dist/tui/components/{OverviewView.jsx → OverviewView.js} +11 -35
- package/dist/tui/components/{OverviewView.jsx.map → OverviewView.js.map} +1 -1
- package/dist/tui/components/StatsView.js +86 -0
- package/dist/tui/components/StatsView.js.map +1 -0
- package/dist/tui/components/TokenBreakdown.js +10 -0
- package/dist/tui/components/TokenBreakdown.js.map +1 -0
- package/dist/tui/{index.jsx → index.js} +3 -2
- package/dist/tui/{index.jsx.map → index.js.map} +1 -1
- package/package.json +6 -4
- package/src/tui/App.tsx +339 -0
- package/src/tui/components/BarChart.tsx +198 -0
- package/src/tui/components/DailyView.tsx +113 -0
- package/src/tui/components/DateBreakdownPanel.tsx +79 -0
- package/src/tui/components/Footer.tsx +225 -0
- package/src/tui/components/Header.tsx +68 -0
- package/src/tui/components/Legend.tsx +39 -0
- package/src/tui/components/LoadingSpinner.tsx +82 -0
- package/src/tui/components/ModelRow.tsx +47 -0
- package/src/tui/components/ModelView.tsx +145 -0
- package/src/tui/components/OverviewView.tsx +108 -0
- package/{dist/tui/components/StatsView.jsx → src/tui/components/StatsView.tsx} +128 -83
- package/{dist/tui/components/TokenBreakdown.jsx → src/tui/components/TokenBreakdown.tsx} +28 -9
- package/src/tui/components/index.ts +15 -0
- package/src/tui/config/settings.ts +130 -0
- package/src/tui/config/themes.ts +115 -0
- package/src/tui/hooks/useData.ts +518 -0
- package/src/tui/index.tsx +44 -0
- package/src/tui/opentui.d.ts +137 -0
- package/src/tui/types/index.ts +165 -0
- package/src/tui/utils/cleanup.ts +65 -0
- package/src/tui/utils/colors.ts +65 -0
- package/src/tui/utils/format.ts +36 -0
- package/src/tui/utils/responsive.ts +8 -0
- package/dist/tui/components/DateBreakdownPanel.jsx +0 -61
- package/dist/tui/components/DateBreakdownPanel.jsx.map +0 -1
- package/dist/tui/components/Footer.jsx +0 -158
- package/dist/tui/components/Footer.jsx.map +0 -1
- package/dist/tui/components/Header.jsx +0 -38
- package/dist/tui/components/Header.jsx.map +0 -1
- package/dist/tui/components/Legend.jsx +0 -27
- package/dist/tui/components/Legend.jsx.map +0 -1
- package/dist/tui/components/ModelRow.jsx +0 -28
- package/dist/tui/components/ModelRow.jsx.map +0 -1
- package/dist/tui/components/StatsView.jsx.map +0 -1
- package/dist/tui/components/TokenBreakdown.jsx.map +0 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { For, createMemo, type Accessor } from "solid-js";
|
|
2
|
+
import type { TUIData, SortType } from "../hooks/useData.js";
|
|
3
|
+
import { getModelColor } from "../utils/colors.js";
|
|
4
|
+
import { formatTokensCompact, formatCostFull } from "../utils/format.js";
|
|
5
|
+
import { isNarrow, isVeryNarrow } from "../utils/responsive.js";
|
|
6
|
+
|
|
7
|
+
const STRIPE_BG = "#232328";
|
|
8
|
+
|
|
9
|
+
const INPUT_COL_WIDTH = 12;
|
|
10
|
+
const OUTPUT_COL_WIDTH = 12;
|
|
11
|
+
const CACHE_COL_WIDTH = 12;
|
|
12
|
+
const TOTAL_COL_WIDTH = 14;
|
|
13
|
+
const COST_COL_WIDTH = 12;
|
|
14
|
+
const METRIC_COLUMNS_WIDTH_FULL = INPUT_COL_WIDTH + OUTPUT_COL_WIDTH + CACHE_COL_WIDTH + TOTAL_COL_WIDTH + COST_COL_WIDTH;
|
|
15
|
+
const METRIC_COLUMNS_WIDTH_NARROW = TOTAL_COL_WIDTH + COST_COL_WIDTH;
|
|
16
|
+
const SIDE_PADDING = 2;
|
|
17
|
+
const MIN_NAME_COLUMN = 16;
|
|
18
|
+
const MIN_NAME_COLUMN_NARROW = 12;
|
|
19
|
+
|
|
20
|
+
interface ModelViewProps {
|
|
21
|
+
data: TUIData;
|
|
22
|
+
sortBy: SortType;
|
|
23
|
+
sortDesc: boolean;
|
|
24
|
+
selectedIndex: Accessor<number>;
|
|
25
|
+
height: number;
|
|
26
|
+
width: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ModelView(props: ModelViewProps) {
|
|
30
|
+
const sortedEntries = createMemo(() => {
|
|
31
|
+
const entries = props.data.modelEntries;
|
|
32
|
+
const sortBy = props.sortBy;
|
|
33
|
+
const sortDesc = props.sortDesc;
|
|
34
|
+
|
|
35
|
+
return [...entries].sort((a, b) => {
|
|
36
|
+
let cmp = 0;
|
|
37
|
+
if (sortBy === "cost") cmp = a.cost - b.cost;
|
|
38
|
+
else if (sortBy === "tokens") cmp = a.total - b.total;
|
|
39
|
+
else cmp = a.model.localeCompare(b.model);
|
|
40
|
+
return sortDesc ? -cmp : cmp;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const isNarrowTerminal = () => isNarrow(props.width);
|
|
45
|
+
const isVeryNarrowTerminal = () => isVeryNarrow(props.width);
|
|
46
|
+
|
|
47
|
+
const nameColumnWidths = createMemo(() => {
|
|
48
|
+
const metricWidth = isNarrowTerminal() ? METRIC_COLUMNS_WIDTH_NARROW : METRIC_COLUMNS_WIDTH_FULL;
|
|
49
|
+
const minName = isNarrowTerminal() ? MIN_NAME_COLUMN_NARROW : MIN_NAME_COLUMN;
|
|
50
|
+
const available = Math.max(props.width - SIDE_PADDING - metricWidth, minName);
|
|
51
|
+
const nameColumn = Math.max(minName, available);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
column: nameColumn,
|
|
55
|
+
text: Math.max(nameColumn - 1, 1),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const visibleEntries = createMemo(() => {
|
|
60
|
+
const maxRows = Math.max(props.height - 3, 0);
|
|
61
|
+
return sortedEntries().slice(0, maxRows);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const formattedRows = createMemo(() => {
|
|
65
|
+
const nameWidth = nameColumnWidths().text;
|
|
66
|
+
return visibleEntries().map((entry) => {
|
|
67
|
+
const sourceLabel = entry.source.charAt(0).toUpperCase() + entry.source.slice(1);
|
|
68
|
+
const fullName = `${sourceLabel} ${entry.model}`;
|
|
69
|
+
let displayName = fullName;
|
|
70
|
+
if (fullName.length > nameWidth) {
|
|
71
|
+
displayName = nameWidth > 1 ? `${fullName.slice(0, nameWidth - 1)}…` : fullName.slice(0, 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
entry,
|
|
76
|
+
displayName,
|
|
77
|
+
nameWidth,
|
|
78
|
+
input: formatTokensCompact(entry.input),
|
|
79
|
+
output: formatTokensCompact(entry.output),
|
|
80
|
+
cache: formatTokensCompact(entry.cacheRead),
|
|
81
|
+
total: formatTokensCompact(entry.total),
|
|
82
|
+
cost: formatCostFull(entry.cost),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const sortArrow = () => (props.sortDesc ? "▼" : "▲");
|
|
88
|
+
const nameHeader = () => isVeryNarrowTerminal()
|
|
89
|
+
? ` Model`
|
|
90
|
+
: ` Source/Model`;
|
|
91
|
+
const totalHeader = () => (props.sortBy === "tokens" ? `${sortArrow()} Total` : "Total");
|
|
92
|
+
const costHeader = () => (props.sortBy === "cost" ? `${sortArrow()} Cost` : "Cost");
|
|
93
|
+
|
|
94
|
+
const renderHeader = () => {
|
|
95
|
+
if (isNarrowTerminal()) {
|
|
96
|
+
return `${nameHeader().padEnd(nameColumnWidths().column)}${totalHeader().padStart(TOTAL_COL_WIDTH)}${costHeader().padStart(COST_COL_WIDTH)}`;
|
|
97
|
+
}
|
|
98
|
+
return `${nameHeader().padEnd(nameColumnWidths().column)}${"Input".padStart(INPUT_COL_WIDTH)}${"Output".padStart(OUTPUT_COL_WIDTH)}${"Cache".padStart(CACHE_COL_WIDTH)}${totalHeader().padStart(TOTAL_COL_WIDTH)}${costHeader().padStart(COST_COL_WIDTH)}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const renderRowData = (row: typeof formattedRows extends () => (infer T)[] ? T : never) => {
|
|
102
|
+
if (isNarrowTerminal()) {
|
|
103
|
+
return `${row.displayName.padEnd(row.nameWidth)}${row.total.padStart(TOTAL_COL_WIDTH)}`;
|
|
104
|
+
}
|
|
105
|
+
return `${row.displayName.padEnd(row.nameWidth)}${row.input.padStart(INPUT_COL_WIDTH)}${row.output.padStart(OUTPUT_COL_WIDTH)}${row.cache.padStart(CACHE_COL_WIDTH)}${row.total.padStart(TOTAL_COL_WIDTH)}`;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<box flexDirection="column">
|
|
110
|
+
<box flexDirection="row">
|
|
111
|
+
<text fg="cyan" bold>
|
|
112
|
+
{renderHeader()}
|
|
113
|
+
</text>
|
|
114
|
+
</box>
|
|
115
|
+
|
|
116
|
+
<For each={formattedRows()}>
|
|
117
|
+
{(row, i) => {
|
|
118
|
+
const isActive = createMemo(() => i() === props.selectedIndex());
|
|
119
|
+
const rowBg = createMemo(() => isActive() ? "blue" : (i() % 2 === 1 ? STRIPE_BG : undefined));
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<box flexDirection="row">
|
|
123
|
+
<text
|
|
124
|
+
fg={getModelColor(row.entry.model)}
|
|
125
|
+
bg={rowBg()}
|
|
126
|
+
>●</text>
|
|
127
|
+
<text
|
|
128
|
+
bg={rowBg()}
|
|
129
|
+
fg={isActive() ? "white" : undefined}
|
|
130
|
+
>
|
|
131
|
+
{renderRowData(row)}
|
|
132
|
+
</text>
|
|
133
|
+
<text
|
|
134
|
+
fg="green"
|
|
135
|
+
bg={rowBg()}
|
|
136
|
+
>
|
|
137
|
+
{row.cost.padStart(COST_COL_WIDTH)}
|
|
138
|
+
</text>
|
|
139
|
+
</box>
|
|
140
|
+
);
|
|
141
|
+
}}
|
|
142
|
+
</For>
|
|
143
|
+
</box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Show, For, createMemo, type Accessor } from "solid-js";
|
|
2
|
+
import { BarChart } from "./BarChart.js";
|
|
3
|
+
import { Legend } from "./Legend.js";
|
|
4
|
+
import { ModelRow } from "./ModelRow.js";
|
|
5
|
+
import type { TUIData, SortType } from "../hooks/useData.js";
|
|
6
|
+
import { formatCost } from "../utils/format.js";
|
|
7
|
+
import { isNarrow, isVeryNarrow } from "../utils/responsive.js";
|
|
8
|
+
|
|
9
|
+
interface OverviewViewProps {
|
|
10
|
+
data: TUIData;
|
|
11
|
+
sortBy: SortType;
|
|
12
|
+
sortDesc: boolean;
|
|
13
|
+
selectedIndex: Accessor<number>;
|
|
14
|
+
scrollOffset: Accessor<number>;
|
|
15
|
+
height: number;
|
|
16
|
+
width: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function OverviewView(props: OverviewViewProps) {
|
|
20
|
+
const safeHeight = () => Math.max(props.height, 12);
|
|
21
|
+
const chartHeight = () => Math.max(5, Math.floor(safeHeight() * 0.35));
|
|
22
|
+
const listHeight = () => Math.max(4, safeHeight() - chartHeight() - 4);
|
|
23
|
+
const itemsPerPage = () => Math.max(1, Math.floor(listHeight() / 2));
|
|
24
|
+
|
|
25
|
+
const isNarrowTerminal = () => isNarrow(props.width);
|
|
26
|
+
const isVeryNarrowTerminal = () => isVeryNarrow(props.width);
|
|
27
|
+
|
|
28
|
+
const legendModelLimit = () => isVeryNarrowTerminal() ? 3 : 5;
|
|
29
|
+
const topModelsForLegend = () => props.data.topModels.slice(0, legendModelLimit()).map(m => m.modelId);
|
|
30
|
+
|
|
31
|
+
const maxModelNameWidth = () => isVeryNarrowTerminal() ? 20 : isNarrowTerminal() ? 30 : 50;
|
|
32
|
+
|
|
33
|
+
const sortedModels = createMemo(() => {
|
|
34
|
+
const models = [...props.data.topModels];
|
|
35
|
+
return models.sort((a, b) => {
|
|
36
|
+
let cmp = 0;
|
|
37
|
+
if (props.sortBy === "cost") cmp = a.cost - b.cost;
|
|
38
|
+
else if (props.sortBy === "tokens") cmp = a.totalTokens - b.totalTokens;
|
|
39
|
+
return props.sortDesc ? -cmp : cmp;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const totalForPercentage = createMemo(() => {
|
|
44
|
+
const models = props.data.topModels;
|
|
45
|
+
if (props.sortBy === "tokens") {
|
|
46
|
+
return models.reduce((sum, m) => sum + m.totalTokens, 0) || 1;
|
|
47
|
+
}
|
|
48
|
+
return models.reduce((sum, m) => sum + m.cost, 0) || 1;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const getPercentage = (model: typeof props.data.topModels[0]) => {
|
|
52
|
+
if (props.sortBy === "tokens") {
|
|
53
|
+
return (model.totalTokens / totalForPercentage()) * 100;
|
|
54
|
+
}
|
|
55
|
+
return (model.cost / totalForPercentage()) * 100;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const visibleModels = () => sortedModels().slice(props.scrollOffset(), props.scrollOffset() + itemsPerPage());
|
|
59
|
+
const totalModels = () => sortedModels().length;
|
|
60
|
+
const endIndex = () => Math.min(props.scrollOffset() + visibleModels().length, totalModels());
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<box flexDirection="column" gap={1}>
|
|
64
|
+
<box flexDirection="column">
|
|
65
|
+
<BarChart data={props.data.chartData} width={props.width - 4} height={chartHeight()} />
|
|
66
|
+
<Legend models={topModelsForLegend()} width={props.width} />
|
|
67
|
+
</box>
|
|
68
|
+
|
|
69
|
+
<box flexDirection="column">
|
|
70
|
+
<box flexDirection="row" justifyContent="space-between" marginBottom={0}>
|
|
71
|
+
<text bold>{isVeryNarrowTerminal() ? "Top Models" : `Models by ${props.sortBy === "tokens" ? "Tokens" : "Cost"}`}</text>
|
|
72
|
+
<box flexDirection="row">
|
|
73
|
+
<text dim>{isVeryNarrowTerminal() ? "" : "Total: "}</text>
|
|
74
|
+
<text fg="green">{formatCost(props.data.totalCost)}</text>
|
|
75
|
+
</box>
|
|
76
|
+
</box>
|
|
77
|
+
|
|
78
|
+
<box flexDirection="column">
|
|
79
|
+
<For each={visibleModels()}>
|
|
80
|
+
{(model, i) => {
|
|
81
|
+
const isActive = createMemo(() => i() === props.selectedIndex());
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<ModelRow
|
|
85
|
+
modelId={model.modelId}
|
|
86
|
+
tokens={{
|
|
87
|
+
input: model.inputTokens,
|
|
88
|
+
output: model.outputTokens,
|
|
89
|
+
cacheRead: model.cacheReadTokens,
|
|
90
|
+
cacheWrite: model.cacheWriteTokens,
|
|
91
|
+
}}
|
|
92
|
+
percentage={getPercentage(model)}
|
|
93
|
+
isActive={isActive()}
|
|
94
|
+
compact={isVeryNarrowTerminal()}
|
|
95
|
+
maxNameWidth={maxModelNameWidth()}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}}
|
|
99
|
+
</For>
|
|
100
|
+
</box>
|
|
101
|
+
|
|
102
|
+
<Show when={totalModels() > visibleModels().length}>
|
|
103
|
+
<text dim>{`↓ ${props.scrollOffset() + 1}-${endIndex()} of ${totalModels()} models (↑↓ to scroll)`}</text>
|
|
104
|
+
</Show>
|
|
105
|
+
</box>
|
|
106
|
+
</box>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -1,102 +1,143 @@
|
|
|
1
1
|
import { For, Show, createMemo, createSignal } from "solid-js";
|
|
2
|
+
import type { TUIData } from "../hooks/useData.js";
|
|
3
|
+
import type { ColorPaletteName } from "../config/themes.js";
|
|
2
4
|
import { getPalette, getGradeColor } from "../config/themes.js";
|
|
3
5
|
import { getModelColor } from "../utils/colors.js";
|
|
4
6
|
import { formatTokens } from "../utils/format.js";
|
|
5
7
|
import { isNarrow } from "../utils/responsive.js";
|
|
6
8
|
import { DateBreakdownPanel } from "./DateBreakdownPanel.js";
|
|
9
|
+
|
|
10
|
+
interface StatsViewProps {
|
|
11
|
+
data: TUIData;
|
|
12
|
+
height: number;
|
|
13
|
+
colorPalette: ColorPaletteName;
|
|
14
|
+
width?: number;
|
|
15
|
+
selectedDate?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
8
19
|
const MONTHS_SHORT = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
|
|
9
20
|
const DAYS = ["", "Mon", "", "Wed", "", "Fri", ""];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
21
|
+
|
|
22
|
+
interface MonthLabel {
|
|
23
|
+
month: string;
|
|
24
|
+
weekIndex: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function StatsView(props: StatsViewProps) {
|
|
28
|
+
const palette = () => getPalette(props.colorPalette);
|
|
29
|
+
const isNarrowTerminal = () => isNarrow(props.width);
|
|
30
|
+
const grid = () => props.data.contributionGrid;
|
|
31
|
+
const cellWidth = 2;
|
|
32
|
+
|
|
33
|
+
const [clickedCell, setClickedCell] = createSignal<string | null>(null);
|
|
34
|
+
|
|
35
|
+
const selectedBreakdown = createMemo(() => {
|
|
36
|
+
const date = clickedCell();
|
|
37
|
+
if (!date) return null;
|
|
38
|
+
if (!props.data.dailyBreakdowns) return null;
|
|
39
|
+
if (!(props.data.dailyBreakdowns instanceof Map)) return null;
|
|
40
|
+
return props.data.dailyBreakdowns.get(date) || null;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const monthPositions = createMemo(() => {
|
|
44
|
+
const sundayRow = grid()[0] || [];
|
|
45
|
+
if (sundayRow.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
const positions: MonthLabel[] = [];
|
|
48
|
+
let lastMonth = -1;
|
|
49
|
+
const monthNames = isNarrowTerminal() ? MONTHS_SHORT : MONTHS;
|
|
50
|
+
|
|
51
|
+
for (let weekIdx = 0; weekIdx < sundayRow.length; weekIdx++) {
|
|
52
|
+
const cell = sundayRow[weekIdx];
|
|
53
|
+
if (!cell.date) continue;
|
|
54
|
+
const month = new Date(cell.date + "T00:00:00").getMonth();
|
|
55
|
+
if (month !== lastMonth) {
|
|
56
|
+
positions.push({ month: monthNames[month], weekIndex: weekIdx });
|
|
57
|
+
lastMonth = month;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return positions;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const totalWeeks = createMemo(() => (grid()[0] || []).length);
|
|
64
|
+
|
|
65
|
+
const monthLabelRow = createMemo(() => {
|
|
66
|
+
const weeks = totalWeeks();
|
|
67
|
+
const positions = monthPositions();
|
|
68
|
+
const chars: string[] = new Array(weeks * cellWidth).fill(" ");
|
|
69
|
+
|
|
70
|
+
for (const pos of positions) {
|
|
71
|
+
const startIdx = pos.weekIndex * cellWidth;
|
|
72
|
+
const monthChars = pos.month.split("");
|
|
73
|
+
for (let i = 0; i < monthChars.length && startIdx + i < chars.length; i++) {
|
|
74
|
+
chars[startIdx + i] = monthChars[i];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return chars.join("");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const dayLabelWidth = () => isNarrowTerminal() ? 2 : 4;
|
|
82
|
+
|
|
83
|
+
const isSelected = (cellDate: string | null) =>
|
|
84
|
+
cellDate && (clickedCell() === cellDate || props.selectedDate === cellDate);
|
|
85
|
+
|
|
86
|
+
const getCellColor = (level: number) =>
|
|
87
|
+
level === 0 ? "#666666" : getGradeColor(palette(), level as 0 | 1 | 2 | 3 | 4);
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<box flexDirection="column" gap={1}>
|
|
63
93
|
<box flexDirection="column">
|
|
64
94
|
<box flexDirection="row">
|
|
65
95
|
<text dim>{" ".repeat(dayLabelWidth())}</text>
|
|
66
96
|
<text dim>{monthLabelRow()}</text>
|
|
67
97
|
</box>
|
|
68
98
|
|
|
69
|
-
<box onMouseDown={(e) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
<box onMouseDown={(e: { x: number; y: number }) => {
|
|
100
|
+
const labelW = dayLabelWidth();
|
|
101
|
+
const col = Math.floor((e.x - labelW) / cellWidth);
|
|
102
|
+
const row = e.y - 2;
|
|
103
|
+
const gridRows = grid().length;
|
|
104
|
+
|
|
105
|
+
if (row < 0 || row >= gridRows) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (col < 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rowData = grid()[row];
|
|
113
|
+
if (!rowData || col >= rowData.length) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cell = rowData[col];
|
|
118
|
+
if (!cell?.date) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const newDate = clickedCell() === cell.date ? null : cell.date;
|
|
123
|
+
setClickedCell(newDate);
|
|
90
124
|
}}>
|
|
91
125
|
<For each={DAYS}>
|
|
92
|
-
{(day, dayIndex) => (
|
|
126
|
+
{(day, dayIndex) => (
|
|
127
|
+
<box flexDirection="row">
|
|
93
128
|
<text dim>{isNarrowTerminal() ? " " : day.padStart(3) + " "}</text>
|
|
94
129
|
<For each={grid()[dayIndex()] || []}>
|
|
95
|
-
{(cell) => (
|
|
130
|
+
{(cell) => (
|
|
131
|
+
<text
|
|
132
|
+
fg={isSelected(cell.date) ? "#ffffff" : getCellColor(cell.level)}
|
|
133
|
+
bg={isSelected(cell.date) ? getCellColor(cell.level) : undefined}
|
|
134
|
+
>
|
|
96
135
|
{isSelected(cell.date) ? "▓▓" : (cell.level === 0 ? "· " : "██")}
|
|
97
|
-
</text>
|
|
136
|
+
</text>
|
|
137
|
+
)}
|
|
98
138
|
</For>
|
|
99
|
-
</box>
|
|
139
|
+
</box>
|
|
140
|
+
)}
|
|
100
141
|
</For>
|
|
101
142
|
</box>
|
|
102
143
|
</box>
|
|
@@ -105,9 +146,13 @@ export function StatsView(props) {
|
|
|
105
146
|
<text dim>Less</text>
|
|
106
147
|
<box flexDirection="row" gap={0}>
|
|
107
148
|
<For each={[0, 1, 2, 3, 4]}>
|
|
108
|
-
{(level) => (
|
|
149
|
+
{(level) => (
|
|
150
|
+
<text
|
|
151
|
+
fg={level === 0 ? "#666666" : getGradeColor(palette(), level as 0 | 1 | 2 | 3 | 4)}
|
|
152
|
+
>
|
|
109
153
|
{level === 0 ? "· " : "██"}
|
|
110
|
-
</text>
|
|
154
|
+
</text>
|
|
155
|
+
)}
|
|
111
156
|
</For>
|
|
112
157
|
</box>
|
|
113
158
|
<text dim>More</text>
|
|
@@ -118,7 +163,7 @@ export function StatsView(props) {
|
|
|
118
163
|
</box>
|
|
119
164
|
|
|
120
165
|
<Show when={selectedBreakdown()}>
|
|
121
|
-
<DateBreakdownPanel breakdown={selectedBreakdown()} isNarrow={isNarrowTerminal()}/>
|
|
166
|
+
<DateBreakdownPanel breakdown={selectedBreakdown()!} isNarrow={isNarrowTerminal()} />
|
|
122
167
|
</Show>
|
|
123
168
|
|
|
124
169
|
<Show when={!selectedBreakdown()}>
|
|
@@ -175,6 +220,6 @@ export function StatsView(props) {
|
|
|
175
220
|
</box>
|
|
176
221
|
</Show>
|
|
177
222
|
</Show>
|
|
178
|
-
</box>
|
|
223
|
+
</box>
|
|
224
|
+
);
|
|
179
225
|
}
|
|
180
|
-
//# sourceMappingURL=StatsView.jsx.map
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { formatTokens, formatTokensCompact } from "../utils/format.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
|
|
3
|
+
export interface TokenBreakdownData {
|
|
4
|
+
input: number;
|
|
5
|
+
output: number;
|
|
6
|
+
cacheRead: number;
|
|
7
|
+
cacheWrite: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TokenBreakdownProps {
|
|
11
|
+
tokens: TokenBreakdownData;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
indent?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TokenBreakdown(props: TokenBreakdownProps) {
|
|
17
|
+
const indentStr = () => " ".repeat(props.indent ?? 0);
|
|
18
|
+
|
|
19
|
+
if (props.compact) {
|
|
20
|
+
return (
|
|
21
|
+
<box flexDirection="row">
|
|
6
22
|
<text fg="#666666">{indentStr()}</text>
|
|
7
23
|
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.input)}</text>
|
|
8
24
|
<text fg="#666666">{"/"}</text>
|
|
@@ -11,9 +27,12 @@ export function TokenBreakdown(props) {
|
|
|
11
27
|
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.cacheRead)}</text>
|
|
12
28
|
<text fg="#666666">{"/"}</text>
|
|
13
29
|
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.cacheWrite)}</text>
|
|
14
|
-
</box>
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
</box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<box flexDirection="row">
|
|
17
36
|
<text fg="#666666">{`${indentStr()}In: `}</text>
|
|
18
37
|
<text fg="#AAAAAA">{formatTokens(props.tokens.input)}</text>
|
|
19
38
|
<text fg="#666666">{" · Out: "}</text>
|
|
@@ -22,6 +41,6 @@ export function TokenBreakdown(props) {
|
|
|
22
41
|
<text fg="#AAAAAA">{formatTokens(props.tokens.cacheRead)}</text>
|
|
23
42
|
<text fg="#666666">{" · CW: "}</text>
|
|
24
43
|
<text fg="#AAAAAA">{formatTokens(props.tokens.cacheWrite)}</text>
|
|
25
|
-
</box>
|
|
44
|
+
</box>
|
|
45
|
+
);
|
|
26
46
|
}
|
|
27
|
-
//# sourceMappingURL=TokenBreakdown.jsx.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Header } from "./Header.js";
|
|
2
|
+
export { Footer } from "./Footer.js";
|
|
3
|
+
export { OverviewView } from "./OverviewView.js";
|
|
4
|
+
export { ModelView } from "./ModelView.js";
|
|
5
|
+
export { DailyView } from "./DailyView.js";
|
|
6
|
+
export { StatsView } from "./StatsView.js";
|
|
7
|
+
export { BarChart } from "./BarChart.js";
|
|
8
|
+
export type { ChartDataPoint } from "./BarChart.js";
|
|
9
|
+
export { Legend } from "./Legend.js";
|
|
10
|
+
export { LoadingSpinner } from "./LoadingSpinner.js";
|
|
11
|
+
export { ModelRow } from "./ModelRow.js";
|
|
12
|
+
export { TokenBreakdown } from "./TokenBreakdown.js";
|
|
13
|
+
export type { TokenBreakdownData } from "./TokenBreakdown.js";
|
|
14
|
+
export { DateBreakdownPanel } from "./DateBreakdownPanel.js";
|
|
15
|
+
export type { DateBreakdownPanelProps } from "./DateBreakdownPanel.js";
|