/** * ParticipantSelector Component * ADR-024: Chat & Message Architecture * * Select chat participants: Users AND AI Agents * Replaces the old "Agent Selector" approach */ import { useState, useMemo, useRef, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/shared/utils/apiClient'; import { cn } from '@/shared/utils/cn'; import { Search, X, User, Bot, Users, ChevronDown, Check, Loader2 } from 'lucide-react'; interface UserInfo { id: number; name: string; email?: string; avatar_url?: string; status?: 'online' | 'offline' | 'away'; } interface AgentInfo { id: number; name: string; avatar?: string; description?: string; type?: string; } export type ParticipantType = 'user' | 'agent'; export interface Participant { type: ParticipantType; id: number; name: string; avatar?: string; email?: string; avatarUrl?: string; status?: string; description?: string; } export interface ParticipantSelectorProps { value?: Participant | null; participants?: Participant[]; selectedParticipants?: Participant[]; onSelect?: (participant: Participant) => void; onMultiSelect?: (participants: Participant[]) => void; onParticipantsChange?: (participants: Participant[]) => void; multiSelect?: boolean; showAgents?: boolean; showUsers?: boolean; maxParticipants?: number; placeholder?: string; showStatus?: boolean; filterType?: ParticipantType | 'all'; excludeIds?: { users?: number[]; agents?: number[] }; className?: string; } export function ParticipantSelector({ value, participants = [], selectedParticipants = [], onSelect, onMultiSelect, onParticipantsChange, multiSelect = false, showAgents = true, showUsers = true, maxParticipants, placeholder = 'Выберите участника...', showStatus = true, filterType = 'all', excludeIds = {}, className }: ParticipantSelectorProps) { const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState<'all' | 'users' | 'agents'>( filterType === 'all' ? 'all' : filterType === 'user' ? 'users' : 'agents' ); const dropdownRef = useRef(null); const inputRef = useRef(null); // Fetch users const { data: users = [], isLoading: isLoadingUsers } = useQuery({ queryKey: ['users-for-chat'], queryFn: async () => { const response = await apiClient.get<{ success: boolean; data: UserInfo[] }>('/users'); return response.success ? response.data : []; }, enabled: isOpen && (filterType === 'all' || filterType === 'user') }); // Fetch agents const { data: agents = [], isLoading: isLoadingAgents } = useQuery({ queryKey: ['ai-agents-for-chat'], queryFn: async () => { const response = await apiClient.get<{ success: boolean; data: AgentInfo[] }>('/ai-agents'); return response.success ? response.data : []; }, enabled: isOpen && (filterType === 'all' || filterType === 'agent') }); // Convert to unified Participant format const allParticipants = useMemo(() => { const result: Participant[] = []; // Add users if (filterType === 'all' || filterType === 'user') { users.forEach(user => { if (!excludeIds.users?.includes(user.id)) { result.push({ type: 'user', id: user.id, name: user.name, avatar: user.avatar_url, status: user.status || 'offline' }); } }); } // Add agents if (filterType === 'all' || filterType === 'agent') { agents.forEach(agent => { if (!excludeIds.agents?.includes(agent.id)) { result.push({ type: 'agent', id: agent.id, name: agent.name, avatar: agent.avatar, description: agent.description }); } }); } return result; }, [users, agents, filterType, excludeIds]); // Filter by search query and tab const filteredParticipants = useMemo(() => { return allParticipants.filter(p => { // Filter by tab if (activeTab === 'users' && p.type !== 'user') return false; if (activeTab === 'agents' && p.type !== 'agent') return false; // Filter by search if (searchQuery) { const query = searchQuery.toLowerCase(); return p.name.toLowerCase().includes(query) || (p.description?.toLowerCase().includes(query)); } return true; }); }, [allParticipants, activeTab, searchQuery]); // Check if participant is selected (for multi-select) const isSelected = (p: Participant) => { return participants.some(sp => sp.type === p.type && sp.id === p.id); }; // Handle selection const handleSelect = (p: Participant) => { if (multiSelect && onMultiSelect) { const isAlreadySelected = isSelected(p); if (isAlreadySelected) { onMultiSelect(participants.filter(sp => !(sp.type === p.type && sp.id === p.id))); } else { onMultiSelect([...participants, p]); } } else if (onSelect) { onSelect(p); setIsOpen(false); setSearchQuery(''); } }; // Close on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setIsOpen(false); setSearchQuery(''); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isOpen]); // Focus input when opened useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); } }, [isOpen]); const isLoading = isLoadingUsers || isLoadingAgents; const renderParticipantIcon = (p: Participant) => { if (p.type === 'agent') { return ; } return ; }; const renderStatusDot = (status?: string) => { if (!showStatus || !status) return null; const colors: Record = { online: 'bg-green-500', away: 'bg-yellow-500', offline: 'bg-gray-400' }; return ( ); }; return (
{/* Trigger Button */} {/* Dropdown */} {isOpen && (
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Поиск..." className="w-full pl-8 pr-3 py-1.5 text-sm rounded border border-[var(--border-secondary)] bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary-500)]" /> {searchQuery && ( )}
{/* Tabs */} {filterType === 'all' && (
{[ { key: 'all', label: 'Все' }, { key: 'users', label: 'Пользователи' }, { key: 'agents', label: 'AI Агенты' } ].map(tab => ( ))}
)} {/* List */}
{isLoading ? (
) : filteredParticipants.length === 0 ? (
{searchQuery ? 'Не найдено' : 'Нет участников'}
) : ( filteredParticipants.map(p => { const selected = multiSelect ? isSelected(p) : value?.type === p.type && value?.id === p.id; return ( ); }) )}
)}
); }