11import { useNavigation } from "@react-navigation/native" ;
2- import type { MergedUsage } from "@t3tools/shared/usageMerge" ;
2+ import type { DailyTotals , MergedUsage } from "@t3tools/shared/usageMerge" ;
33import {
44 enumerateDays ,
5+ enumerateHourStarts ,
56 formatCount ,
67 formatDayShort ,
8+ formatHourShort ,
79 formatPercent ,
810 formatTokens ,
911 formatUsd ,
@@ -23,6 +25,7 @@ import type { UsageChartMetric } from "./usageChartData";
2325import { PROVIDER_LABEL , useProviderColors } from "./usageProviders" ;
2426
2527const WINDOW_OPTIONS = [
28+ { days : 1 , label : "Past 24h" } ,
2629 { days : 7 , label : "7 days" } ,
2730 { days : 30 , label : "30 days" } ,
2831 { days : 90 , label : "90 days" } ,
@@ -33,23 +36,62 @@ const CHART_HEIGHT = 180;
3336export function UsageRouteScreen ( ) {
3437 const navigation = useNavigation ( ) ;
3538 const insets = useSafeAreaInsets ( ) ;
36- const [ windowDays , setWindowDays ] = useState < number > ( 30 ) ;
39+ const [ windowSelection , setWindowSelection ] = useState ( ( ) => ( {
40+ days : 30 ,
41+ window : makeWindow ( 30 ) ,
42+ } ) ) ;
3743 const [ metric , setMetric ] = useState < UsageChartMetric > ( "cost" ) ;
38-
39- // Recomputed only when the window length changes, so a re-render does not
40- // shift the range and refetch every environment.
41- const window = useMemo ( ( ) => makeWindow ( windowDays ) , [ windowDays ] ) ;
44+ const { days : windowDays , window } = windowSelection ;
45+ const isPast24Hours = windowDays === 1 ;
4246 const { merged, environments, isPending, isPartial, refresh } = useUsage ( window ) ;
4347
4448 const days = useMemo (
4549 ( ) => enumerateDays ( window . sinceDay , window . untilDay ) ,
4650 [ window . sinceDay , window . untilDay ] ,
4751 ) ;
52+ const chartDays = useMemo (
53+ ( ) =>
54+ isPast24Hours && window . sinceTime !== undefined && window . untilTime !== undefined
55+ ? enumerateHourStarts ( window . sinceTime , window . untilTime )
56+ : days ,
57+ [ days , isPast24Hours , window . sinceTime , window . untilTime ] ,
58+ ) ;
59+ const chartTotals = useMemo (
60+ ( ) : readonly DailyTotals [ ] =>
61+ isPast24Hours
62+ ? merged . hourly . map ( ( hour ) => ( {
63+ day : hour . hourStart ,
64+ costUsd : hour . costUsd ,
65+ totalTokens : hour . totalTokens ,
66+ byProvider : hour . byProvider ,
67+ } ) )
68+ : merged . daily ,
69+ [ isPast24Hours , merged . daily , merged . hourly ] ,
70+ ) ;
4871
4972 // The pull spinner tracks re-scans of environments that have answered
5073 // before. The initial scan renders its own placeholder, and an unreachable
5174 // environment stays pending forever — neither may pin the spinner on.
5275 const refreshing = environments . some ( ( entry ) => entry . isPending && entry . summary !== null ) ;
76+ const selectWindow = ( days : number ) => {
77+ setWindowSelection ( {
78+ days,
79+ window : makeWindow ( days , undefined , days === 1 ? "hour" : "day" ) ,
80+ } ) ;
81+ } ;
82+ const refreshWindow = ( ) => {
83+ const nextWindow = makeWindow ( windowDays , undefined , isPast24Hours ? "hour" : "day" ) ;
84+ if (
85+ nextWindow . sinceDay === window . sinceDay &&
86+ nextWindow . untilDay === window . untilDay &&
87+ nextWindow . sinceTime === window . sinceTime &&
88+ nextWindow . untilTime === window . untilTime
89+ ) {
90+ refresh ( ) ;
91+ } else {
92+ setWindowSelection ( { days : windowDays , window : nextWindow } ) ;
93+ }
94+ } ;
5395
5496 return (
5597 < View collapsable = { false } className = "flex-1 bg-sheet" >
@@ -65,12 +107,12 @@ export function UsageRouteScreen() {
65107 className = "flex-1"
66108 contentContainerClassName = "gap-6 px-5 pt-4"
67109 contentContainerStyle = { { paddingBottom : Math . max ( insets . bottom , 18 ) + 18 } }
68- refreshControl = { < RefreshControl refreshing = { refreshing } onRefresh = { refresh } /> }
110+ refreshControl = { < RefreshControl refreshing = { refreshing } onRefresh = { refreshWindow } /> }
69111 >
70112 < SegmentedControl
71113 options = { WINDOW_OPTIONS . map ( ( option ) => ( { value : option . days , label : option . label } ) ) }
72114 selected = { windowDays }
73- onSelect = { setWindowDays }
115+ onSelect = { selectWindow }
74116 />
75117
76118 < UsageCoverageNotice environments = { environments } merged = { merged } isPartial = { isPartial } />
@@ -87,14 +129,17 @@ export function UsageRouteScreen() {
87129 < >
88130 < ChartCard
89131 merged = { merged }
90- days = { days }
132+ days = { chartDays }
133+ daily = { chartTotals }
91134 metric = { metric }
92135 onMetricChange = { setMetric }
93136 sinceDay = { window . sinceDay }
94137 untilDay = { window . untilDay }
138+ isPast24Hours = { isPast24Hours }
139+ timeZone = { window . timeZone }
95140 />
96141 < ProviderSection merged = { merged } metric = { metric } />
97- < TotalsSection merged = { merged } />
142+ < TotalsSection merged = { merged } isPast24Hours = { isPast24Hours } />
98143 < ModelsSection merged = { merged } />
99144 </ >
100145 ) }
@@ -142,14 +187,17 @@ function SegmentedControl<Value extends number | string>(props: {
142187function ChartCard ( props : {
143188 readonly merged : MergedUsage ;
144189 readonly days : readonly string [ ] ;
190+ readonly daily : readonly DailyTotals [ ] ;
145191 readonly metric : UsageChartMetric ;
146192 readonly onMetricChange : ( metric : UsageChartMetric ) => void ;
147193 readonly sinceDay : string ;
148194 readonly untilDay : string ;
195+ readonly isPast24Hours : boolean ;
196+ readonly timeZone : string ;
149197} ) {
150198 const { merged, metric } = props ;
151199 const colors = useProviderColors ( ) ;
152- const hasActivity = merged . daily . some ( ( day ) => day . totalTokens > 0 ) ;
200+ const hasActivity = props . daily . some ( ( period ) => period . totalTokens > 0 ) ;
153201
154202 return (
155203 < View className = "gap-4 rounded-[24px] border-continuous bg-card p-4" >
@@ -173,7 +221,7 @@ function ChartCard(props: {
173221 { hasActivity ? (
174222 < UsageDailyChart
175223 days = { props . days }
176- daily = { merged . daily }
224+ daily = { props . daily }
177225 metric = { metric }
178226 height = { CHART_HEIGHT }
179227 />
@@ -184,7 +232,11 @@ function ChartCard(props: {
184232 ) }
185233
186234 < View className = "flex-row items-center justify-between" >
187- < Text className = "text-xs text-foreground-tertiary" > { formatDayShort ( props . sinceDay ) } </ Text >
235+ < Text className = "text-xs text-foreground-tertiary" >
236+ { props . isPast24Hours
237+ ? formatHourShort ( props . days [ 0 ] ?? "" , props . timeZone )
238+ : formatDayShort ( props . sinceDay ) }
239+ </ Text >
188240 < View className = "flex-row items-center gap-4" >
189241 { merged . providers . map ( ( provider ) => (
190242 < View key = { provider . provider } className = "flex-row items-center gap-1.5" >
@@ -198,7 +250,11 @@ function ChartCard(props: {
198250 </ View >
199251 ) ) }
200252 </ View >
201- < Text className = "text-xs text-foreground-tertiary" > { formatDayShort ( props . untilDay ) } </ Text >
253+ < Text className = "text-xs text-foreground-tertiary" >
254+ { props . isPast24Hours
255+ ? formatHourShort ( props . days [ props . days . length - 1 ] ?? "" , props . timeZone )
256+ : formatDayShort ( props . untilDay ) }
257+ </ Text >
202258 </ View >
203259 </ View >
204260 ) ;
@@ -292,10 +348,12 @@ function ProviderSection(props: {
292348 ) ;
293349}
294350
295- function TotalsSection ( props : { readonly merged : MergedUsage } ) {
351+ function TotalsSection ( props : { readonly merged : MergedUsage ; readonly isPast24Hours : boolean } ) {
296352 const { merged } = props ;
297- const activeDays = merged . daily . filter ( ( day ) => day . totalTokens > 0 ) . length ;
298- const dailyAverage = activeDays === 0 ? 0 : merged . totalTokens / activeDays ;
353+ const activePeriods = ( props . isPast24Hours ? merged . hourly : merged . daily ) . filter (
354+ ( period ) => period . totalTokens > 0 ,
355+ ) . length ;
356+ const periodAverage = activePeriods === 0 ? 0 : merged . totalTokens / activePeriods ;
299357 const observedInput = merged . uncachedInputTokens + merged . cachedInputTokens ;
300358 const cachedShare = observedInput === 0 ? 0 : merged . cachedInputTokens / observedInput ;
301359
@@ -305,7 +363,7 @@ function TotalsSection(props: { readonly merged: MergedUsage }) {
305363 < MetricCell
306364 label = "Processed tokens"
307365 value = { formatTokens ( merged . totalTokens ) }
308- detail = { `${ formatTokens ( dailyAverage ) } per active day` }
366+ detail = { `${ formatTokens ( periodAverage ) } per active ${ props . isPast24Hours ? "hour" : " day" } ` }
309367 />
310368 < MetricCell
311369 label = "Cache savings"
0 commit comments