99 ToolCallRenderer ,
1010 useAgentToolcall ,
1111 useChat ,
12- AgentStateProvider ,
12+ useAgentState ,
1313} from '@tdesign-react/chat' ;
14- import { CheckCircleFilledIcon , TimeFilledIcon , ErrorCircleFilledIcon } from 'tdesign-icons-react' ;
14+ import { CheckCircleFilledIcon , TimeFilledIcon , ErrorCircleFilledIcon , LoadingIcon } from 'tdesign-icons-react' ;
1515import type {
1616 TdChatMessageConfig ,
1717 TdChatSenderParams ,
@@ -89,10 +89,9 @@ const PlanningSteps: React.FC<ToolcallComponentProps<PlanningArgs>> = ({
8989 respond,
9090 agentState,
9191} ) => {
92- // 直接使用注入的 agentState,无需额外 Hook
93- const planningState = agentState ?. [ args ?. taskId ] || { } ;
94- const items = planningState ?. items || [ ] ;
95-
92+ // 因为配置了 subscribeKey,agentState 已经是 taskId 对应的状态对象
93+ const planningState = agentState || { } ;
94+
9695 const isComplete = status === 'complete' ;
9796
9897 React . useEffect ( ( ) => {
@@ -101,49 +100,21 @@ const PlanningSteps: React.FC<ToolcallComponentProps<PlanningArgs>> = ({
101100 }
102101 } , [ isComplete , respond ] ) ;
103102
104- const getStatusIcon = ( itemStatus : string ) => {
105- switch ( itemStatus ) {
106- case 'completed' :
107- return < CheckCircleFilledIcon style = { { color : '#00a870' } } /> ;
108- case 'running' :
109- return < TimeFilledIcon style = { { color : '#0052d9' } } /> ;
110- case 'failed' :
111- return < ErrorCircleFilledIcon style = { { color : '#e34d59' } } /> ;
112- default :
113- return < TimeFilledIcon style = { { color : '#bbbbbb' } } /> ;
114- }
115- } ;
116-
117103 return (
118104 < Card bordered style = { { marginTop : 8 } } >
119105 < div style = { { fontSize : 14 , fontWeight : 600 , marginBottom : 12 } } >
120106 正在为您规划 { args ?. destination } { args ?. days } 日游
121107 </ div >
122108
123- { /* 进度条 */ }
109+ { /* 只保留进度条 */ }
124110 { planningState ?. progress !== undefined && (
125- < div style = { { marginBottom : 16 } } >
111+ < div >
126112 < Progress percentage = { planningState . progress } />
127113 < div style = { { fontSize : 12 , color : '#888' , marginTop : 4 } } >
128114 { planningState . message || '规划中...' }
129115 </ div >
130116 </ div >
131117 ) }
132-
133- { /* 步骤列表 */ }
134- { items . length > 0 && (
135- < Space direction = "vertical" size = "small" style = { { width : '100%' } } >
136- { items . map ( ( item : any , index : number ) => (
137- < div key = { index } style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
138- { getStatusIcon ( item . status ) }
139- < span style = { { flex : 1 } } > { item . label } </ span >
140- < Tag theme = { item . status === 'completed' ? 'success' : 'default' } size = "small" >
141- { item . status }
142- </ Tag >
143- </ div >
144- ) ) }
145- </ Space >
146- ) }
147118 </ Card >
148119 ) ;
149120} ;
@@ -152,6 +123,7 @@ const PlanningSteps: React.FC<ToolcallComponentProps<PlanningArgs>> = ({
152123const UserPreferencesForm : React . FC < ToolcallComponentProps < UserPreferencesArgs , any , UserPreferencesResponse > > = ( {
153124 status,
154125 respond,
126+ result,
155127} ) => {
156128 const [ budget , setBudget ] = useState ( 5000 ) ;
157129 const [ interests , setInterests ] = useState < string [ ] > ( [ '美食' , '文化' ] ) ;
@@ -165,10 +137,23 @@ const UserPreferencesForm: React.FC<ToolcallComponentProps<UserPreferencesArgs,
165137 } ) ;
166138 } ;
167139
168- if ( status === 'complete' ) {
140+ if ( status === 'complete' && result ) {
169141 return (
170142 < Card bordered style = { { marginTop : 8 } } >
171- < div style = { { color : '#00a870' } } > ✓ 已收到您的偏好设置</ div >
143+ < div style = { { fontSize : 14 , fontWeight : 600 , marginBottom : 8 , color : '#00a870' } } >
144+ ✓ 已收到您的偏好设置
145+ </ div >
146+ < Space direction = "vertical" size = "small" >
147+ < div style = { { fontSize : 12 , color : '#666' } } >
148+ 预算:¥{ result . budget }
149+ </ div >
150+ < div style = { { fontSize : 12 , color : '#666' } } >
151+ 兴趣:{ result . interests . join ( '、' ) }
152+ </ div >
153+ < div style = { { fontSize : 12 , color : '#666' } } >
154+ 住宿:{ result . accommodation }
155+ </ div >
156+ </ Space >
172157 </ Card >
173158 ) ;
174159 }
@@ -222,14 +207,118 @@ const UserPreferencesForm: React.FC<ToolcallComponentProps<UserPreferencesArgs,
222207 ) ;
223208} ;
224209
210+ // ==================== 外部进度面板组件 ====================
211+
212+ /**
213+ * 右侧进度面板组件
214+ * 演示如何在对话组件外部使用 useAgentState 获取状态
215+ *
216+ * 💡 使用场景:展示规划行程的详细子步骤(从后端 STATE_DELTA 事件推送)
217+ *
218+ * 实现方式:
219+ * 1. 使用 useAgentState 订阅状态更新
220+ * 2. 从 stateMap 中获取规划步骤的详细进度
221+ */
222+ const ProgressPanel : React . FC = ( ) => {
223+ // 使用 useAgentState 订阅状态更新
224+ const { stateMap, currentStateKey } = useAgentState ( ) ;
225+
226+ // 获取规划状态
227+ const planningState = useMemo ( ( ) => {
228+ if ( ! currentStateKey || ! stateMap [ currentStateKey ] ) {
229+ return null ;
230+ }
231+ return stateMap [ currentStateKey ] ;
232+ } , [ stateMap , currentStateKey ] ) ;
233+
234+ // 如果没有规划状态,不显示面板
235+ if ( ! planningState || ! planningState . items || planningState . items . length === 0 ) {
236+ return null ;
237+ }
238+
239+ const items = planningState . items || [ ] ;
240+ const completedCount = items . filter ( ( item : any ) => item . status === 'completed' ) . length ;
241+ const totalCount = items . length ;
242+
243+ // 如果所有步骤都完成了,隐藏面板
244+ if ( completedCount === totalCount && totalCount > 0 ) {
245+ return null ;
246+ }
247+
248+ const getStatusIcon = ( itemStatus : string ) => {
249+ switch ( itemStatus ) {
250+ case 'completed' :
251+ return < CheckCircleFilledIcon style = { { color : '#00a870' , fontSize : '14px' } } /> ;
252+ case 'running' :
253+ return < LoadingIcon style = { { color : '#0052d9' , fontSize : '14px' } } /> ;
254+ case 'failed' :
255+ return < ErrorCircleFilledIcon style = { { color : '#e34d59' , fontSize : '14px' } } /> ;
256+ default :
257+ return < TimeFilledIcon style = { { color : '#bbbbbb' , fontSize : '14px' } } /> ;
258+ }
259+ } ;
260+
261+ return (
262+ < div style = { {
263+ position : 'fixed' ,
264+ right : '200px' ,
265+ top : '50%' ,
266+ transform : 'translateY(-50%)' ,
267+ width : '200px' ,
268+ background : '#fff' ,
269+ padding : '16px' ,
270+ borderRadius : '8px' ,
271+ boxShadow : '0 2px 12px rgba(0, 0, 0, 0.1)' ,
272+ border : '1px solid #e7e7e7' ,
273+ zIndex : 1000 ,
274+ } } >
275+ < div style = { {
276+ marginBottom : '12px' ,
277+ paddingBottom : '8px' ,
278+ borderBottom : '1px solid #e7e7e7'
279+ } } >
280+ < div style = { { fontSize : '14px' , fontWeight : 600 , color : '#000' , marginBottom : '4px' } } >
281+ 规划进度
282+ </ div >
283+ < Tag theme = "primary" variant = "light" size = "small" >
284+ { completedCount } /{ totalCount }
285+ </ Tag >
286+ </ div >
287+
288+ { /* 步骤列表 */ }
289+ < Space direction = "vertical" size = "small" style = { { width : '100%' } } >
290+ { items . map ( ( item : any , index : number ) => (
291+ < div key = { index } style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
292+ { getStatusIcon ( item . status ) }
293+ < span style = { {
294+ flex : 1 ,
295+ fontSize : '12px' ,
296+ color : item . status === 'completed' ? '#00a870' : ( item . status === 'running' ? '#0052d9' : '#666' ) ,
297+ fontWeight : item . status === 'running' ? 600 : 400 ,
298+ } } >
299+ { item . label }
300+ </ span >
301+ </ div >
302+ ) ) }
303+ </ Space >
304+ </ div >
305+ ) ;
306+ } ;
307+
225308// ==================== 主组件 ====================
226309const TravelPlannerContent : React . FC = ( ) => {
227- const listRef = useRef < TdChatListApi > ( null ) ;
228- const inputRef = useRef < TdChatSenderApi > ( null ) ;
310+ const listRef = useRef < TdChatListApi & HTMLElement > ( null ) ;
311+ const inputRef = useRef < TdChatSenderApi & HTMLElement > ( null ) ;
229312 const [ inputValue , setInputValue ] = useState < string > ( '请为我规划一个北京3日游行程' ) ;
230313
231- // 注册工具配置(利用 agentState 注入)
314+ // 注册工具配置
232315 useAgentToolcall ( [
316+ {
317+ name : 'collect_user_preferences' ,
318+ description : '收集用户偏好' ,
319+ parameters : [ { name : 'destination' , type : 'string' , required : true } ] ,
320+ component : UserPreferencesForm as any ,
321+ } ,
233322 {
234323 name : 'query_weather' ,
235324 description : '查询目的地天气' ,
@@ -245,12 +334,8 @@ const TravelPlannerContent: React.FC = () => {
245334 { name : 'taskId' , type : 'string' , required : true } ,
246335 ] ,
247336 component : PlanningSteps as any ,
248- } ,
249- {
250- name : 'collect_user_preferences' ,
251- description : '收集用户偏好' ,
252- parameters : [ { name : 'destination' , type : 'string' , required : true } ] ,
253- component : UserPreferencesForm as any ,
337+ // 配置 subscribeKey,让组件订阅对应 taskId 的状态
338+ subscribeKey : ( props ) => props . args ?. taskId ,
254339 } ,
255340 ] ) ;
256341
@@ -290,17 +375,20 @@ const TravelPlannerContent: React.FC = () => {
290375 // 处理工具调用响应
291376 const handleToolCallRespond = useCallback (
292377 async ( toolcall : ToolCall , response : any ) => {
293- const tools = chatEngine . getToolcallByName ( toolcall . toolCallName ) || { } ;
294- await chatEngine . sendAIMessage ( {
295- params : {
296- toolCallMessage : {
297- ...tools ,
298- result : JSON . stringify ( response ) ,
378+ // 判断如果是手机用户偏好的响应,则使用 toolcall 中的信息来构建新的请求
379+ if ( toolcall . toolCallName === 'collect_user_preferences' ) {
380+ await chatEngine . sendAIMessage ( {
381+ params : {
382+ toolCallMessage : {
383+ toolCallId : toolcall . toolCallId ,
384+ toolCallName : toolcall . toolCallName ,
385+ result : JSON . stringify ( response ) ,
386+ } ,
299387 } ,
300- } ,
301- sendRequest : true ,
302- } ) ;
303- listRef . current ?. scrollList ( { to : 'bottom' } ) ;
388+ sendRequest : true ,
389+ } ) ;
390+ listRef . current ?. scrollList ( { to : 'bottom' } ) ;
391+ }
304392 } ,
305393 [ chatEngine ] ,
306394 ) ;
@@ -320,17 +408,6 @@ const TravelPlannerContent: React.FC = () => {
320408 [ handleToolCallRespond ] ,
321409 ) ;
322410
323- // 操作栏
324- const actionHandler = ( name : string ) => {
325- switch ( name ) {
326- case 'replay' :
327- chatEngine . regenerateAIMessage ( ) ;
328- break ;
329- default :
330- console . log ( '触发操作' , name ) ;
331- }
332- } ;
333-
334411 const renderMsgContents = ( message : ChatMessagesData ) => (
335412 < >
336413 { message . content ?. map ( ( item : any , index : number ) => renderMessageContent ( item , index ) ) }
@@ -344,10 +421,13 @@ const TravelPlannerContent: React.FC = () => {
344421 } ;
345422
346423 return (
347- < div style = { { height : '400px' , display : 'flex' , flexDirection : 'column' } } >
424+ < div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' , position : 'relative' } } >
425+ { /* 右侧进度面板:使用 useAgentState 订阅状态 */ }
426+ < ProgressPanel />
427+
348428 < div style = { { flex : 1 , display : 'flex' , flexDirection : 'column' } } >
349429 < ChatList ref = { listRef } >
350- { messages . map ( ( message , idx ) => (
430+ { messages . map ( ( message ) => (
351431 < ChatMessage key = { message . id } { ...messageProps [ message . role ] } message = { message } >
352432 { renderMsgContents ( message ) }
353433 </ ChatMessage >
@@ -367,11 +447,5 @@ const TravelPlannerContent: React.FC = () => {
367447 ) ;
368448} ;
369449
370- // 使用 Provider 包裹
371- export default function TravelPlannerChat ( ) {
372- return (
373- < AgentStateProvider initialState = { { } } >
374- < TravelPlannerContent />
375- </ AgentStateProvider >
376- ) ;
377- }
450+ // 导出主组件(不需要 Provider,因为 useAgentState 内部已处理)
451+ export default TravelPlannerContent ;
0 commit comments