1+ "use client"
2+
3+ import React , { useState , useEffect , useRef } from 'react'
4+ import { createPortal } from 'react-dom'
5+ import { ChevronDown } from 'lucide-react'
6+ import { Button } from '@/components/ui/button'
7+ import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from '@/components/ui/tooltip'
8+ import { fetchAllAgents } from '@/services/agentConfigService'
9+ import { useTranslation } from 'react-i18next'
10+
11+ interface Agent {
12+ agent_id : number
13+ name : string
14+ description : string
15+ is_available : boolean
16+ }
17+
18+ interface AgentSelectorProps {
19+ selectedAgentId : number | null
20+ onAgentSelect : ( agentId : number | null ) => void
21+ disabled ?: boolean
22+ isInitialMode ?: boolean
23+ }
24+
25+ export function AgentSelector ( { selectedAgentId, onAgentSelect, disabled = false , isInitialMode = false } : AgentSelectorProps ) {
26+ const [ agents , setAgents ] = useState < Agent [ ] > ( [ ] )
27+ const [ isOpen , setIsOpen ] = useState ( false )
28+ const [ isLoading , setIsLoading ] = useState ( false )
29+ const [ dropdownPosition , setDropdownPosition ] = useState ( { top : 0 , left : 0 , direction : 'down' } )
30+ const { t } = useTranslation ( 'common' )
31+ const buttonRef = useRef < HTMLDivElement > ( null )
32+
33+ const selectedAgent = agents . find ( agent => agent . agent_id === selectedAgentId )
34+
35+ useEffect ( ( ) => {
36+ loadAgents ( )
37+ } , [ ] )
38+
39+ // 计算下拉框位置
40+ useEffect ( ( ) => {
41+ if ( isOpen && buttonRef . current ) {
42+ const buttonRect = buttonRef . current . getBoundingClientRect ( )
43+ const viewportHeight = window . innerHeight
44+ const dropdownHeight = 240 // 估算下拉框高度 (max-h-60)
45+
46+ // 检查是否有足够空间向下显示
47+ const hasSpaceBelow = buttonRect . bottom + dropdownHeight + 10 < viewportHeight
48+ // 检查是否有足够空间向上显示
49+ const hasSpaceAbove = buttonRect . top - dropdownHeight - 10 > 0
50+
51+ let direction = 'down'
52+ let top = buttonRect . bottom + 4
53+
54+ // 决定方向:优先使用建议方向,但如果空间不足则调整
55+ if ( isInitialMode ) {
56+ // 初始模式优先向下
57+ if ( ! hasSpaceBelow && hasSpaceAbove ) {
58+ direction = 'up'
59+ top = buttonRect . top - 4
60+ }
61+ } else {
62+ // 非初始模式优先向上
63+ direction = 'up'
64+ top = buttonRect . top - 4
65+ if ( ! hasSpaceAbove && hasSpaceBelow ) {
66+ direction = 'down'
67+ top = buttonRect . bottom + 4
68+ }
69+ }
70+
71+ setDropdownPosition ( {
72+ top,
73+ left : buttonRect . left ,
74+ direction
75+ } )
76+ }
77+ } , [ isOpen , isInitialMode ] )
78+
79+ // 监听窗口滚动和尺寸变化,关闭下拉框
80+ useEffect ( ( ) => {
81+ if ( ! isOpen ) return
82+
83+ const handleScroll = ( e : Event ) => {
84+ // 如果滚动发生在下拉框内部,不关闭下拉框
85+ const target = e . target as Node
86+ const dropdownElement = document . querySelector ( '.agent-selector-dropdown' )
87+ if ( dropdownElement && ( dropdownElement === target || dropdownElement . contains ( target ) ) ) {
88+ return
89+ }
90+
91+ // 如果是页面滚动或其他容器的滚动,则关闭下拉框
92+ setIsOpen ( false )
93+ }
94+
95+ const handleResize = ( ) => {
96+ setIsOpen ( false )
97+ }
98+
99+ // 使用事件捕获阶段
100+ window . addEventListener ( 'scroll' , handleScroll , true )
101+ window . addEventListener ( 'resize' , handleResize )
102+
103+ return ( ) => {
104+ window . removeEventListener ( 'scroll' , handleScroll , true )
105+ window . removeEventListener ( 'resize' , handleResize )
106+ }
107+ } , [ isOpen ] )
108+
109+ const loadAgents = async ( ) => {
110+ setIsLoading ( true )
111+ try {
112+ const result = await fetchAllAgents ( )
113+ if ( result . success ) {
114+ setAgents ( result . data )
115+ }
116+ } catch ( error ) {
117+ console . error ( '加载Agent列表失败:' , error )
118+ } finally {
119+ setIsLoading ( false )
120+ }
121+ }
122+
123+ const handleAgentSelect = ( agentId : number | null ) => {
124+ // 只有可用的Agent才能被选择
125+ if ( agentId !== null ) {
126+ const agent = agents . find ( a => a . agent_id === agentId )
127+ if ( agent && ! agent . is_available ) {
128+ return // 不可用的Agent不能被选择
129+ }
130+ }
131+ onAgentSelect ( agentId )
132+ setIsOpen ( false )
133+ }
134+
135+ // 显示所有agents,包括不可用的
136+ const allAgents = agents
137+
138+ return (
139+ < div className = "relative" >
140+ < div
141+ ref = { buttonRef }
142+ className = { `
143+ relative h-8 min-w-[120px] max-w-[180px] px-2
144+ rounded-md border border-slate-200
145+ bg-white hover:bg-slate-50
146+ flex items-center justify-between
147+ cursor-pointer select-none
148+ transition-colors duration-150
149+ ${ disabled || isLoading ? 'opacity-50 cursor-not-allowed' : '' }
150+ ${ isOpen ? 'border-blue-400 ring-2 ring-blue-100' : 'hover:border-slate-300' }
151+ ` }
152+ onClick = { ( ) => ! disabled && ! isLoading && setIsOpen ( ! isOpen ) }
153+ >
154+ < div className = "flex items-center gap-2 truncate" >
155+ { selectedAgent && (
156+ < div className = "w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
157+ ) }
158+ < span className = { `truncate text-sm ${ selectedAgent ? 'font-medium text-slate-700' : 'text-slate-500' } ` } >
159+ { isLoading
160+ ? (
161+ < div className = "flex items-center gap-2" >
162+ < div className = "w-4 h-4 border-2 border-slate-300 border-t-slate-500 rounded-full animate-spin" />
163+ < span > { t ( 'agentSelector.loading' ) } </ span >
164+ </ div >
165+ )
166+ : selectedAgent
167+ ? selectedAgent . name
168+ : t ( 'agentSelector.selectAgent' )
169+ }
170+ </ span >
171+ </ div >
172+ < ChevronDown
173+ className = { `h-4 w-4 text-slate-400 transition-transform duration-200 ${ isOpen ? 'rotate-180' : '' } ` }
174+ />
175+ </ div >
176+
177+ { /* Portal渲染下拉框到body,避免被父容器遮挡 */ }
178+ { isOpen && typeof window !== 'undefined' && createPortal (
179+ < >
180+ { /* 覆盖层 */ }
181+ < div
182+ className = "fixed inset-0 z-[9998]"
183+ onClick = { ( ) => setIsOpen ( false ) }
184+ onWheel = { ( e ) => {
185+ // 如果滚动发生在下拉框内部,不关闭下拉框
186+ const target = e . target as Node
187+ const dropdownElement = document . querySelector ( '.agent-selector-dropdown' )
188+ if ( dropdownElement && ( dropdownElement === target || dropdownElement . contains ( target ) ) ) {
189+ return
190+ }
191+ setIsOpen ( false )
192+ } }
193+ />
194+
195+ { /* 下拉框 */ }
196+ < div
197+ className = "agent-selector-dropdown fixed w-64 bg-white border border-slate-200 rounded-md shadow-lg z-[9999] max-h-60 overflow-y-auto"
198+ style = { {
199+ top : dropdownPosition . direction === 'up'
200+ ? `${ dropdownPosition . top } px`
201+ : `${ dropdownPosition . top } px` ,
202+ left : `${ dropdownPosition . left } px` ,
203+ transform : dropdownPosition . direction === 'up' ? 'translateY(-100%)' : 'none'
204+ } }
205+ onWheel = { ( e ) => {
206+ // 阻止滚动事件冒泡,但允许正常滚动
207+ e . stopPropagation ( )
208+ } }
209+ >
210+ < div className = "py-1" >
211+ { allAgents . length === 0 ? (
212+ < div className = "px-3 py-2.5 text-sm text-slate-500 text-center" >
213+ { isLoading ? (
214+ < div className = "flex items-center justify-center gap-2" >
215+ < div className = "w-4 h-4 border-2 border-slate-300 border-t-slate-500 rounded-full animate-spin" />
216+ < span > { t ( 'agentSelector.loading' ) } </ span >
217+ </ div >
218+ ) : (
219+ t ( 'agentSelector.noAvailableAgents' )
220+ ) }
221+ </ div >
222+ ) : (
223+ allAgents . map ( ( agent , idx ) => (
224+ < TooltipProvider key = { agent . agent_id } >
225+ < Tooltip >
226+ < TooltipTrigger asChild >
227+ < div
228+ className = { `
229+ flex items-center px-3.5 py-2.5 text-sm
230+ transition-all duration-150 ease-in-out
231+ ${ agent . is_available
232+ ? `hover:bg-slate-50 cursor-pointer ${
233+ selectedAgentId === agent . agent_id
234+ ? 'bg-blue-50/70 text-blue-600 hover:bg-blue-50/70 font-medium'
235+ : ''
236+ } `
237+ : 'cursor-not-allowed bg-slate-50/50'
238+ }
239+ ${ selectedAgentId === agent . agent_id ? 'shadow-[inset_2px_0_0_0] shadow-blue-500' : '' }
240+ ${ idx !== 0 ? 'border-t border-slate-100' : '' }
241+ ` }
242+ onClick = { ( ) => agent . is_available && handleAgentSelect ( agent . agent_id ) }
243+ >
244+ < div className = "flex-1 truncate" >
245+ < div className = { `${
246+ agent . is_available
247+ ? selectedAgentId === agent . agent_id
248+ ? 'text-blue-600'
249+ : 'text-slate-700 hover:text-slate-900'
250+ : 'text-slate-400'
251+ } `} >
252+ { agent . name }
253+ </ div >
254+ </ div >
255+ </ div >
256+ </ TooltipTrigger >
257+ < TooltipContent
258+ side = "right"
259+ className = "max-w-lg bg-white rounded-lg shadow-lg border border-slate-200 p-0 overflow-hidden"
260+ sideOffset = { 5 }
261+ >
262+ < div className = "relative" >
263+ { /* 顶部状态条 */ }
264+ < div className = { `h-1 w-full ${ agent . is_available ? 'bg-green-500' : 'bg-red-400' } ` } />
265+
266+ { /* 主要内容区 */ }
267+ < div className = "p-4" >
268+ { /* 描述文本 */ }
269+ < div className = "text-sm leading-relaxed text-slate-600 whitespace-normal break-words" >
270+ { agent . description }
271+ </ div >
272+
273+ { /* 不可用状态提示 */ }
274+ { ! agent . is_available && (
275+ < div className = "mt-3 pt-3 border-t border-slate-200 flex items-start gap-2.5 text-sm text-red-500" >
276+ < div className = "w-2 h-2 rounded-full bg-red-500 flex-shrink-0 animate-pulse mt-1" />
277+ { t ( 'agentSelector.agentUnavailable' ) }
278+ </ div >
279+ ) }
280+ </ div >
281+ </ div >
282+ </ TooltipContent >
283+ </ Tooltip >
284+ </ TooltipProvider >
285+ ) )
286+ ) }
287+ </ div >
288+ </ div >
289+ </ > ,
290+ document . body
291+ ) }
292+ </ div >
293+ )
294+ }
0 commit comments