import React, { useState, useRef, useEffect, useCallback } from "react";
import ChatArea from "../components/ChatArea";
import SessionSidebar from "../components/SessionSidebar";
import MultiNodeConsole from "../components/MultiNodeConsole";
import useCodeAssistant from "../hooks/useCodeAssistant";
import {
updateSession, getSessionNodeStatus, attachNodesToSession,
detachNodeFromSession, getUserAccessibleNodes, getUserNodePreferences, nodeFsList
} from "../services/apiService";
import FileSystemNavigator from "../components/FileSystemNavigator";
const CodeAssistantPage = () => {
const pageContainerRef = useRef(null);
const onNewSessionCreated = useCallback(async (newSid) => {
try {
const nodes = await getUserAccessibleNodes();
const ids = nodes.map(n => n.node_id);
if (ids.length > 0) {
// Authoritative attach: overwrite session nodes with all available nodes
await attachNodesToSession(newSid, ids, { source: 'server' });
// Immediate local sync
setAttachedNodeIds(ids);
// Refresh full status immediately
const status = await getSessionNodeStatus(newSid);
const apiIds = (status.nodes || []).map(n => n.node_id);
setAttachedNodeIds(apiIds);
}
} catch (e) {
console.warn("M3: Failed auto attaching defaults on new session", e);
}
}, []);
const {
chatHistory,
isProcessing,
errorMessage,
showErrorModal,
tokenUsage,
handleSendChat,
setShowErrorModal,
handleSwitchSession,
sessionId,
userConfigData,
localActiveLLM,
setLocalActiveLLM,
isConfigured,
missingConfigs
} = useCodeAssistant({ pageContainerRef, onNewSessionCreated });
const [showConfigModal, setShowConfigModal] = useState(false);
const [showNodeSelector, setShowNodeSelector] = useState(false);
const isEditingMeshRef = useRef(false);
useEffect(() => {
isEditingMeshRef.current = showNodeSelector;
}, [showNodeSelector]);
const [sidebarRefreshTick, setSidebarRefreshTick] = useState(0);
// M3/M6 Node Integration State
const [sessionNodeStatus, setSessionNodeStatus] = useState({}); // node_id -> { status, last_sync }
const [accessibleNodes, setAccessibleNodes] = useState([]);
const [attachedNodeIds, setAttachedNodeIds] = useState([]);
const [workspaceId, setWorkspaceId] = useState("");
const [showConsole, setShowConsole] = useState(false);
const [syncConfig, setSyncConfig] = useState({ source: 'server', path: '', source_node_id: '', read_only_node_ids: [] });
const [activeSyncConfig, setActiveSyncConfig] = useState(null);
const [pathSuggestions, setPathSuggestions] = useState([]);
const [isSearchingPath, setIsSearchingPath] = useState(false);
const [showPathSuggestions, setShowPathSuggestions] = useState(false);
const [hasLoadedDefaults, setHasLoadedDefaults] = useState(false);
const [isInitiatingSync, setIsInitiatingSync] = useState(false);
const [showFileExplorer, setShowFileExplorer] = useState(true);
// M6: Persistence - if we have an active config, populate the form with it when modal opens
useEffect(() => {
if (showNodeSelector && activeSyncConfig) {
setSyncConfig({
source: activeSyncConfig.source || 'server',
path: activeSyncConfig.path || '',
source_node_id: activeSyncConfig.source_node_id || '',
read_only_node_ids: activeSyncConfig.read_only_node_ids || []
});
}
}, [showNodeSelector, activeSyncConfig]);
// Auto-enforce Receiver Only logic in UI
useEffect(() => {
if (syncConfig.source === 'server') {
setSyncConfig(prev => ({ ...prev, read_only_node_ids: [...attachedNodeIds] }));
} else if (syncConfig.source === 'node_local' && syncConfig.source_node_id) {
const others = attachedNodeIds.filter(id => id !== syncConfig.source_node_id);
setSyncConfig(prev => ({ ...prev, read_only_node_ids: others }));
}
}, [syncConfig.source, syncConfig.source_node_id, attachedNodeIds]);
const handleInitiateSync = async () => {
if (!sessionId) return;
setIsInitiatingSync(true);
try {
await attachNodesToSession(sessionId, attachedNodeIds, syncConfig);
// M3: Explicitly turn off the editing mesh ref and modal BEFORE fetching
// so the server's newly saved attached nodes replace the user checkboxes immediately
isEditingMeshRef.current = false;
setShowNodeSelector(false);
await fetchNodeInfo();
} catch (err) {
alert(`Sync Error: ${err.message}`);
} finally {
setIsInitiatingSync(false);
}
};
const fetchNodeInfo = async () => {
if (!sessionId) return;
try {
const [status, nodes] = await Promise.all([
getSessionNodeStatus(sessionId),
getUserAccessibleNodes()
]);
const apiNodes = status.nodes || [];
const apiIds = apiNodes.map(n => n.node_id);
// Sanitized ID List: only show IDs that are actually in the live accessible list
const liveIds = nodes.map(n => n.node_id);
const sanitizedIds = apiIds.filter(id => liveIds.includes(id));
const syncStatusMap = {};
apiNodes.forEach(n => {
syncStatusMap[n.node_id] = { status: n.status, last_sync: n.last_sync };
});
setSessionNodeStatus(syncStatusMap);
setWorkspaceId(status.sync_workspace_id || "");
setAccessibleNodes(nodes);
// Stop auto-poll from wiping out user's active checkbox edits
if (!isEditingMeshRef.current) {
setAttachedNodeIds(sanitizedIds);
setActiveSyncConfig(status.sync_config || null);
}
} catch (e) {
console.warn("M3: Failed to fetch session node info", e);
}
};
useEffect(() => {
fetchNodeInfo();
const interval = setInterval(fetchNodeInfo, 5000); // Polling status
return () => clearInterval(interval);
}, [sessionId]);
// M3: Path Autocomplete Logic
useEffect(() => {
const fetchSuggestions = async () => {
if (syncConfig.source !== 'node_local' || !syncConfig.source_node_id || !syncConfig.path) {
setPathSuggestions([]);
return;
}
const path = syncConfig.path;
// Determine the directory to list and the fragment being typed
let dirToList = ".";
let fragment = "";
if (path.includes('/')) {
const lastSlashIndex = path.lastIndexOf('/');
dirToList = path.substring(0, lastSlashIndex) || "/";
fragment = path.substring(lastSlashIndex + 1);
} else {
fragment = path;
}
setIsSearchingPath(true);
try {
const results = await nodeFsList(syncConfig.source_node_id, dirToList);
const fileList = results.files || [];
// Filter: must be directory AND must start with the current fragment
const dirs = fileList
.filter(item => item.is_dir && item.name.toLowerCase().startsWith(fragment.toLowerCase()))
.map(item => item.name);
setPathSuggestions(dirs);
} catch (e) {
setPathSuggestions([]);
} finally {
setIsSearchingPath(false);
}
};
const timer = setTimeout(fetchSuggestions, 500);
return () => clearTimeout(timer);
}, [syncConfig.path, syncConfig.source_node_id, syncConfig.source]);
useEffect(() => {
const loadDefaults = async () => {
try {
const prefs = await getUserNodePreferences();
if (prefs && prefs.data_source && !hasLoadedDefaults) {
setSyncConfig(prefs.data_source);
setHasLoadedDefaults(true);
}
} catch (e) {
console.warn("M3: Failed to load node defaults", e);
}
};
loadDefaults();
}, [hasLoadedDefaults]);
const handleToggleNode = (nodeId, isCurrentlyAttached) => {
if (isCurrentlyAttached) {
setAttachedNodeIds(prev => prev.filter(id => id !== nodeId));
// Also remove from read-only if it's there
setSyncConfig(prev => ({
...prev,
read_only_node_ids: (prev.read_only_node_ids || []).filter(id => id !== nodeId)
}));
} else {
setAttachedNodeIds(prev => [...prev, nodeId]);
}
};
const handleToggleReadOnly = (nodeId) => {
setSyncConfig(prev => {
const current = prev.read_only_node_ids || [];
if (current.includes(nodeId)) {
return { ...prev, read_only_node_ids: current.filter(id => id !== nodeId) };
} else {
return { ...prev, read_only_node_ids: [...current, nodeId] };
}
});
};
const handleSaveQuickConfig = async () => {
try {
if (sessionId && localActiveLLM) {
await updateSession(sessionId, { provider_name: localActiveLLM });
setSidebarRefreshTick(t => t + 1);
}
setShowConfigModal(false);
} catch (e) {
console.error("Failed to update session configs:", e);
}
};
useEffect(() => {
if (pageContainerRef.current) {
pageContainerRef.current.scrollTop = pageContainerRef.current.scrollHeight;
}
}, [chatHistory]);
// Antigravity: Auto-show console when processing
useEffect(() => {
if (isProcessing && attachedNodeIds.length > 0) {
setShowConsole(true);
}
}, [isProcessing, attachedNodeIds]);
// Antigravity: Auto-show explorer when sync starts
useEffect(() => {
if (workspaceId && attachedNodeIds.length > 0) {
setShowFileExplorer(true);
}
}, [workspaceId, attachedNodeIds.length]);
return (
<div className="flex flex-row flex-grow h-full overflow-hidden bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 relative">
<SessionSidebar
featureName="coding_assistant"
currentSessionId={sessionId}
onSwitchSession={handleSwitchSession}
onNewSession={() => handleSendChat("/new")}
refreshTick={sidebarRefreshTick}
/>
{/* Main content area */}
<div className="flex-grow flex flex-col min-h-0 bg-transparent" ref={pageContainerRef}>
<div className="max-w-4xl mx-auto w-full h-full pt-12 flex flex-col min-h-0">
{/* Chat Area & Header */}
<div className="flex flex-col h-full min-h-0">
<div className="flex-grow p-4 bg-white dark:bg-gray-800 rounded-xl rounded-b-none shadow-lg border border-gray-200 dark:border-gray-700 flex flex-col min-h-0 transition-all duration-300">
<h2 className="text-xl font-bold mb-4 flex justify-between items-center border-b border-gray-100 dark:border-gray-700 pb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
<svg className="w-6 h-6 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</div>
<div className="flex flex-col">
<span className="text-lg">Coding Assistant</span>
<span className="text-[10px] text-gray-500 font-medium uppercase tracking-widest whitespace-nowrap">
Mesh: {accessibleNodes.filter(n => n.last_status === 'online' || n.last_status === 'idle').length} Online / {accessibleNodes.length} Total
</span>
</div>
{/* Nodes Indicator Bar (M3/M6) */}
<div className="flex items-center space-x-2 ml-4 border-l pl-4 dark:border-gray-700 h-10 overflow-visible">
{attachedNodeIds.length === 0 ? (
<span
className="text-[10px] text-gray-400 italic cursor-pointer hover:text-indigo-500 transition-colors"
onClick={() => setShowNodeSelector(true)}
>
{accessibleNodes.length === 0 ? 'No nodes found in mesh' : 'Click to attach nodes'}
</span>
) : (
<div className="flex items-center space-x-1.5 relative group cursor-pointer" onClick={() => setShowNodeSelector(true)}>
<div className="flex -space-x-1.5 mr-1">
{accessibleNodes.filter(n => attachedNodeIds.includes(n.node_id)).slice(0, 3).map((node, i) => {
const isOnline = node.last_status === 'online' || node.last_status === 'idle' || node.last_status === 'busy';
return (
<div key={node.node_id} style={{ zIndex: 10 - i }} className={`w-5 h-5 rounded-full border border-white dark:border-gray-800 flex items-center justify-center text-[9px] font-bold text-white shadow-sm ring-1 ${isOnline ? 'bg-indigo-500 ring-indigo-300' : 'bg-gray-400 ring-gray-300'}`} title={node.display_name}>
{node.display_name.charAt(0).toUpperCase()}
</div>
)
})}
{attachedNodeIds.length > 3 && (
<div style={{ zIndex: 1 }} className="w-5 h-5 rounded-full border border-white dark:border-gray-800 flex items-center justify-center text-[9px] font-bold text-indigo-700 bg-indigo-50 z-0">
+{attachedNodeIds.length - 3}
</div>
)}
</div>
<span className="text-[11px] font-bold text-indigo-700 dark:text-indigo-300 whitespace-nowrap">
{attachedNodeIds.length} Attached
</span>
{(workspaceId || syncConfig.source !== 'empty') && (
<div className="relative group/sync flex items-center h-full ml-1">
<div className="p-1 bg-emerald-50 dark:bg-emerald-900/40 rounded-full border border-emerald-200 dark:border-emerald-800">
<svg className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</div>
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-2 bg-gray-900 border border-gray-700 text-white text-[10px] font-bold rounded shadow-xl opacity-0 group-hover/sync:opacity-100 transition-all whitespace-nowrap z-[60] pointer-events-none">
<span className="text-emerald-400">File Sync Active</span><br />
<span className="text-gray-300 font-mono text-[9px] font-normal tracking-wide mt-1 block">
Workspace: {workspaceId || 'Initializing...'}
</span>
</div>
</div>
)}
<div className="absolute top-full left-0 mt-2 px-2 py-1 bg-gray-900 text-white text-[10px] font-bold rounded opacity-0 group-hover:opacity-100 transition-all whitespace-nowrap z-[50] pointer-events-none delay-150">
Click to manage mesh strategy
</div>
</div>
)}
{attachedNodeIds.length > 0 && (
<div className="flex items-center gap-1 border-l dark:border-gray-700 ml-2 pl-2">
<button
onClick={() => setShowConsole(!showConsole)}
className={`p-1.5 rounded-lg transition-colors ${showConsole ? 'bg-indigo-100 text-indigo-600' : 'text-gray-400 hover:bg-gray-50'}`}
title="Toggle Execution Console"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</button>
{workspaceId && (
<button
onClick={() => setShowFileExplorer(!showFileExplorer)}
className={`p-1.5 rounded-lg transition-colors ${showFileExplorer ? 'bg-indigo-100 text-indigo-600' : 'text-gray-400 hover:bg-gray-50'}`}
title="Toggle File Explorer"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
</button>
)}
</div>
)}
</div>
{!isConfigured && (
<div className="group relative flex items-center">
<svg className="w-5 h-5 text-yellow-500 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-56 bg-gray-900 text-white text-[11px] rounded shadow-lg p-2.5 opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none text-left">
<p className="font-bold mb-1 text-red-400">Missing Key</p>
<ul className="list-disc pl-3 space-y-0.5">
{missingConfigs?.map((m, i) => <li key={i}>{m}</li>)}
</ul>
</div>
</div>
)}
<button
onClick={() => setShowConfigModal(true)}
className="text-gray-400 hover:text-indigo-600 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</button>
</div>
<div className="flex items-center space-x-6">
<div className="flex flex-col items-end hidden md:flex">
<div className="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-1">
Token Usage
</div>
<div className="flex items-center gap-2">
<div className="w-24 h-1.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-700 ease-out ${tokenUsage?.percentage > 80 ? 'bg-red-500' : 'bg-indigo-500'}`}
style={{ width: `${Math.min(tokenUsage?.percentage || 0, 100)}%` }}
></div>
</div>
<span className={`text-xs font-mono font-bold ${tokenUsage?.percentage > 80 ? 'text-red-500' : 'text-gray-400'}`}>
{tokenUsage?.percentage || 0}%
</span>
</div>
</div>
<button
onClick={() => handleSendChat("/new")}
className="text-xs font-bold px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-all shadow-md hover:shadow-indigo-500/20 active:scale-95"
>
+ NEW
</button>
</div>
</h2>
<div className="flex-grow flex flex-col min-h-0">
<ChatArea
chatHistory={chatHistory}
onSendMessage={handleSendChat}
isProcessing={isProcessing}
featureName="coding_assistant"
workspaceId={workspaceId}
syncConfig={activeSyncConfig}
isSourceDisconnected={
activeSyncConfig?.source === 'node_local' &&
activeSyncConfig?.source_node_id &&
(() => {
const sourceNode = accessibleNodes.find(n => n.node_id === activeSyncConfig.source_node_id);
return !sourceNode || (sourceNode.last_status !== 'online' && sourceNode.last_status !== 'idle');
})()
}
/>
</div>
</div>
{/* Antigravity Console (M6) */}
{showConsole && attachedNodeIds.length > 0 && (
<div className="h-64 shadow-xl border-x border-b dark:border-gray-800 animate-in slide-in-from-bottom duration-300">
<MultiNodeConsole attachedNodeIds={attachedNodeIds} />
</div>
)}
</div>
</div>
</div>
{/* Workspace File Explorer (M6) */}
{showFileExplorer && workspaceId && attachedNodeIds.length > 0 && (
<div className="w-80 h-full border-l dark:border-gray-800 bg-white dark:bg-gray-900 flex flex-col min-w-[280px] z-10 animate-in slide-in-from-right duration-300">
<FileSystemNavigator
nodeId={attachedNodeIds[0]}
sessionId={workspaceId}
initialPath="."
showSyncStatus={true}
/>
</div>
)}
{/* Node Selector Modal (M3) */}
{showNodeSelector && (
<div className="fixed inset-0 bg-gray-900/40 backdrop-blur-sm flex justify-center items-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden animate-in zoom-in-95 duration-200">
<div className="px-6 py-5 border-b dark:border-gray-700 flex justify-between items-center bg-gray-50 dark:bg-gray-800/50">
<h3 className="font-bold text-gray-900 dark:text-white flex items-center gap-2">
<svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
Mesh Node Selection
</h3>
<button onClick={() => setShowNodeSelector(false)} className="text-gray-400 hover:text-gray-600">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div className="p-6">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 font-medium">
Select agent nodes to attach to this session. <strong>Click "Save & Sync" below to apply your changes.</strong> Attached nodes share the workspace <span className="font-mono bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded italic">{workspaceId}</span>.
</p>
<div className="bg-indigo-50 dark:bg-indigo-900/10 p-4 rounded-xl mb-6 border border-indigo-100 dark:border-indigo-800">
<h4 className="text-[10px] font-black uppercase text-indigo-600 dark:text-indigo-400 mb-3 tracking-widest">Initialization Strategy</h4>
<div className="flex gap-6">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="radio"
className="text-indigo-600 focus:ring-indigo-500"
checked={syncConfig.source === 'server'}
onChange={() => setSyncConfig({ ...syncConfig, source: 'server' })}
/>
<div className="flex flex-col">
<span className="text-xs font-bold text-gray-700 dark:text-gray-200">Sync from Hub</span>
<span className="text-[10px] text-gray-400">Pull latest session state (Default)</span>
</div>
</label>
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="radio"
className="text-indigo-600 focus:ring-indigo-500"
checked={syncConfig.source === 'node_local'}
onChange={() => setSyncConfig({ ...syncConfig, source: 'node_local' })}
/>
<div className="flex flex-col">
<span className="text-xs font-bold text-gray-700 dark:text-gray-200">Seed from Local</span>
<span className="text-[10px] text-gray-400">Initialize from a node folder</span>
</div>
</label>
</div>
{syncConfig.source === 'node_local' && (
<div className="mt-4 space-y-3 p-3 bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700">
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase mb-1">Source Node</label>
<select
className="w-full bg-gray-50 dark:bg-gray-900 border-none rounded-lg px-3 py-2 text-xs font-bold dark:text-white"
value={syncConfig.source_node_id}
onChange={(e) => setSyncConfig({ ...syncConfig, source_node_id: e.target.value })}
>
<option value="">Select a source node...</option>
{accessibleNodes.filter(n => n.last_status === 'online' || n.last_status === 'idle').map(n => (
<option key={n.node_id} value={n.node_id}>{n.display_name} ({n.node_id})</option>
))}
</select>
</div>
<div className="relative">
<label className="block text-[10px] font-bold text-gray-400 uppercase mb-1 flex justify-between">
<span>Local Absolute Path</span>
{isSearchingPath && <span className="animate-pulse text-indigo-500">Searching...</span>}
</label>
<input
type="text"
placeholder="/home/user/my-project"
className="w-full bg-gray-50 dark:bg-gray-900 border-none rounded-lg px-3 py-2 text-xs font-mono focus:ring-1 focus:ring-indigo-500 transition-all dark:text-white"
value={syncConfig.path}
onChange={(e) => {
setSyncConfig({ ...syncConfig, path: e.target.value });
setShowPathSuggestions(true);
}}
onFocus={() => setShowPathSuggestions(true)}
onBlur={() => setTimeout(() => setShowPathSuggestions(false), 200)}
onKeyDown={(e) => {
if (e.key === 'Enter' && pathSuggestions.length > 0 && showPathSuggestions) {
e.preventDefault();
const topSuggestion = pathSuggestions[0];
const lastSlash = syncConfig.path.lastIndexOf('/');
const base = lastSlash >= 0 ? syncConfig.path.substring(0, lastSlash + 1) : "";
setSyncConfig({ ...syncConfig, path: `${base}${topSuggestion}/` });
setPathSuggestions([]);
}
}}
/>
{showPathSuggestions && pathSuggestions.length > 0 && (
<div className="absolute left-0 right-0 top-full mt-1 bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-lg shadow-xl z-[70] max-h-40 overflow-y-auto overflow-x-hidden">
{pathSuggestions.map((suggestion, idx) => (
<div
key={idx}
className="px-3 py-2 text-[11px] font-mono cursor-pointer hover:bg-indigo-50 dark:hover:bg-indigo-900/30 dark:text-gray-300 border-b last:border-none dark:border-gray-700 truncate"
onClick={() => {
const lastSlash = syncConfig.path.lastIndexOf('/');
const base = lastSlash >= 0 ? syncConfig.path.substring(0, lastSlash + 1) : "";
setSyncConfig({ ...syncConfig, path: `${base}${suggestion}/` });
setPathSuggestions([]);
}}
>
📁 {suggestion}/
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
<div className="space-y-2 max-h-80 overflow-y-auto pr-2">
{accessibleNodes.length === 0 && <span className="text-sm text-gray-400 italic">No nodes available for your account.</span>}
{accessibleNodes.map(node => {
const isAttached = attachedNodeIds.includes(node.node_id);
const isOnline = node.last_status === 'online' || node.last_status === 'idle';
return (
<div
key={node.node_id}
className={`flex items-center justify-between p-4 rounded-xl border-2 transition-all ${isAttached ? 'border-indigo-500 bg-indigo-50/50 dark:bg-indigo-900/10' : 'border-gray-100 dark:border-gray-700 hover:border-gray-200'}`}
>
<div className="flex items-center space-x-3">
<div className={`w-2.5 h-2.5 rounded-full ${isOnline ? 'bg-green-500' : 'bg-gray-300'}`}></div>
<div>
<p className="font-bold text-sm text-gray-900 dark:text-gray-100">{node.display_name}</p>
<p className="text-[10px] text-gray-500 font-mono italic">{node.node_id}</p>
</div>
</div>
<div className="flex items-center gap-2">
{isAttached && (
<button
onClick={() => handleToggleReadOnly(node.node_id)}
className={`px-2 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-tighter transition-all flex items-center gap-1 ${syncConfig.read_only_node_ids?.includes(node.node_id) ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-600 border border-amber-200 dark:border-amber-800' : 'bg-gray-50 dark:bg-gray-700/50 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200'}`}
title={syncConfig.read_only_node_ids?.includes(node.node_id) ? "Receiver only mode active" : "Enable receiver only mode"}
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
{syncConfig.read_only_node_ids?.includes(node.node_id) ? 'Receiver Only' : 'Full Sync'}
</button>
)}
<button
onClick={() => handleToggleNode(node.node_id, isAttached)}
disabled={!isOnline && !isAttached}
className={`px-4 py-1.5 rounded-lg text-xs font-bold transition-all ${isAttached
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: isOnline
? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-indigo-500 hover:text-white'
: 'bg-gray-50 dark:bg-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed grayscale'
}`}
title={!isOnline && !isAttached ? "Node is currently offline" : ""}
>
{isAttached ? 'DETACH' : 'ATTACH'}
</button>
</div>
</div>
);
})}
</div>
</div>
<div className="px-6 py-4 border-t dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex justify-between">
<button
onClick={handleInitiateSync}
disabled={
isInitiatingSync ||
(syncConfig.source === 'node_local' && (!syncConfig.source_node_id || !syncConfig.path))
}
className="bg-indigo-600 text-white px-6 py-2 rounded-xl text-sm font-bold shadow-lg shadow-indigo-600/20 active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isInitiatingSync ? (
<>
<svg className="animate-spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
Initiating...
</>
) : 'Save & Sync'}
</button>
<button
onClick={() => {
setShowNodeSelector(false);
isEditingMeshRef.current = false;
fetchNodeInfo(); // Revert any unsaved toggle states instantly
}}
className="bg-gray-900 dark:bg-white dark:text-gray-900 text-white px-6 py-2 rounded-xl text-sm font-bold shadow-lg shadow-gray-900/20 active:scale-95 transition-all"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Error Modal */}
{showErrorModal && (
<div className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm flex justify-center items-center z-50 animate-in fade-in duration-300">
<div className="bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-2xl max-w-sm w-full text-center border border-red-100 dark:border-red-900/30">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
</div>
<h2 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">Attention Required</h2>
<p className="text-gray-500 dark:text-gray-400 mb-6 text-sm">{errorMessage}</p>
<button
onClick={() => setShowErrorModal(false)}
className="w-full bg-gray-900 dark:bg-white dark:text-gray-900 text-white font-bold py-3 rounded-xl transition-all active:scale-95"
>
Understand
</button>
</div>
</div>
)}
{showConfigModal && (
<div className="fixed inset-0 bg-gray-900/40 backdrop-blur-sm flex justify-center items-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in-95 duration-200">
<div className="px-6 py-5 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex justify-between items-center">
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<svg className="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Session Engine
</h3>
<button onClick={() => setShowConfigModal(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div className="p-8 space-y-6">
<div>
<label className="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Active LLM Provider</label>
<select
value={localActiveLLM}
onChange={(e) => setLocalActiveLLM(e.target.value)}
className="w-full border-2 border-gray-100 dark:border-gray-700 rounded-xl p-3 bg-white dark:bg-gray-800 text-sm font-medium focus:border-indigo-500 focus:ring-0 transition-all outline-none"
>
<option value="">-- Choose Provider --</option>
{userConfigData?.effective?.llm?.providers && Object.keys(userConfigData.effective.llm.providers).map(pid => {
const modelName = userConfigData.effective.llm.providers[pid].model;
return (
<option key={pid} value={pid}>
{pid} {modelName ? `(${modelName})` : ''}
</option>
);
})}
</select>
</div>
</div>
<div className="px-8 py-5 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-100 dark:border-gray-700 flex justify-end gap-3">
<button
onClick={() => setShowConfigModal(false)}
className="px-6 py-2.5 text-sm font-bold text-gray-500 hover:text-gray-800 transition-colors"
>
Cancel
</button>
<button
onClick={handleSaveQuickConfig}
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-500/20 active:scale-95 transition-all"
>
Apply Changes
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default CodeAssistantPage;