'use client'; import { useCallback, useRef, useState } from 'react'; import { History, PanelRightClose, PanelRightOpen, SquarePen, Trash2, UsersRound, } from 'lucide-react'; import type { AssistantRuntime } from '@assistant-ui/react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; import type { AgentEditSessionRecord } from '@/lib/agent/client/agent-edit-session-types'; import { useI18n } from '@/lib/hooks/use-i18n'; import { AgentPanel } from '@/components/edit/AgentPanel/AgentPanel'; import { AgentRosterPanel } from '@/components/edit/AgentsView/AgentRosterPanel'; import { shouldRenderAgentPanel } from '@/components/edit/agent-panel-visibility'; const MIN_WIDTH = 320; const MAX_WIDTH = 640; const DEFAULT_WIDTH = 384; type RailTab = 'ai' | 'agents'; export interface RightRailTabsProps { readonly scene?: { id: string; title: string; type?: string }; readonly runtime: AssistantRuntime; readonly clearThread: () => void; readonly hasMessages: boolean; readonly canSend: boolean; readonly agentEnabled: boolean; readonly isRunning: boolean; readonly sessions: AgentEditSessionRecord[]; readonly activeSessionId: string | undefined; readonly switchSession: (id: string) => Promise; readonly deleteSessionAndRefresh: (id: string) => Promise; readonly refreshSessions: () => Promise; } /** * Tabbed right rail: "Edit with AI" | "课堂阵容" * * Owns the aside wrapper, resize handle, collapse state, and tab state. * The AgentPanel is rendered in naked (no-wrapper) mode for the AI tab; * the 课堂阵容 tab renders AgentRosterPanel. Both are kept mounted so state * is preserved when switching tabs (hidden via CSS). * * The "Edit with AI" tab is gated by shouldRenderAgentPanel — when the current * scene type does not support AI editing, the tab is hidden and the active tab * falls back to 课堂阵容 (which is always available, as agents are stage-level). */ export function RightRailTabs({ scene, runtime, clearThread, hasMessages, canSend, agentEnabled, isRunning, sessions, activeSessionId, switchSession, deleteSessionAndRefresh, refreshSessions, }: RightRailTabsProps) { const { t } = useI18n(); const showAiTab = shouldRenderAgentPanel({ agentEnabled, hasMessages, isRunning }); const [activeTab, setActiveTab] = useState(() => (showAiTab ? 'ai' : 'agents')); // When the AI tab becomes unavailable (e.g. PBL scene), fall back to agents tab. // Render-time setState: React re-renders immediately before painting. if (!showAiTab && activeTab === 'ai') { setActiveTab('agents'); } const [collapsed, setCollapsed] = useState(false); const railRef = useRef(null); const [width, setWidth] = useState(DEFAULT_WIDTH); const dragRef = useRef<{ startX: number; startW: number; lastW: number; pointerId: number; } | null>(null); const onResizeStart = useCallback( (e: React.PointerEvent) => { const startW = railRef.current?.getBoundingClientRect().width ?? width; dragRef.current = { startX: e.clientX, startW, lastW: startW, pointerId: e.pointerId }; try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* best effort */ } document.body.style.cursor = 'col-resize'; }, [width], ); const onResizeMove = useCallback((e: React.PointerEvent) => { const d = dragRef.current; if (!d || e.pointerId !== d.pointerId) return; const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, d.startW + (d.startX - e.clientX))); d.lastW = next; if (railRef.current) railRef.current.style.width = `${next}px`; }, []); const onResizeEnd = useCallback((e: React.PointerEvent) => { const d = dragRef.current; if (!d || e.pointerId !== d.pointerId) return; try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { /* may already be released */ } setWidth(d.lastW); dragRef.current = null; document.body.style.cursor = ''; }, []); if (collapsed) { return ( ); } const agentPanelProps = { scene, runtime, clearThread, hasMessages, canSend, sessions, activeSessionId, switchSession, deleteSessionAndRefresh, refreshSessions, }; return ( ); } function RailTabButton({ label, icon, active, onClick, }: { readonly label: string; readonly icon?: React.ReactNode; readonly active: boolean; readonly onClick: () => void; }) { return ( ); }