1414
1515import { VanillaChatStore } from '@/store/chatStore' ;
1616import { AgentStep , ChatTaskStatus } from '@/types/constants' ;
17- import { motion } from 'framer-motion' ;
17+ import { AnimatePresence , motion } from 'framer-motion' ;
1818import { FileText } from 'lucide-react' ;
19- import React , {
20- useEffect ,
21- useRef ,
22- useState ,
23- useSyncExternalStore ,
24- } from 'react' ;
19+ import React , { useEffect , useRef , useState } from 'react' ;
20+ import { useTranslation } from 'react-i18next' ;
2521import { AgentMessageCard } from './MessageItem/AgentMessageCard' ;
2622import { NoticeCard } from './MessageItem/NoticeCard' ;
2723import { TaskCompletionCard } from './MessageItem/TaskCompletionCard' ;
2824import { UserMessageCard } from './MessageItem/UserMessageCard' ;
2925import { StreamingTaskList } from './TaskBox/StreamingTaskList' ;
3026import { TaskCard } from './TaskBox/TaskCard' ;
3127import { TypeCardSkeleton } from './TaskBox/TypeCardSkeleton' ;
28+ import { AnimatedTokenNumber } from './TokenUtils' ;
3229
3330interface QueryGroup {
3431 queryId : string ;
@@ -54,35 +51,25 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
5451 onQueryActive,
5552 index,
5653} ) => {
54+ const { t } = useTranslation ( ) ;
5755 const groupRef = useRef < HTMLDivElement > ( null ) ;
5856 const taskBoxRef = useRef < HTMLDivElement > ( null ) ;
5957 const [ _isTaskBoxSticky , setIsTaskBoxSticky ] = useState ( false ) ;
6058 const [ isCompletionReady , setIsCompletionReady ] = useState ( false ) ;
6159 const chatState = chatStore . getState ( ) ;
6260 const activeTaskId = chatState . activeTaskId ;
63-
64- // Subscribe to streaming decompose text separately for efficient updates
65- const streamingDecomposeText = useSyncExternalStore (
66- ( callback ) => chatStore . subscribe ( callback ) ,
67- ( ) => {
68- const state = chatStore . getState ( ) ;
69- const taskId = state . activeTaskId ;
70- if ( ! taskId || ! state . tasks [ taskId ] ) return '' ;
71- return state . tasks [ taskId ] . streamingDecomposeText || '' ;
72- }
73- ) ;
61+ const activeTask = activeTaskId ? chatState . tasks [ activeTaskId ] : null ;
7462
7563 // Show task if this query group has a task message OR if it's the most recent user query during splitting
7664 // During splitting phase (no to_sub_tasks yet), show task for the most recent query only
7765 // Exclude human-reply scenarios (when user is replying to an activeAsk)
7866 const isHumanReply =
7967 queryGroup . userMessage &&
80- activeTaskId &&
81- chatState . tasks [ activeTaskId ] &&
82- ( chatState . tasks [ activeTaskId ] . activeAsk ||
68+ activeTask &&
69+ ( activeTask . activeAsk ||
8370 // Check if this user message follows an 'ask' message in the message sequence
8471 ( ( ) => {
85- const messages = chatState . tasks [ activeTaskId ] . messages ;
72+ const messages = activeTask . messages ;
8673 const userMessageIndex = messages . findIndex (
8774 ( m : any ) => m . id === queryGroup . userMessage . id
8875 ) ;
@@ -99,29 +86,27 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
9986 const isLastUserQuery =
10087 ! queryGroup . taskMessage &&
10188 ! isHumanReply &&
102- activeTaskId &&
103- chatState . tasks [ activeTaskId ] &&
89+ activeTask &&
10490 queryGroup . userMessage &&
10591 queryGroup . userMessage . id ===
106- chatState . tasks [ activeTaskId ] . messages
107- . filter ( ( m : any ) => m . role === 'user' )
108- . pop ( ) ?. id &&
92+ activeTask . messages . filter ( ( m : any ) => m . role === 'user' ) . pop ( ) ?. id &&
10993 // Only show during active phases (not finished)
110- chatState . tasks [ activeTaskId ] . status !== ChatTaskStatus . FINISHED ;
94+ activeTask . status !== ChatTaskStatus . FINISHED ;
11195
11296 // Only show the fallback task box for the newest query while the agent is still splitting work.
11397 // Simple Q&A sessions set hasWaitComfirm to true, so we should not render an empty task box there.
11498 // Also, do not show fallback task if we are currently decomposing (streaming text).
99+ const streamingDecomposeText = activeTask ?. streamingDecomposeText || '' ;
115100 const isDecomposing = streamingDecomposeText . length > 0 ;
116101 const shouldShowFallbackTask =
117102 isLastUserQuery &&
118- activeTaskId &&
119- ! chatState . tasks [ activeTaskId ] . hasWaitComfirm &&
103+ activeTask &&
104+ ! activeTask . hasWaitComfirm &&
120105 ! isDecomposing ;
121106
122107 const task =
123- ( queryGroup . taskMessage || shouldShowFallbackTask ) && activeTaskId
124- ? chatState . tasks [ activeTaskId ]
108+ ( queryGroup . taskMessage || shouldShowFallbackTask ) && activeTask
109+ ? activeTask
125110 : null ;
126111
127112 // Reset completion flag when active task or query group changes
@@ -292,6 +277,25 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
292277 </ motion . div >
293278 ) }
294279
280+ { /* Live token count – visible only while the task is running */ }
281+ < AnimatePresence >
282+ { task && task . status === ChatTaskStatus . RUNNING && (
283+ < motion . div
284+ key = "live-token-count"
285+ initial = { { opacity : 0 , y : 4 } }
286+ animate = { { opacity : 1 , y : 0 } }
287+ exit = { { opacity : 0 , y : - 4 } }
288+ transition = { { duration : 0.25 , ease : 'easeOut' } }
289+ className = "mt-6 flex items-center justify-end gap-1 px-sm py-1 text-xs text-text-label"
290+ >
291+ < span > { t ( 'chat.current-task' ) } </ span >
292+ < span > ·</ span >
293+ < AnimatedTokenNumber value = { task . tokens || 0 } />
294+ < span > { t ( 'chat.tokens' ) } </ span >
295+ </ motion . div >
296+ ) }
297+ </ AnimatePresence >
298+
295299 { /* Other Messages */ }
296300 { queryGroup . otherMessages . map ( ( message ) => {
297301 if ( message . content . length > 0 ) {
0 commit comments