import { useState, useEffect, useRef, useCallback } from 'react'; import { UserCheck, ChevronDown, Loader2, Settings, Check } from 'lucide-react'; import { getSelectedExpert, selectExpert, listExperts } from '../../api/experts'; interface ExpertItem { name: string; description: string; source: string; path?: string; body?: string; disabled_in_scopes: string[]; } interface SelectedExpert { name: string; description: string; } interface ExpertSelectorProps { sessionId: string | null; onManageExperts?: () => void; onSelectionChange?: (expert: SelectedExpert | null) => void; /** 设置弹窗关闭时触发的刷新信号(每次关闭时递增) */ settingsClosedTick?: number; } export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange, settingsClosedTick, }: ExpertSelectorProps) { const [selectedExpert, setSelectedExpert] = useState(null); const [expertList, setExpertList] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [listLoading, setListLoading] = useState(false); const [error, setError] = useState(null); const containerRef = useRef(null); // 刷新当前会话选中的专家(后端会对禁用专家返回 null) const refreshSelection = useCallback(() => { if (!sessionId) { setSelectedExpert(null); onSelectionChange?.(null); return; } setLoading(true); setError(null); getSelectedExpert(sessionId) .then((data) => { if (data?.expert) { setSelectedExpert({ name: data.expert.name, description: data.expert.description }); onSelectionChange?.({ name: data.expert.name, description: data.expert.description }); } else { // 已选专家被禁用/删除时,后端返回 null,前端同步清除 setSelectedExpert(null); onSelectionChange?.(null); } }) .catch(() => { // Silent fail: default to no expert setSelectedExpert(null); onSelectionChange?.(null); }) .finally(() => setLoading(false)); }, [sessionId, onSelectionChange]); // Load current selection whenever sessionId changes useEffect(() => { refreshSelection(); }, [refreshSelection]); // 设置弹窗关闭时刷新选中状态(处理已选专家被禁用/删除的情况) useEffect(() => { if (settingsClosedTick === undefined) return; refreshSelection(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [settingsClosedTick]); // Click outside to close dropdown useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const fetchExpertList = useCallback(async () => { setListLoading(true); const data = await listExperts(); if (data) { // Only show enabled experts (disabled_in_scopes.length === 0) const enabled = (data.experts ?? []).filter( (e: ExpertItem) => e.disabled_in_scopes.length === 0, ); setExpertList(enabled); } setListLoading(false); }, []); const handleToggleOpen = () => { const next = !open; setOpen(next); // 每次打开都重新拉取列表和选中状态,确保设置页面的启用/禁用变更能及时反映 if (next) { fetchExpertList(); refreshSelection(); } }; const handleSelect = async (expert: SelectedExpert | null) => { if (!sessionId) return; // Optimistic update const prev = selectedExpert; setSelectedExpert(expert); onSelectionChange?.(expert); setOpen(false); try { const result = await selectExpert(sessionId, expert?.name ?? null); if (!result.success) { // Revert setSelectedExpert(prev); onSelectionChange?.(prev); setError(result.error || '切换专家失败'); setTimeout(() => setError(null), 3000); } } catch { setSelectedExpert(prev); onSelectionChange?.(prev); setError('网络错误,切换专家失败'); setTimeout(() => setError(null), 3000); } }; const handleManage = () => { setOpen(false); onManageExperts?.(); }; // If sessionId is null, render nothing if (!sessionId) return null; return (
{open && (
{listLoading && expertList.length === 0 ? (
加载中...
) : ( <> {/* 无专家 option */} {expertList.length > 0 ? (
{expertList.map((expert) => { const isSelected = selectedExpert?.name === expert.name; return ( ); })}
) : (
)} )}
)}
{error && {error}}
); }