diff --git a/gitnexus-web/src/App.tsx b/gitnexus-web/src/App.tsx index 553a4c7cd..33809a9af 100644 --- a/gitnexus-web/src/App.tsx +++ b/gitnexus-web/src/App.tsx @@ -13,7 +13,7 @@ import { FileEntry } from './services/zip'; import { getActiveProviderConfig } from './core/llm/settings-service'; import { createKnowledgeGraph } from './core/graph/graph'; import { connectToServer, fetchRepos, normalizeServerUrl, type ConnectToServerResult } from './services/server-connection'; -import { HelpPanel } from './components/HelpPanel'; +import { ERROR_RESET_DELAY_MS } from './config/ui-constants'; const AppContent = () => { const { @@ -29,11 +29,9 @@ const AppContent = () => { runPipelineFromFiles, isSettingsPanelOpen, setSettingsPanelOpen, - isHelpDialogBoxOpen, - setHelpDialogBoxOpen, refreshLLMSettings, initializeAgent, - startEmbeddings, + startEmbeddingsWithFallback, embeddingStatus, codeReferences, selectedNode, @@ -44,7 +42,6 @@ const AppContent = () => { setAvailableRepos, switchRepo, loadServerGraph, - graph } = useAppState(); const graphCanvasRef = useRef(null); @@ -72,13 +69,7 @@ const AppContent = () => { // Auto-start embeddings pipeline in background // Uses WebGPU if available, falls back to WASM - startEmbeddings().catch((err) => { - if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { - startEmbeddings('wasm').catch(console.warn); - } else { - console.warn('Embeddings auto-start failed:', err); - } - }); + startEmbeddingsWithFallback(); } catch (error) { console.error('Pipeline error:', error); setProgress({ @@ -90,9 +81,9 @@ const AppContent = () => { setTimeout(() => { setViewMode('onboarding'); setProgress(null); - }, 3000); + }, ERROR_RESET_DELAY_MS); } - }, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipeline, startEmbeddings, initializeAgent]); + }, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipeline, startEmbeddingsWithFallback, initializeAgent]); const handleGitClone = useCallback(async (files: FileEntry[]) => { const firstPath = files[0]?.path || 'repository'; @@ -115,13 +106,7 @@ const AppContent = () => { initializeAgent(projectName); } - startEmbeddings().catch((err) => { - if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { - startEmbeddings('wasm').catch(console.warn); - } else { - console.warn('Embeddings auto-start failed:', err); - } - }); + startEmbeddingsWithFallback(); } catch (error) { console.error('Pipeline error:', error); setProgress({ @@ -133,9 +118,9 @@ const AppContent = () => { setTimeout(() => { setViewMode('onboarding'); setProgress(null); - }, 3000); + }, ERROR_RESET_DELAY_MS); } - }, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent]); + }, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddingsWithFallback, initializeAgent]); const handleServerConnect = useCallback((result: ConnectToServerResult): Promise => { // Extract project name from repoPath @@ -172,13 +157,7 @@ const AppContent = () => { } }) .then(() => { - startEmbeddings().catch((err) => { - if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { - startEmbeddings('wasm').catch(console.warn); - } else { - console.warn('Embeddings auto-start failed:', err); - } - }); + startEmbeddingsWithFallback(); }) .catch((err) => { console.warn('Failed to load graph into LadybugDB:', err); @@ -186,7 +165,7 @@ const AppContent = () => { }); return loadGraphPromise; - }, [setViewMode, setGraph, setFileContents, setProjectName, loadServerGraph, initializeAgent, startEmbeddings]); + }, [setViewMode, setGraph, setFileContents, setProjectName, loadServerGraph, initializeAgent, startEmbeddingsWithFallback]); // Auto-connect when ?server query param is present (bookmarkable shortcut) const autoConnectRan = useRef(false); @@ -235,7 +214,7 @@ const AppContent = () => { setTimeout(() => { setViewMode('onboarding'); setProgress(null); - }, 3000); + }, ERROR_RESET_DELAY_MS); }); }, [handleServerConnect, setProgress, setViewMode, setServerBaseUrl, setAvailableRepos]); @@ -309,13 +288,6 @@ const AppContent = () => { onSettingsSaved={handleSettingsSaved} /> - setHelpDialogBoxOpen(false)} - nodeCount={graph!.nodes.length} - edgeCount={graph!.relationships.length} - /> - ); }; diff --git a/gitnexus-web/src/components/BackendRepoSelector.tsx b/gitnexus-web/src/components/BackendRepoSelector.tsx index 7e158c4b1..4eb107f0b 100644 --- a/gitnexus-web/src/components/BackendRepoSelector.tsx +++ b/gitnexus-web/src/components/BackendRepoSelector.tsx @@ -1,4 +1,4 @@ -import { Server, ArrowRight } from 'lucide-react'; +import { Server, ArrowRight } from '@/lib/lucide-icons'; import { BackendRepo } from '../services/backend'; interface BackendRepoSelectorProps { diff --git a/gitnexus-web/src/components/CodeReferencesPanel.tsx b/gitnexus-web/src/components/CodeReferencesPanel.tsx index 021ccd902..89ff3026d 100644 --- a/gitnexus-web/src/components/CodeReferencesPanel.tsx +++ b/gitnexus-web/src/components/CodeReferencesPanel.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Code, PanelLeftClose, PanelLeft, Trash2, X, Target, FileCode, Sparkles, MousePointerClick } from 'lucide-react'; +import { Code, PanelLeftClose, PanelLeft, Trash2, X, Target, FileCode, Sparkles, MousePointerClick } from '@/lib/lucide-icons'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useAppState } from '../hooks/useAppState'; +import type { GraphNode } from '../core/graph/types'; import { NODE_COLORS } from '../lib/constants'; /** Map file extension to Prism syntax highlighter language identifier */ @@ -75,6 +76,11 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = codeReferenceFocus, } = useAppState(); + const nodeById = useMemo(() => { + if (!graph) return new Map(); + return new Map(graph.nodes.map(n => [n.id, n])); + }, [graph]); + const [isCollapsed, setIsCollapsed] = useState(false); const [glowRefId, setGlowRefId] = useState(null); const panelRef = useRef(null); @@ -161,8 +167,9 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = if (!target) return; // Double rAF: wait for collapse state + list DOM to render. - requestAnimationFrame(() => { - requestAnimationFrame(() => { + const rafIds: number[] = []; + const outerRafId = requestAnimationFrame(() => { + const innerRafId = requestAnimationFrame(() => { const el = refCardEls.current.get(target.id); if (!el) return; @@ -177,7 +184,13 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = glowTimerRef.current = null; }, 1200); }); + rafIds.push(innerRafId); }); + rafIds.push(outerRafId); + + return () => { + rafIds.forEach(id => cancelAnimationFrame(id)); + }; }, [codeReferenceFocus?.ts, aiReferences]); const refsWithSnippets = useMemo(() => { @@ -414,7 +427,7 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = const nodeId = ref.nodeId!; // Sync selection + focus graph if (graph) { - const node = graph.nodes.find((n) => n.id === nodeId); + const node = nodeById.get(nodeId); if (node) setSelectedNode(node); } onFocusNode(nodeId); diff --git a/gitnexus-web/src/components/DropZone.tsx b/gitnexus-web/src/components/DropZone.tsx index fa7857668..64e5798eb 100644 --- a/gitnexus-web/src/components/DropZone.tsx +++ b/gitnexus-web/src/components/DropZone.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, DragEvent } from 'react'; -import { Upload, FileArchive, Github, Loader2, ArrowRight, Key, Eye, EyeOff, Globe, X } from 'lucide-react'; +import { Upload, FileArchive, Github, Loader2, ArrowRight, Key, Eye, EyeOff, Globe, X } from '@/lib/lucide-icons'; import { cloneRepository, parseGitHubUrl } from '../services/git-clone'; import { connectToServer, type ConnectToServerResult } from '../services/server-connection'; import { FileEntry } from '../services/zip'; @@ -27,9 +27,13 @@ export const DropZone = ({ onFileSelect, onGitClone, onServerConnect }: DropZone const [error, setError] = useState(null); // Server tab state - const [serverUrl, setServerUrl] = useState(() => - localStorage.getItem('gitnexus-server-url') || '' - ); + const [serverUrl, setServerUrl] = useState(() => { + try { + return localStorage.getItem('gitnexus-server-url') || ''; + } catch { + return ''; + } + }); const [isConnecting, setIsConnecting] = useState(false); const [serverProgress, setServerProgress] = useState<{ phase: string; @@ -133,7 +137,11 @@ export const DropZone = ({ onFileSelect, onGitClone, onServerConnect }: DropZone } // Persist URL to localStorage - localStorage.setItem('gitnexus-server-url', serverUrl); + try { + localStorage.setItem('gitnexus-server-url', serverUrl); + } catch { + // localStorage may be unavailable (e.g. private browsing, quota exceeded) + } setError(null); setIsConnecting(true); diff --git a/gitnexus-web/src/components/EmbeddingStatus.tsx b/gitnexus-web/src/components/EmbeddingStatus.tsx index 4bf6001c9..a7c051ab1 100644 --- a/gitnexus-web/src/components/EmbeddingStatus.tsx +++ b/gitnexus-web/src/components/EmbeddingStatus.tsx @@ -1,4 +1,4 @@ -import { Brain, Loader2, Check, AlertCircle, Zap, FlaskConical } from 'lucide-react'; +import { Brain, Loader2, Check, AlertCircle, Zap, FlaskConical } from '@/lib/lucide-icons'; import { useAppState } from '../hooks/useAppState'; import { useState } from 'react'; import { WebGPUFallbackDialog } from './WebGPUFallbackDialog'; diff --git a/gitnexus-web/src/components/FileTreePanel.tsx b/gitnexus-web/src/components/FileTreePanel.tsx index 6daca3709..2007e54d4 100644 --- a/gitnexus-web/src/components/FileTreePanel.tsx +++ b/gitnexus-web/src/components/FileTreePanel.tsx @@ -14,7 +14,7 @@ import { Variable, Hash, Target, -} from 'lucide-react'; +} from '@/lib/lucide-icons'; import { useAppState } from '../hooks/useAppState'; import { FILTERABLE_LABELS, NODE_COLORS, ALL_EDGE_TYPES, EDGE_INFO, type EdgeType } from '../lib/constants'; import { GraphNode, NodeLabel } from '../core/graph/types'; @@ -98,13 +98,15 @@ const TreeItem = ({ const isSelected = selectedPath === node.path; const hasChildren = node.children.length > 0; - // Filter children based on search + // Filter children based on search (recursive) const filteredChildren = useMemo(() => { if (!searchQuery) return node.children; - return node.children.filter(child => - child.name.toLowerCase().includes(searchQuery.toLowerCase()) || - child.children.some(c => c.name.toLowerCase().includes(searchQuery.toLowerCase())) - ); + const searchLower = searchQuery.toLowerCase(); + const matchesSearch = (node: TreeNode, query: string): boolean => { + if (node.name.toLowerCase().includes(query)) return true; + return node.children?.some(child => matchesSearch(child, query)) ?? false; + }; + return node.children.filter(child => matchesSearch(child, searchLower)); }, [node.children, searchQuery]); // Check if this node matches search diff --git a/gitnexus-web/src/components/GraphCanvas.tsx b/gitnexus-web/src/components/GraphCanvas.tsx index 7618478a0..9585584d9 100644 --- a/gitnexus-web/src/components/GraphCanvas.tsx +++ b/gitnexus-web/src/components/GraphCanvas.tsx @@ -1,8 +1,9 @@ import { useEffect, useCallback, useMemo, useState, forwardRef, useImperativeHandle } from 'react'; -import { ZoomIn, ZoomOut, Maximize2, Focus, RotateCcw, Play, Pause, Lightbulb, LightbulbOff } from 'lucide-react'; +import { ZoomIn, ZoomOut, Maximize2, Focus, RotateCcw, Play, Pause, Lightbulb, LightbulbOff } from '@/lib/lucide-icons'; import { useSigma } from '../hooks/useSigma'; import { useAppState } from '../hooks/useAppState'; import { knowledgeGraphToGraphology, filterGraphByDepth, SigmaNodeAttributes, SigmaEdgeAttributes } from '../lib/graph-adapter'; +import type { GraphNode } from '../core/graph/types'; import { QueryFAB } from './QueryFAB'; import Graph from 'graphology'; @@ -54,30 +55,44 @@ export const GraphCanvas = forwardRef((_, ref) => { return animatedNodes; }, [animatedNodes, isAIHighlightsEnabled]); + const nodeById = useMemo(() => { + if (!graph) return new Map(); + return new Map(graph.nodes.map(n => [n.id, n])); + }, [graph]); + const handleNodeClick = useCallback((nodeId: string) => { if (!graph) return; - const node = graph.nodes.find(n => n.id === nodeId); + const node = nodeById.get(nodeId); if (node) { setSelectedNode(node); openCodePanel(); } - }, [graph, setSelectedNode, openCodePanel]); + }, [graph, nodeById, setSelectedNode, openCodePanel]); const handleNodeHover = useCallback((nodeId: string | null) => { if (!nodeId || !graph) { setHoveredNodeName(null); return; } - const node = graph.nodes.find(n => n.id === nodeId); - if (node) { - setHoveredNodeName(node.properties.name); - } - }, [graph]); + const node = nodeById.get(nodeId); + setHoveredNodeName(node ? node.properties.name : null); + }, [graph, nodeById]); const handleStageClick = useCallback(() => { setSelectedNode(null); }, [setSelectedNode]); + const handleToggleAIHighlights = useCallback(() => { + if (isAIHighlightsEnabled) { + clearAIToolHighlights(); + clearAICitationHighlights(); + clearBlastRadius(); + setSelectedNode(null); + setSigmaSelectedNode(null); + } + toggleAIHighlights(); + }, [isAIHighlightsEnabled, clearAIToolHighlights, clearAICitationHighlights, clearBlastRadius, setSelectedNode, toggleAIHighlights]); + const { containerRef, sigmaRef, @@ -106,7 +121,7 @@ export const GraphCanvas = forwardRef((_, ref) => { focusNode: (nodeId: string) => { // Also update app state so the selection syncs properly if (graph) { - const node = graph.nodes.find(n => n.id === nodeId); + const node = nodeById.get(nodeId); if (node) { setSelectedNode(node); openCodePanel(); @@ -114,7 +129,7 @@ export const GraphCanvas = forwardRef((_, ref) => { } focusNode(nodeId); } - }), [focusNode, graph, setSelectedNode, openCodePanel]); + }), [focusNode, graph, nodeById, setSelectedNode, openCodePanel]); // Update Sigma graph when KnowledgeGraph changes useEffect(() => { @@ -126,10 +141,11 @@ export const GraphCanvas = forwardRef((_, ref) => { graph.relationships.forEach(rel => { if (rel.type === 'MEMBER_OF') { // Find the community node to get its index - const communityNode = graph.nodes.find(n => n.id === rel.targetId && n.label === 'Community'); - if (communityNode) { + const communityNode = nodeById.get(rel.targetId); + if (communityNode && communityNode.label === 'Community') { // Extract community index from id (e.g., "comm_5" -> 5) - const communityIdx = parseInt(rel.targetId.replace('comm_', ''), 10) || 0; + const numericPart = rel.targetId.replace('comm_', ''); + const communityIdx = /^\d+$/.test(numericPart) ? parseInt(numericPart, 10) : 0; communityMemberships.set(rel.sourceId, communityIdx); } } @@ -137,7 +153,7 @@ export const GraphCanvas = forwardRef((_, ref) => { const sigmaGraph = knowledgeGraphToGraphology(graph, communityMemberships); setSigmaGraph(sigmaGraph); - }, [graph, setSigmaGraph]); + }, [graph, nodeById, setSigmaGraph]); // Update node visibility when filters change useEffect(() => { @@ -149,7 +165,8 @@ export const GraphCanvas = forwardRef((_, ref) => { filterGraphByDepth(sigmaGraph, appSelectedNode?.id || null, depthFilter, visibleLabels); sigma.refresh(); - }, [visibleLabels, depthFilter, appSelectedNode, sigmaRef]); + // eslint-disable-next-line react-hooks/exhaustive-deps -- sigmaRef identity never changes + }, [visibleLabels, depthFilter, appSelectedNode]); // Sync app selected node with sigma useEffect(() => { @@ -307,17 +324,7 @@ export const GraphCanvas = forwardRef((_, ref) => { {/* AI Highlights toggle - Top Right */}
+ } + > + setShowModal(false)} + /> + )} ); diff --git a/gitnexus-web/src/components/ProcessFlowModal.tsx b/gitnexus-web/src/components/ProcessFlowModal.tsx index fabad1eed..93017f376 100644 --- a/gitnexus-web/src/components/ProcessFlowModal.tsx +++ b/gitnexus-web/src/components/ProcessFlowModal.tsx @@ -148,7 +148,7 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph, isFullScree const { svg } = await mermaid.render(id, mermaidCode); if (!diagramRef.current) return; - diagramRef.current!.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }); + diagramRef.current!.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: ['foreignObject'] }); } catch (error) { console.error('Mermaid render error:', error); const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/gitnexus-web/src/components/QueryFAB.tsx b/gitnexus-web/src/components/QueryFAB.tsx index ccb3c10e9..c8be4c876 100644 --- a/gitnexus-web/src/components/QueryFAB.tsx +++ b/gitnexus-web/src/components/QueryFAB.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect, useCallback } from 'react'; -import { Terminal, Play, X, ChevronDown, ChevronUp, Loader2, Sparkles, Table } from 'lucide-react'; +import { Terminal, Play, X, ChevronDown, ChevronUp, Loader2, Sparkles, Table } from '@/lib/lucide-icons'; import { useAppState } from '../hooks/useAppState'; const EXAMPLE_QUERIES = [ diff --git a/gitnexus-web/src/components/RightPanel.tsx b/gitnexus-web/src/components/RightPanel.tsx index 84be0f738..b806240fc 100644 --- a/gitnexus-web/src/components/RightPanel.tsx +++ b/gitnexus-web/src/components/RightPanel.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { Send, Square, Sparkles, User, PanelRightClose, Loader2, AlertTriangle, GitBranch -} from 'lucide-react'; +} from '@/lib/lucide-icons'; import { useAppState } from '../hooks/useAppState'; import { ToolCallCard } from './ToolCallCard'; import { isProviderConfigured } from '../core/llm/settings-service'; diff --git a/gitnexus-web/src/components/SettingsPanel.tsx b/gitnexus-web/src/components/SettingsPanel.tsx index d60475107..c93dd7df9 100644 --- a/gitnexus-web/src/components/SettingsPanel.tsx +++ b/gitnexus-web/src/components/SettingsPanel.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, ChevronDown, Loader2, Search } from 'lucide-react'; +import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, ChevronDown, Loader2, Search } from '@/lib/lucide-icons'; import { loadSettings, saveSettings, @@ -7,6 +7,8 @@ import { fetchOpenRouterModels, } from '../core/llm/settings-service'; import type { LLMSettings, LLMProvider } from '../core/llm/types'; +import { DEFAULT_OLLAMA_BASE_URL } from '../config/ui-constants'; +import { ProviderConfigCard } from './settings/ProviderConfigCard'; interface SettingsPanelProps { isOpen: boolean; @@ -216,6 +218,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is const [settings, setSettings] = useState(loadSettings); const [showApiKey, setShowApiKey] = useState>({}); const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle'); + const saveTimerRef = useRef>(undefined); // Ollama connection state const [ollamaError, setOllamaError] = useState(null); const [isCheckingOllama, setIsCheckingOllama] = useState(false); @@ -223,6 +226,15 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is const [openRouterModels, setOpenRouterModels] = useState>([]); const [isLoadingModels, setIsLoadingModels] = useState(false); + // Clean up save timer on unmount + useEffect(() => { + return () => { + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + } + }; + }, []); + // Load settings when panel opens useEffect(() => { if (isOpen) { @@ -252,7 +264,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is useEffect(() => { if (settings.activeProvider === 'ollama') { - const baseUrl = settings.ollama?.baseUrl ?? 'http://localhost:11434'; + const baseUrl = settings.ollama?.baseUrl ?? DEFAULT_OLLAMA_BASE_URL; const timer = setTimeout(() => { checkOllamaConnection(baseUrl); }, 300); @@ -269,7 +281,10 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is saveSettings(settings); setSaveStatus('saved'); onSettingsSaved?.(); - setTimeout(() => setSaveStatus('idle'), 2000); + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + } + saveTimerRef.current = setTimeout(() => setSaveStatus('idle'), 2000); } catch { setSaveStatus('error'); } @@ -374,60 +389,36 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is +
+ API keys are stored in session storage and will be cleared when you close this tab. +
+ {/* OpenAI Settings */} {settings.activeProvider === 'openai' && ( -
-
- -
- setSettings(prev => ({ - ...prev, - openai: { ...prev.openai!, apiKey: e.target.value } - }))} - placeholder="Enter your OpenAI API key" - className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" - /> - -
-

- Get your API key from{' '} - - OpenAI Platform - -

-
- -
- - setSettings(prev => ({ - ...prev, - openai: { ...prev.openai!, model: e.target.value } - }))} - placeholder="e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> -
- + setSettings(prev => ({ + ...prev, + openai: { ...prev.openai!, apiKey: value } + })), + onToggleVisibility: () => toggleApiKeyVisibility('openai'), + }} + model={{ + value: settings.openai?.model ?? 'gpt-5.2-chat', + placeholder: 'e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo', + onChange: (value) => setSettings(prev => ({ + ...prev, + openai: { ...prev.openai!, model: value } + })), + }} + >
-
+ )} {/* Gemini Settings */} {settings.activeProvider === 'gemini' && ( -
-
- -
- setSettings(prev => ({ - ...prev, - gemini: { ...prev.gemini!, apiKey: e.target.value } - }))} - placeholder="Enter your Google AI API key" - className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" - /> - -
-

- Get your API key from{' '} - - Google AI Studio - -

-
- -
- - setSettings(prev => ({ - ...prev, - gemini: { ...prev.gemini!, model: e.target.value } - }))} - placeholder="e.g., gemini-2.0-flash, gemini-1.5-pro" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> -
-
+ setSettings(prev => ({ + ...prev, + gemini: { ...prev.gemini!, apiKey: value } + })), + onToggleVisibility: () => toggleApiKeyVisibility('gemini'), + }} + model={{ + value: settings.gemini?.model ?? 'gemini-2.0-flash', + placeholder: 'e.g., gemini-2.0-flash, gemini-1.5-pro', + onChange: (value) => setSettings(prev => ({ + ...prev, + gemini: { ...prev.gemini!, model: value } + })), + }} + /> )} {/* Anthropic Settings */} {settings.activeProvider === 'anthropic' && ( -
-
- -
- setSettings(prev => ({ - ...prev, - anthropic: { ...prev.anthropic!, apiKey: e.target.value } - }))} - placeholder="Enter your Anthropic API key" - className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" - /> - -
-

- Get your API key from{' '} - - Anthropic Console - -

-
- -
- - setSettings(prev => ({ - ...prev, - anthropic: { ...prev.anthropic!, model: e.target.value } - }))} - placeholder="e.g., claude-sonnet-4-20250514, claude-3-opus" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> -
-
+ setSettings(prev => ({ + ...prev, + anthropic: { ...prev.anthropic!, apiKey: value } + })), + onToggleVisibility: () => toggleApiKeyVisibility('anthropic'), + }} + model={{ + value: settings.anthropic?.model ?? 'claude-sonnet-4-20250514', + placeholder: 'e.g., claude-sonnet-4-20250514, claude-3-opus', + onChange: (value) => setSettings(prev => ({ + ...prev, + anthropic: { ...prev.anthropic!, model: value } + })), + }} + /> )} {/* Azure OpenAI Settings */} @@ -695,17 +630,17 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is
setSettings(prev => ({ ...prev, ollama: { ...prev.ollama!, baseUrl: e.target.value } }))} - placeholder="http://localhost:11434" + placeholder={DEFAULT_OLLAMA_BASE_URL} className="flex-1 px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" /> -
-

- Get your API key from{' '} - - OpenRouter Keys - -

- - + setSettings(prev => ({ + ...prev, + openrouter: { ...prev.openrouter!, apiKey: value } + })), + onToggleVisibility: () => toggleApiKeyVisibility('openrouter'), + }} + >

- +
)} {/* MiniMax Settings */} {settings.activeProvider === 'minimax' && ( -
-
- -
- setSettings(prev => ({ - ...prev, - minimax: { ...prev.minimax!, apiKey: e.target.value } - }))} - placeholder="Enter your MiniMax API key" - className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" - /> - -
-

- Get your API key from{' '} - - MiniMax Platform - -

-
- -
- - setSettings(prev => ({ - ...prev, - minimax: { ...prev.minimax!, model: e.target.value } - }))} - placeholder="e.g., MiniMax-M2.5, MiniMax-M2.5-highspeed" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> -

- Available models: MiniMax-M2.5 (default), MiniMax-M2.5-highspeed (faster) -

-
-
+ setSettings(prev => ({ + ...prev, + minimax: { ...prev.minimax!, apiKey: value } + })), + onToggleVisibility: () => toggleApiKeyVisibility('minimax'), + }} + model={{ + value: settings.minimax?.model ?? 'MiniMax-M2.5', + placeholder: 'e.g., MiniMax-M2.5, MiniMax-M2.5-highspeed', + onChange: (value) => setSettings(prev => ({ + ...prev, + minimax: { ...prev.minimax!, model: value } + })), + helperText: 'Available: MiniMax-M2.5 (default), MiniMax-M2.5-highspeed (faster)', + }} + /> )} {/* Privacy Note */} @@ -880,8 +763,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved, backendUrl, is πŸ”’
- Privacy: Your API keys are stored only in your browser's local storage. - They're sent directly to the LLM provider when you chat. Your code never leaves your machine. + Privacy: Your API keys are stored only in your browser's session storage and are cleared when the tab closes. They're sent directly to the LLM provider when you chat. Your code never leaves your machine.
diff --git a/gitnexus-web/src/components/StatusBar.tsx b/gitnexus-web/src/components/StatusBar.tsx index 3240a008a..6a6bed6a1 100644 --- a/gitnexus-web/src/components/StatusBar.tsx +++ b/gitnexus-web/src/components/StatusBar.tsx @@ -1,4 +1,5 @@ -import { Heart } from 'lucide-react'; +import { useMemo } from 'react'; +import { Heart } from '@/lib/lucide-icons'; import { useAppState } from '../hooks/useAppState'; export const StatusBar = () => { @@ -8,7 +9,7 @@ export const StatusBar = () => { const edgeCount = graph?.relationships.length ?? 0; // Detect primary language - const primaryLanguage = (() => { + const primaryLanguage = useMemo(() => { if (!graph) return null; const languages = graph.nodes .map(n => n.properties.language) @@ -21,7 +22,7 @@ export const StatusBar = () => { }, {} as Record); return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0]; - })(); + }, [graph]); return (