Initial commit: RA-H Open Source Edition

Local-first knowledge management system with BYO API keys.

Features:
- 3-panel UI (Nodes | Focus | Helpers)
- SQLite + sqlite-vec for vector search
- Agent system (Easy/Hard mode orchestrators)
- Content extraction (YouTube, PDF, web)
- Integrate workflow for connection discovery
- Dimension system with auto-assignment

Tech stack:
- Next.js 15 + TypeScript + Tailwind CSS
- Anthropic (Claude) + OpenAI (GPT) via Vercel AI SDK

Setup:
  npm install && npm rebuild better-sqlite3
  scripts/dev/bootstrap-local.sh
  npm run dev

MIT License
This commit is contained in:
“BeeRad”
2025-12-15 16:14:28 +11:00
commit 733d1c3407
226 changed files with 46231 additions and 0 deletions
+561
View File
@@ -0,0 +1,561 @@
"use client";
import { useState, useCallback, useEffect, useRef } from 'react';
import NodesPanel from '../nodes/NodesPanel';
import FocusPanel from '../focus/FocusPanel';
import AgentsPanel from '../agents/AgentsPanel';
import SettingsCog from '../settings/SettingsCog';
import SettingsModal, { SettingsTab } from '../settings/SettingsModal';
import { Node } from '@/types/database';
import { DatabaseEvent } from '@/services/events';
import { usePersistentState } from '@/hooks/usePersistentState';
import FolderViewOverlay from '../nodes/FolderViewOverlay';
export default function ThreePanelLayout() {
// Panel widths as percentages (20% | 40% | 40%)
const [leftWidth, setLeftWidth] = useState(20);
const [middleWidth, setMiddleWidth] = useState(40);
// Collapsible state for nodes panel
const [nodesCollapsed, setNodesCollapsed] = usePersistentState('ui.nodesCollapsed', false);
// Settings dropdown state
const [showSettings, setShowSettings] = useState(false);
const [settingsInitialTab, setSettingsInitialTab] = useState<SettingsTab>();
const handleCloseSettings = useCallback(() => {
setShowSettings(false);
setSettingsInitialTab(undefined);
}, []);
// Dragging states
const [isDraggingLeft, setIsDraggingLeft] = useState(false);
const [isDraggingRight, setIsDraggingRight] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Track selected nodes and open tabs
const [selectedNodes, setSelectedNodes] = useState<Set<number>>(new Set<number>());
const [openTabs, setOpenTabs] = usePersistentState<number[]>('ui.focus.openTabs', []);
const [activeTab, setActiveTab] = usePersistentState<number | null>('ui.focus.activeTab', null);
const [openTabsData, setOpenTabsData] = useState<Node[]>([]);
// Event handlers for SSE events
const [nodesPanelRefresh, setNodesPanelRefresh] = useState(0);
const [focusPanelRefresh, setFocusPanelRefresh] = useState(0);
const [folderViewOpen, setFolderViewOpen] = useState(false);
const [folderViewRefresh, setFolderViewRefresh] = useState(0);
// Ref to get current openTabs value in SSE handler
const openTabsRef = useRef<number[]>([]);
// Fetch full node data for open tabs
const fetchOpenTabsData = async (tabIds: number[]) => {
if (tabIds.length === 0) {
setOpenTabsData([]);
return;
}
try {
const nodePromises = tabIds.map(async (id) => {
const response = await fetch(`/api/nodes/${id}`);
if (response.ok) {
const data = await response.json();
return data.node as Node;
}
return null;
});
const nodes = await Promise.all(nodePromises);
const validNodes = nodes.filter((node): node is Node => Boolean(node)).map(node => ({
id: node.id,
title: node.title,
link: node.link,
content: node.content,
dimensions: node.dimensions,
created_at: node.created_at,
updated_at: node.updated_at
}));
setOpenTabsData(validNodes);
} catch (error) {
console.error('Failed to fetch tab data:', error);
setOpenTabsData([]);
}
};
// Update tab data whenever openTabs changes
useEffect(() => {
openTabsRef.current = openTabs; // Keep ref updated
fetchOpenTabsData(openTabs);
}, [openTabs]);
// Keyboard shortcut handler
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Check for Cmd+1 (Mac) or Ctrl+1 (Windows/Linux)
if ((e.metaKey || e.ctrlKey) && e.key === '1') {
e.preventDefault();
setNodesCollapsed(prev => !prev);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [setNodesCollapsed]);
useEffect(() => {
const handleSettingsOpen = (event: Event) => {
const detail = (event as CustomEvent<{ tab?: SettingsTab }>).detail;
setSettingsInitialTab(detail?.tab);
setShowSettings(true);
};
window.addEventListener('settings:open', handleSettingsOpen as EventListener);
return () => window.removeEventListener('settings:open', handleSettingsOpen as EventListener);
}, []);
// SSE connection for real-time updates
useEffect(() => {
let eventSource: EventSource | null = null;
try {
eventSource = new EventSource('/api/events');
eventSource.onopen = () => {
console.log('🔌 SSE connected for real-time updates');
};
eventSource.onmessage = (event) => {
try {
const data: DatabaseEvent = JSON.parse(event.data);
switch (data.type) {
case 'NODE_CREATED':
// Trigger NodesPanel refresh
setNodesPanelRefresh(prev => prev + 1);
console.log('📥 Node created via helper:', data.data.node.title);
break;
case 'NODE_UPDATED':
const currentOpenTabs = openTabsRef.current;
const updatedNodeId = Number(data.data.nodeId);
console.log('🔍 NODE_UPDATED - nodeId:', updatedNodeId, 'openTabs:', currentOpenTabs);
// Trigger FocusPanel refresh for open tabs
if (currentOpenTabs.includes(updatedNodeId)) {
console.log('🔄 Triggering FocusPanel refresh for node:', updatedNodeId);
setFocusPanelRefresh(prev => prev + 1);
} else {
console.log('⚠️ Node not in open tabs, skipping FocusPanel refresh');
}
// Also refresh NodesPanel in case title changed
setNodesPanelRefresh(prev => prev + 1);
console.log('✏️ Node updated via helper:', updatedNodeId);
break;
case 'NODE_DELETED':
// Remove from tabs and selection if open
handleNodeDeleted(data.data.nodeId);
// Trigger NodesPanel refresh
setNodesPanelRefresh(prev => prev + 1);
console.log('🗑️ Node deleted via helper:', data.data.nodeId);
break;
case 'EDGE_CREATED':
case 'EDGE_DELETED':
// Trigger FocusPanel refresh for affected nodes
const currentOpenTabsForEdge = openTabsRef.current;
console.log('🔗 Edge changed - fromNodeId:', data.data.fromNodeId, 'toNodeId:', data.data.toNodeId, 'openTabs:', currentOpenTabsForEdge);
if (currentOpenTabsForEdge.includes(data.data.fromNodeId) || currentOpenTabsForEdge.includes(data.data.toNodeId)) {
console.log('🔄 Triggering FocusPanel refresh for edge change');
setFocusPanelRefresh(prev => prev + 1);
} else {
console.log('⚠️ Neither edge node in open tabs, skipping FocusPanel refresh');
}
console.log('🔗 Edge changed via helper');
break;
case 'DIMENSION_UPDATED':
console.log('📁 Dimension updated event received:', data.data?.dimension);
setNodesPanelRefresh(prev => prev + 1);
setFolderViewRefresh(prev => prev + 1);
break;
case 'HELPER_UPDATED':
case 'AGENT_UPDATED':
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('agents:updated', { detail: data.data }));
}
break;
case 'AGENT_DELEGATION_CREATED':
console.log('[ThreePanelLayout] AGENT_DELEGATION_CREATED received:', data.data);
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('delegations:created', { detail: data.data }));
console.log('[ThreePanelLayout] Dispatched delegations:created event');
}
break;
case 'AGENT_DELEGATION_UPDATED':
console.log('[ThreePanelLayout] AGENT_DELEGATION_UPDATED received:', data.data);
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('delegations:updated', { detail: data.data }));
console.log('[ThreePanelLayout] Dispatched delegations:updated event');
}
break;
case 'WORKFLOW_PROGRESS':
console.log('[ThreePanelLayout] WORKFLOW_PROGRESS received:', data.data);
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('workflow:progress', { detail: data.data }));
}
break;
case 'CONNECTION_ESTABLISHED':
console.log('✅ SSE connection established');
break;
default:
console.log('📡 Unknown SSE event:', data.type);
}
} catch (error) {
console.error('Failed to parse SSE event:', error);
}
};
eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
};
} catch (error) {
console.error('Failed to establish SSE connection:', error);
}
// Cleanup on unmount
return () => {
if (eventSource) {
eventSource.close();
console.log('🔌 SSE connection closed');
}
};
}, []); // Empty dependency array - connect once on mount
// Handle panel resizing
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!containerRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect();
const mouseX = ((e.clientX - containerRect.left) / containerRect.width) * 100;
if (isDraggingLeft) {
const newLeftWidth = Math.max(15, Math.min(35, mouseX));
const diff = newLeftWidth - leftWidth;
setLeftWidth(newLeftWidth);
setMiddleWidth(prev => Math.max(20, prev - diff));
} else if (isDraggingRight) {
const rightEdge = leftWidth + middleWidth;
const newMiddleWidth = Math.max(20, Math.min(65, mouseX - leftWidth));
setMiddleWidth(newMiddleWidth);
}
}, [isDraggingLeft, isDraggingRight, leftWidth, middleWidth]);
const handleMouseUp = useCallback(() => {
setIsDraggingLeft(false);
setIsDraggingRight(false);
}, []);
useEffect(() => {
if (isDraggingLeft || isDraggingRight) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}
}, [isDraggingLeft, isDraggingRight, handleMouseMove, handleMouseUp]);
const handleToggleFolderView = (nextState?: boolean) => {
setFolderViewOpen(prev => typeof nextState === 'boolean' ? nextState : !prev);
};
const handleFolderViewDataChanged = useCallback(() => {
setFolderViewRefresh(prev => prev + 1);
setNodesPanelRefresh(prev => prev + 1);
}, []);
const handleNodeOpenFromFolderView = (nodeId: number) => {
handleNodeSelect(nodeId, false);
setFolderViewOpen(false);
};
const handleReorderTabs = (fromIndex: number, toIndex: number) => {
if (fromIndex === toIndex) return;
setOpenTabs(prev => {
if (fromIndex < 0 || toIndex < 0 || fromIndex >= prev.length || toIndex >= prev.length) {
return prev;
}
const updated = [...prev];
const [moved] = updated.splice(fromIndex, 1);
updated.splice(toIndex, 0, moved);
return updated;
});
};
// Handle node selection
const handleNodeSelect = (nodeId: number, multiSelect: boolean) => {
if (folderViewOpen) {
setFolderViewOpen(false);
}
if (multiSelect) {
const newSelection = new Set(selectedNodes);
if (newSelection.has(nodeId)) {
newSelection.delete(nodeId);
} else {
newSelection.add(nodeId);
}
setSelectedNodes(newSelection);
const newTabs = Array.from(newSelection);
setOpenTabs(newTabs);
if (newTabs.length > 0 && !activeTab) {
setActiveTab(newTabs[0]);
}
} else {
setSelectedNodes(new Set<number>([nodeId]));
if (!openTabs.includes(nodeId)) {
setOpenTabs([...openTabs, nodeId]);
}
setActiveTab(nodeId);
}
};
const handleTabSelect = (tabId: number) => {
setSelectedNodes(new Set<number>([tabId]));
setActiveTab(tabId);
};
// Handle tab close
const handleCloseTab = (tabId: number) => {
const newTabs = openTabs.filter(id => id !== tabId);
setOpenTabs(newTabs);
if (activeTab === tabId) {
const currentIndex = openTabs.indexOf(tabId);
if (newTabs.length > 0) {
const newIndex = Math.min(currentIndex, newTabs.length - 1);
setActiveTab(newTabs[newIndex]);
} else {
setActiveTab(null);
}
}
const newSelection = new Set(selectedNodes);
newSelection.delete(tabId);
setSelectedNodes(newSelection);
};
// Handle node creation - auto-open the new node in Focus panel
const handleNodeCreated = (newNode: Node) => {
// Auto-select the new node
setSelectedNodes(new Set<number>([newNode.id]));
// Auto-open the new node as a tab
if (!openTabs.includes(newNode.id)) {
setOpenTabs([...openTabs, newNode.id]);
}
// Set as active tab
setActiveTab(newNode.id);
};
// Handle node deletion - close tab and remove from selection
const handleNodeDeleted = (nodeId: number) => {
// Close tab if open
handleCloseTab(nodeId);
};
const rightWidth = nodesCollapsed ? (100 - middleWidth) : (100 - leftWidth - middleWidth);
const effectiveLeftWidth = nodesCollapsed ? 0 : leftWidth;
const effectiveMiddleWidth = nodesCollapsed ? (leftWidth + middleWidth) : middleWidth;
const activeNodeId = activeTab;
return (
<div
ref={containerRef}
style={{
display: 'flex',
height: '100vh',
width: '100vw',
background: '#0a0a0a',
overflow: 'hidden'
}}
>
{/* Left Panel - Nodes (collapsible) */}
{nodesCollapsed ? (
// Collapsed rail
<div style={{
width: '40px',
flexShrink: 0,
borderRight: '1px solid #2a2a2a',
background: '#0f0f0f',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
paddingTop: '12px'
}}>
<button
onClick={() => setNodesCollapsed(false)}
style={{
padding: '8px',
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: '4px',
color: '#888',
cursor: 'pointer',
fontSize: '12px',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2a2a2a';
e.currentTarget.style.color = '#fff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.color = '#888';
}}
title="Expand Nodes (⌘1)"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
</div>
) : (
<>
<div
style={{
width: `${effectiveLeftWidth}%`,
flexShrink: 0,
borderRight: '1px solid #1a1a1a',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
background: '#0f0f0f',
position: 'relative',
padding: '4px'
}}
>
<NodesPanel
selectedNodes={selectedNodes}
onNodeSelect={handleNodeSelect}
onNodeCreated={handleNodeCreated}
onNodeDeleted={handleNodeDeleted}
refreshTrigger={nodesPanelRefresh}
onToggleFolderView={handleToggleFolderView}
folderViewOpen={folderViewOpen}
/>
<SettingsCog onClick={() => {
setSettingsInitialTab(undefined);
setShowSettings(true);
}} />
</div>
{/* Left Resize Handle */}
<div
onMouseDown={() => setIsDraggingLeft(true)}
style={{
width: '3px',
cursor: 'col-resize',
background: isDraggingLeft ? '#353535' : 'transparent',
transition: isDraggingLeft ? 'none' : 'background 0.2s'
}}
onMouseEnter={(e) => { if (!isDraggingLeft) e.currentTarget.style.background = '#1f1f1f'; }}
onMouseLeave={(e) => { if (!isDraggingLeft) e.currentTarget.style.background = 'transparent'; }}
/>
</>
)}
{/* Middle Panel - Focus */}
<div
style={{
width: `${effectiveMiddleWidth}%`,
flexShrink: 0,
borderRight: '1px solid #1a1a1a',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
background: '#0f0f0f',
padding: '4px'
}}
>
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
<div style={{
height: '100%',
visibility: folderViewOpen ? 'hidden' : 'visible',
pointerEvents: folderViewOpen ? 'none' : 'auto'
}}>
<FocusPanel
openTabs={openTabs}
activeTab={activeTab}
onTabSelect={handleTabSelect}
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
onTabClose={handleCloseTab}
refreshTrigger={focusPanelRefresh}
onReorderTabs={handleReorderTabs}
/>
</div>
{folderViewOpen && (
<FolderViewOverlay
onClose={() => handleToggleFolderView(false)}
onNodeOpen={handleNodeOpenFromFolderView}
refreshToken={folderViewRefresh}
onDataChanged={handleFolderViewDataChanged}
/>
)}
</div>
</div>
{/* Right Resize Handle */}
{!nodesCollapsed && (
<div
onMouseDown={() => setIsDraggingRight(true)}
style={{
width: '3px',
cursor: 'col-resize',
background: isDraggingRight ? '#353535' : 'transparent',
transition: isDraggingRight ? 'none' : 'background 0.2s'
}}
onMouseEnter={(e) => { if (!isDraggingRight) e.currentTarget.style.background = '#1f1f1f'; }}
onMouseLeave={(e) => { if (!isDraggingRight) e.currentTarget.style.background = 'transparent'; }}
/>
)}
{/* Right Panel - Agents */}
<div
style={{
width: `${rightWidth}%`,
flexShrink: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
background: '#0a0a0a',
position: 'relative',
padding: '4px'
}}
>
<AgentsPanel
openTabsData={openTabsData}
activeTabId={activeNodeId}
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
/>
</div>
{/* Settings Modal */}
<SettingsModal
isOpen={showSettings}
onClose={handleCloseSettings}
initialTab={settingsInitialTab}
/>
</div>
);
}