import React, { memo, useState, useEffect, useCallback, useRef } from 'react'; import type { RoomResponse, RoomAiResponse, RoomAiUpsertRequest, ModelWithPricingResponse, ModelListResponse } from '@/client'; import { aiList, aiUpsert, aiDelete } from '@/client'; import { client } from '@/client/client.gen'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Loader2, Plus, Trash2, Bot, ChevronDown, ChevronRight, Search, ArrowLeft, Cpu, Lock, DollarSign, Zap, BookOpen, } from 'lucide-react'; import { toast } from 'sonner'; interface RoomSettingsPanelProps { room: RoomResponse; onUpdate: (name: string, isPublic: boolean) => Promise; onClose: () => void; isPending: boolean; } type CatalogModel = ModelWithPricingResponse; type CatalogResponse = ModelListResponse; function formatContextLength(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`; return String(n); } function formatPrice(price?: string | null): string { if (!price) return 'N/A'; const n = parseFloat(price); if (isNaN(n)) return price; if (n === 0) return 'Free'; return `$${n.toFixed(2)}`; } const CAPABILITY_COLORS: Record = { chat: '#3b82f6', code: '#8b5cf6', completion: '#06b6d4', embedding: '#f59e0b', vision: '#ec4899', reasoning: '#10b981', }; const MODALITY_ICONS: Record = { text: 'T', multimodal: 'M', image: 'I', audio: 'A', }; export const RoomSettingsPanel = memo(function RoomSettingsPanel({ room, onUpdate, onClose, isPending, }: RoomSettingsPanelProps) { const [name, setName] = useState(room.room_name ?? ''); const [isPublic, setIsPublic] = useState(!!room.public); useEffect(() => { setName(room.room_name ?? ''); setIsPublic(!!room.public); }, [room.id, room.room_name, room.public]); // AI section state const [aiConfigs, setAiConfigs] = useState([]); const [aiConfigsLoading, setAiConfigsLoading] = useState(false); const [showAddPanel, setShowAddPanel] = useState(false); // Catalog state const [catalogModels, setCatalogModels] = useState([]); const [catalogLoading, setCatalogLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [catalogPage, setCatalogPage] = useState(1); const [catalogTotal, setCatalogTotal] = useState(0); const [catalogPerPage] = useState(20); const searchTimerRef = useRef>(undefined); // Selected model + config form state const [selectedModel, setSelectedModel] = useState(null); const [temperature, setTemperature] = useState(''); const [maxTokens, setMaxTokens] = useState(''); const [historyLimit, setHistoryLimit] = useState(''); const [systemPrompt, setSystemPrompt] = useState(''); const [useExact, setUseExact] = useState(false); const [think, setThink] = useState(false); const [stream, setStream] = useState(true); const [agentType, setAgentType] = useState('chat'); const [showAdvanced, setShowAdvanced] = useState(false); const [isAddingAi, setIsAddingAi] = useState(false); const handleSave = async () => { if (!name.trim()) return; try { await onUpdate(name.trim(), isPublic); toast.success('Room updated'); onClose(); } catch { toast.error('Failed to update room'); } }; // Load AI configs const loadAiConfigs = useCallback(async () => { setAiConfigsLoading(true); try { const resp = await aiList({ path: { room_id: room.id } }); const inner = resp.data as { data?: RoomAiResponse[] } | undefined; setAiConfigs(Array.isArray(inner?.data) ? inner.data : []); } catch { // ignore } finally { setAiConfigsLoading(false); } }, [room.id]); useEffect(() => { loadAiConfigs(); }, [loadAiConfigs]); // Load catalog models const loadCatalog = useCallback(async (search: string, page: number) => { setCatalogLoading(true); try { const params = new URLSearchParams(); params.set('page', String(page)); params.set('per_page', String(catalogPerPage)); if (search) params.set('search', search); const resp = await client.get({ url: `/api/agents/models/catalog?${params.toString()}`, }); const data = (resp.data as { data?: CatalogResponse })?.data; if (data) { setCatalogModels(data.data ?? []); setCatalogTotal(data.total ?? 0); } } catch { toast.error('Failed to load models'); } finally { setCatalogLoading(false); } }, [catalogPerPage]); // Debounced search useEffect(() => { if (!showAddPanel) return; if (searchTimerRef.current) clearTimeout(searchTimerRef.current); searchTimerRef.current = setTimeout(() => { setCatalogPage(1); loadCatalog(searchQuery, 1); }, 300); return () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); }; }, [searchQuery, showAddPanel, loadCatalog]); useEffect(() => { if (showAddPanel) { loadCatalog(searchQuery, catalogPage); } }, [catalogPage, showAddPanel]); // eslint-disable-line react-hooks/exhaustive-deps const openAddPanel = () => { setSelectedModel(null); setSearchQuery(''); setCatalogPage(1); setTemperature(''); setMaxTokens(''); setHistoryLimit(''); setSystemPrompt(''); setUseExact(false); setThink(false); setStream(true); setAgentType('chat'); setShowAdvanced(false); setShowAddPanel(true); }; const handleAddAi = async () => { if (!selectedModel) return; setIsAddingAi(true); try { const body: RoomAiUpsertRequest = { model: selectedModel.id, temperature: temperature ? parseFloat(temperature) : undefined, max_tokens: maxTokens ? parseInt(maxTokens) : undefined, history_limit: historyLimit ? parseInt(historyLimit) : undefined, system_prompt: systemPrompt || undefined, use_exact: useExact, think, stream, agent_type: agentType || undefined, }; await aiUpsert({ path: { room_id: room.id }, body }); toast.success(`${selectedModel.name} added to room`); setShowAddPanel(false); setSelectedModel(null); loadAiConfigs(); } catch { toast.error('Failed to add AI model'); } finally { setIsAddingAi(false); } }; const handleDeleteAi = async (modelId: string) => { try { await aiDelete({ path: { room_id: room.id, model_id: modelId } }); toast.success('AI model removed'); setAiConfigs((prev) => prev.filter((c) => c.model !== modelId)); } catch { toast.error('Failed to remove AI model'); } }; const totalPages = Math.max(1, Math.ceil(catalogTotal / catalogPerPage)); // ── Add Panel View ── if (showAddPanel) { return (