"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Node } from '@/types/database'; import AsciiBanner from './AsciiBanner'; import TerminalMessage from './TerminalMessage'; import TerminalInput from './TerminalInput'; import { Zap, Flame } from 'lucide-react'; import { useSSEChat, ChatMessage, MessageRole } from './hooks/useSSEChat'; import { useQuotaHandler } from '@/hooks/useQuotaHandler'; // Stub type for delegation (delegation system removed in rah-light) type AgentDelegation = { id: number; sessionId: string; task: string; status: 'queued' | 'in_progress' | 'completed' | 'failed'; summary?: string | null; agentType: string; createdAt: string; updatedAt: string; }; // Stub DelegationIndicator component (delegation system removed) function DelegationIndicator({ delegations }: { delegations: AgentDelegation[] }) { return null; } const createSessionId = () => `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; interface HighlightedPassage { selectedText: string; nodeId: number; nodeTitle: string; } interface RAHChatProps { openTabsData: Node[]; activeTabId: number | null; activeDimension?: string | null; onClearDimension?: () => void; onNodeClick?: (nodeId: number) => void; delegations?: AgentDelegation[]; messages?: ChatMessage[]; setMessages?: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void; mode?: 'easy' | 'hard'; delegationMode?: boolean; delegationSessionId?: string; onQuickAdd?: () => void; highlightedPassage?: HighlightedPassage | null; onClearPassage?: () => void; } export default function RAHChat({ openTabsData, activeTabId, activeDimension, onClearDimension: _onClearDimension, onNodeClick, delegations = [], messages: externalMessages, setMessages: externalSetMessages, mode = 'easy', delegationMode = false, delegationSessionId, onQuickAdd: _onQuickAdd, highlightedPassage: _highlightedPassage, onClearPassage: _onClearPassage, }: RAHChatProps) { // Use external state if provided (lifted state), otherwise use local state const [internalMessages, internalSetMessages] = useState([]); const messages = externalMessages !== undefined ? externalMessages : internalMessages; const setMessages = externalSetMessages || internalSetMessages; const [sessionId, setSessionId] = useState(() => createSessionId()); const messagesEndRef = useRef(null); const chatMode = mode === 'hard' ? 'hard' : 'easy'; const helperKey = chatMode === 'hard' ? 'ra-h' : 'ra-h-easy'; const helperDisplayName = helperKey === 'ra-h' ? 'ra-h (hard)' : 'ra-h (easy)'; const { quotaError, handleAPIError: handleQuotaApiError, checkQuotaBeforeRequest, refetchUsage, isQuotaExceeded, } = useQuotaHandler(); const streamCompleteHandlerRef = useRef<() => Promise | void>(async () => { await refetchUsage(); }); const setMessagesRef = useRef(setMessages); const sse = useSSEChat('/api/rah/chat', setMessages, { getAuthToken: () => null, beforeRequest: checkQuotaBeforeRequest, onRequestError: handleQuotaApiError, onStreamComplete: async () => { const handler = streamCompleteHandlerRef.current; if (handler) { await handler(); } }, }); const sendMessage = useCallback(async (text: string) => { if (delegationMode) return; // Delegation chats are read-only if (isQuotaExceeded) { checkQuotaBeforeRequest(); return; } await sse.send({ text, history: messages, openTabs: openTabsData, activeTabId, sessionId, mode: chatMode }); }, [activeTabId, chatMode, checkQuotaBeforeRequest, delegationMode, isQuotaExceeded, messages, openTabsData, sse, sessionId]); const handleStreamComplete = useCallback(async () => { await refetchUsage(); }, [refetchUsage]); useEffect(() => { streamCompleteHandlerRef.current = handleStreamComplete; }, [handleStreamComplete]); useEffect(() => { setMessagesRef.current = setMessages; }, [setMessages]); const focusSummary = useMemo(() => { if (!openTabsData.length) return null; const titles = openTabsData.map((node) => node?.title || 'Untitled'); const activeNode = openTabsData.find((node) => node.id === activeTabId) || openTabsData[0]; const truncate = (value: string, limit = 64) => { if (value.length <= limit) return value; return `${value.slice(0, limit - 1)}…`; }; return { id: activeNode?.id ?? null, title: truncate(activeNode?.title || 'Untitled'), total: titles.length, }; }, [openTabsData, activeTabId]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const handleNewChat = () => { if (delegationMode) return; sse.abort(); setMessages((_prev) => []); setSessionId(createSessionId()); }; // Subscribe to delegation stream if in delegation mode useEffect(() => { if (!delegationMode || !delegationSessionId) return; const eventSource = new EventSource(`/api/rah/delegations/stream?sessionId=${delegationSessionId}`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'text-delta' && data.delta) { setMessagesRef.current((prev) => { const lastMsg = prev[prev.length - 1]; if (lastMsg && lastMsg.role === MessageRole.ASSISTANT) { const updated = [...prev]; updated[updated.length - 1] = { ...lastMsg, content: lastMsg.content + data.delta }; return updated; } return [...prev, { id: crypto.randomUUID(), role: MessageRole.ASSISTANT, content: data.delta, timestamp: new Date() }]; }); } if (data.type === 'tool-input-start' || data.type === 'tool-call') { const toolMessage: ChatMessage = { id: `tool-${data.toolCallId || crypto.randomUUID()}`, role: MessageRole.TOOL, content: data.toolName, timestamp: new Date(), toolName: data.toolName, status: (data.status as ChatMessage['status']) || 'running', toolArgs: data.input ?? data.args ?? data.parameters }; setMessagesRef.current((prev) => [...prev, toolMessage]); } if (data.type === 'tool-output-available' || data.type === 'tool-result') { setMessagesRef.current((prev) => prev.map((m) => m.id === `tool-${data.toolCallId}` ? { ...m, content: `${m.toolName} ${data.status === 'error' ? '✗' : '✓'}`, status: (data.status as ChatMessage['status']) || (data.error ? 'error' : 'complete'), toolResult: data.result ?? data.output ?? (data.summary ? { summary: data.summary } : undefined), } : m ) ); } if (data.type === 'assistant-message') { setMessagesRef.current((prev) => [...prev, { id: crypto.randomUUID(), role: MessageRole.ASSISTANT, content: '', timestamp: new Date() }]); } } catch (error) { console.error('[RAHChat] Failed to parse delegation stream event:', error); } }; eventSource.onerror = () => { console.error('[RAHChat] Delegation stream connection error'); }; return () => { eventSource.close(); }; }, [delegationMode, delegationSessionId]); return (
{focusSummary && (
{/* Focused node info */}
Focused Node ({focusSummary.total}) #{focusSummary.id} {focusSummary.title}
)}
{messages.length === 0 ? ( ) : ( <> {messages.map((message) => ( ))} )}
{!delegationMode && (
{(quotaError || isQuotaExceeded) && (
{quotaError?.message ?? 'Rate limit reached. Please wait a moment and try again.'}
)}
)}
); } interface ModelSelectorProps { chatMode: 'easy' | 'hard'; } function ModelSelector({ chatMode }: ModelSelectorProps) { const [dropdownOpen, setDropdownOpen] = useState(false); const currentModel = chatMode === 'easy' ? 'Easy (GPT)' : 'Hard (Claude)'; const Icon = chatMode === 'easy' ? Zap : Flame; const activeColor = chatMode === 'easy' ? '#22c55e' : '#f97316'; const options = [ { id: 'easy', label: 'Easy (GPT)', icon: Zap, color: '#22c55e' }, { id: 'hard', label: 'Hard (Claude)', icon: Flame, color: '#f97316' }, { id: 'soon', label: 'Ra-h (Soon)', icon: null, color: '#666', disabled: true } ]; return (
{dropdownOpen && (
{options.map((option) => { const OptionIcon = option.icon; const isActive = (option.id === 'easy' && chatMode === 'easy') || (option.id === 'hard' && chatMode === 'hard'); return ( ); })}
)}
); }