"use client"; import { useState, useEffect, useRef } from 'react'; import FocusPanel from '@/components/focus/FocusPanel'; import PaneHeader from './PaneHeader'; import { NodePaneProps, PaneType } from './types'; // Simple truncate for tab titles function truncateTitle(title: string, maxLength = 20): string { if (title.length <= maxLength) return title; return title.slice(0, maxLength - 1) + '…'; } export default function NodePane({ slot, isActive, onPaneAction, onCollapse, onSwapPanes, tabBar, openTabs, activeTab, onTabSelect, onTabClose, onNodeClick, onReorderTabs, refreshTrigger, onOpenInOtherSlot, onTextSelect, highlightedPassage, }: NodePaneProps) { const [nodeTitles, setNodeTitles] = useState>({}); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; tabId: number } | null>(null); const fetchedRef = useRef>(new Set()); const handleTypeChange = (type: PaneType) => { onPaneAction?.({ type: 'switch-pane-type', paneType: type }); }; // Fetch node titles for tabs useEffect(() => { const fetchTitle = async (tabId: number) => { if (fetchedRef.current.has(tabId)) return; fetchedRef.current.add(tabId); try { const response = await fetch(`/api/nodes/${tabId}`); if (response.ok) { const data = await response.json(); if (data.success && data.node) { setNodeTitles(prev => ({ ...prev, [tabId]: data.node.title || 'Untitled' })); } } } catch (error) { console.error('Failed to fetch node title:', error); fetchedRef.current.delete(tabId); // Allow retry on error } }; openTabs.forEach(fetchTitle); }, [openTabs]); // Clear fetched ref when tabs are closed useEffect(() => { const currentTabs = new Set(openTabs); fetchedRef.current.forEach(id => { if (!currentTabs.has(id)) { fetchedRef.current.delete(id); } }); }, [openTabs]); // Close context menu on outside click or escape useEffect(() => { if (!contextMenu) return; const handleClick = () => setContextMenu(null); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setContextMenu(null); }; document.addEventListener('click', handleClick); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('click', handleClick); document.removeEventListener('keydown', handleKeyDown); }; }, [contextMenu]); return (
No tabs open ) : ( openTabs.map((tabId) => { const title = nodeTitles[tabId] || 'Loading...'; const isActiveTab = activeTab === tabId; return (
{ e.dataTransfer.effectAllowed = 'copyMove'; e.dataTransfer.setData('application/x-rah-tab', JSON.stringify({ id: tabId, title, sourceSlot: slot })); e.dataTransfer.setData('text/plain', `[NODE:${tabId}:"${title}"]`); }} onContextMenu={(e) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, tabId }); }} style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 8px', background: isActiveTab ? '#1f1f1f' : 'transparent', borderRadius: '4px', cursor: 'grab', flexShrink: 0, }} >
); }) ) )} />
onPaneAction({ type: 'switch-pane-type', paneType: 'map' }) : undefined} onTabClose={onTabClose} refreshTrigger={refreshTrigger} onTextSelect={onTextSelect} highlightedPassage={highlightedPassage} />
{/* Context menu for tabs */} {contextMenu && (
e.stopPropagation()} > {onOpenInOtherSlot && ( )}
)}
); }