88 FREEBUFF_GEMINI_PRO_MODEL_ID ,
99 FREEBUFF_KIMI_MODEL_ID ,
1010 FREEBUFF_MODELS ,
11+ FREEBUFF_PREMIUM_SESSION_LIMIT ,
12+ isFreebuffPremiumModelId ,
1113 getFreebuffDeploymentAvailabilityLabel ,
1214 isFreebuffModelAvailable ,
1315} from '@codebuff/common/constants/freebuff-models'
@@ -34,6 +36,10 @@ const FREEBUFF_MODEL_SELECTOR_MODELS = [
3436 ) ,
3537]
3638
39+ function formatSessionUnits ( units : number ) : string {
40+ return Number . isInteger ( units ) ? String ( units ) : units . toFixed ( 1 )
41+ }
42+
3743/**
3844 * Dual-purpose model picker:
3945 * - Pre-chat landing (session 'none'): user hasn't joined any queue. Picking
@@ -47,8 +53,7 @@ const FREEBUFF_MODEL_SELECTOR_MODELS = [
4753 * switch. Mouse clicks are still one-step. On the landing screen, pressing
4854 * Enter on the already-focused model also commits — there's nothing to lose.
4955 *
50- * Each row shows a live "N ahead" count sourced from the server's
51- * `queueDepthByModel` snapshot so the choice is informed.
56+ * Each row shows session-budget information so the choice is informed.
5257 */
5358export const FreebuffModelSelector : React . FC = ( ) => {
5459 const theme = useTheme ( )
@@ -86,48 +91,10 @@ export const FreebuffModelSelector: React.FC = () => {
8691 }
8792 } , [ now , selectedModel , session , setSelectedModel ] )
8893
89- // Landing ('none'): depths come from the server snapshot, no "self" to
90- // subtract. In-queue ('queued'): for the user's queue, "ahead" is
91- // `position - 1` (themselves don't count); for every other queue, switching
92- // would land them at the back, so it's that queue's full depth. Null before
93- // any snapshot so the UI doesn't flash misleading zeros — in particular,
94- // landing mode after a session ends initially sets status='none' with no
95- // queueDepthByModel; returning null here keeps the hint blank until the
96- // fetch lands, instead of showing "No wait" on every row.
97- const aheadByModel = useMemo < Record < string , number > | null > ( ( ) => {
98- if ( session ?. status === 'none' ) {
99- if ( ! session . queueDepthByModel ) return null
100- const depths = session . queueDepthByModel
101- const out : Record < string , number > = { }
102- for ( const { id } of FREEBUFF_MODELS ) out [ id ] = depths [ id ] ?? 0
103- return out
104- }
105- if ( session ?. status === 'queued' ) {
106- const depths = session . queueDepthByModel ?? { }
107- const out : Record < string , number > = { }
108- for ( const { id } of FREEBUFF_MODELS ) {
109- out [ id ] =
110- id === session . model
111- ? Math . max ( 0 , session . position - 1 )
112- : ( depths [ id ] ?? 0 )
113- }
114- return out
115- }
116- return null
117- } , [ session ] )
118-
119- // Pad the trailing hint ("3 ahead", "No wait", "…") to a fixed width so
120- // buttons don't visibly resize when the queue depth ticks down (12 → 9) or
121- // when the user's selection moves between queues. The tagline is shown
122- // inline with the name now, so it's no longer part of this slot.
94+ // Pad the trailing hint to a fixed width so buttons don't visibly resize
95+ // when quota snapshots change.
12396 const hintWidth = useMemo (
124- ( ) =>
125- Math . max (
126- 'No wait' . length ,
127- '999 ahead' . length ,
128- 'Used today' . length ,
129- 'Limit used' . length ,
130- ) ,
97+ ( ) => Math . max ( 'Unlimited' . length , '5/5 used' . length , '4.9/5 used' . length ) ,
13198 [ ] ,
13299 )
133100
@@ -246,9 +213,10 @@ export const FreebuffModelSelector: React.FC = () => {
246213 const isFocused = focusedId === model . id && ! isSelected
247214 const isAvailable = isFreebuffModelAvailable ( model . id , new Date ( now ) )
248215 const rateLimit = rateLimitsByModel ?. [ model . id ]
249- const isQuotaExhausted =
250- rateLimit !== undefined && rateLimit . recentCount >= rateLimit . limit
251- const canJoin = isAvailable && ! isQuotaExhausted
216+ const isPremium = isFreebuffPremiumModelId ( model . id )
217+ const hasFullPremiumSession =
218+ ! rateLimit || rateLimit . recentCount < rateLimit . limit
219+ const canJoin = isAvailable && hasFullPremiumSession
252220 const indicator = isSelected ? '●' : isFocused ? '›' : '○'
253221 const indicatorColor = isSelected
254222 ? theme . primary
@@ -263,18 +231,12 @@ export const FreebuffModelSelector: React.FC = () => {
263231 // anything except re-picking the queue we're already in.
264232 const interactable =
265233 ! pending && canJoin && model . id !== committedModelId
266- const ahead = aheadByModel ?. [ model . id ]
267- const hint = ! isAvailable
268- ? 'Closed'
269- : isQuotaExhausted
270- ? model . id === FREEBUFF_GEMINI_PRO_MODEL_ID
271- ? 'Used today'
272- : 'Limit used'
273- : ahead === undefined
274- ? ''
275- : ahead === 0
276- ? 'No wait'
277- : `${ ahead } ahead`
234+ const quotaHint = rateLimit
235+ ? `${ formatSessionUnits ( rateLimit . recentCount ) } /${ rateLimit . limit } used`
236+ : isPremium
237+ ? `0/${ FREEBUFF_PREMIUM_SESSION_LIMIT } used`
238+ : 'Unlimited'
239+ const hint = isAvailable ? quotaHint : 'Closed'
278240 const hintColor = canJoin ? theme . muted : theme . secondary
279241
280242 const borderColor = isSelected
0 commit comments