mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-06 08:16:02 +00:00
fixed settings icon
This commit is contained in:
parent
f038253733
commit
f83d90b090
5 changed files with 135 additions and 44 deletions
|
|
@ -22,7 +22,7 @@ interface HeaderProps {
|
|||
}
|
||||
|
||||
export const Header = ({ onFocusNode }: HeaderProps) => {
|
||||
const { projectName, graph, openChatPanel, isRightPanelOpen, rightPanelTab } = useAppState();
|
||||
const { projectName, graph, openChatPanel, isRightPanelOpen, rightPanelTab, setSettingsPanelOpen } = useAppState();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
|
@ -35,7 +35,7 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
// Search results - filter nodes by name
|
||||
const searchResults = useMemo(() => {
|
||||
if (!graph || !searchQuery.trim()) return [];
|
||||
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return graph.nodes
|
||||
.filter(node => node.properties.name.toLowerCase().includes(query))
|
||||
|
|
@ -73,7 +73,7 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
// Handle keyboard navigation in results
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (!isSearchOpen || searchResults.length === 0) return;
|
||||
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(i => Math.min(i + 1, searchResults.length - 1));
|
||||
|
|
@ -140,7 +140,7 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
⌘K
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{isSearchOpen && searchQuery.trim() && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-surface border border-border-subtle rounded-lg shadow-xl overflow-hidden z-50">
|
||||
|
|
@ -154,14 +154,13 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
<button
|
||||
key={node.id}
|
||||
onClick={() => handleSelectNode(node)}
|
||||
className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors ${
|
||||
index === selectedIndex
|
||||
? 'bg-accent/20 text-text-primary'
|
||||
: 'hover:bg-hover text-text-secondary'
|
||||
}`}
|
||||
className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors ${index === selectedIndex
|
||||
? 'bg-accent/20 text-text-primary'
|
||||
: 'hover:bg-hover text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
{/* Node type indicator */}
|
||||
<span
|
||||
<span
|
||||
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: NODE_TYPE_COLORS[node.label] || '#6b7280' }}
|
||||
/>
|
||||
|
|
@ -208,7 +207,11 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
<EmbeddingStatus />
|
||||
|
||||
{/* Icon buttons */}
|
||||
<button className="w-9 h-9 flex items-center justify-center rounded-md text-text-secondary hover:bg-hover hover:text-text-primary transition-colors">
|
||||
<button
|
||||
onClick={() => setSettingsPanelOpen(true)}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-md text-text-secondary hover:bg-hover hover:text-text-primary transition-colors"
|
||||
title="AI Settings"
|
||||
>
|
||||
<Settings className="w-[18px] h-[18px]" />
|
||||
</button>
|
||||
<button className="w-9 h-9 flex items-center justify-center rounded-md text-text-secondary hover:bg-hover hover:text-text-primary transition-colors">
|
||||
|
|
@ -221,7 +224,7 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
className={`
|
||||
flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-all
|
||||
${isRightPanelOpen && rightPanelTab === 'chat'
|
||||
? 'bg-accent text-white shadow-glow'
|
||||
? 'bg-accent text-white shadow-glow'
|
||||
: 'bg-gradient-to-r from-accent to-accent-dim text-white shadow-glow hover:shadow-lg hover:-translate-y-0.5'
|
||||
}
|
||||
`}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Send, Sparkles, User,
|
||||
PanelRightClose, Loader2, Settings, AlertTriangle
|
||||
PanelRightClose, Loader2, AlertTriangle
|
||||
} from 'lucide-react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
|
@ -43,7 +43,6 @@ export const RightPanel = () => {
|
|||
agentError,
|
||||
isAgentReady,
|
||||
isAgentInitializing,
|
||||
setSettingsPanelOpen,
|
||||
sendChatMessage,
|
||||
clearChat,
|
||||
} = useAppState();
|
||||
|
|
@ -290,8 +289,9 @@ export const RightPanel = () => {
|
|||
};
|
||||
|
||||
const chatSuggestions = [
|
||||
'Explain the project architecture',
|
||||
'What does this project do?',
|
||||
'Show me the entry point',
|
||||
'Show me the most important files',
|
||||
'Find all API handlers',
|
||||
];
|
||||
|
||||
|
|
@ -332,13 +332,6 @@ export const RightPanel = () => {
|
|||
<Loader2 className="w-3 h-3 animate-spin" /> Connecting
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSettingsPanelOpen(true)}
|
||||
className="p-1.5 rounded-md text-text-muted hover:text-text-primary hover:bg-hover transition-colors"
|
||||
title="AI Settings"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ const fetchGeminiModels = async (apiKey: string): Promise<string[]> => {
|
|||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
||||
);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
// Filter for chat-capable models and extract model names
|
||||
const models = (data.models || [])
|
||||
.filter((model: any) => {
|
||||
|
|
@ -49,7 +49,7 @@ const fetchGeminiModels = async (apiKey: string): Promise<string[]> => {
|
|||
};
|
||||
return score(a) - score(b) || a.localeCompare(b);
|
||||
});
|
||||
|
||||
|
||||
return models;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch Gemini models:', error);
|
||||
|
|
@ -61,7 +61,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
const [settings, setSettings] = useState<LLMSettings>(loadSettings);
|
||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle');
|
||||
|
||||
|
||||
// Gemini model fetching state
|
||||
const [geminiModels, setGeminiModels] = useState<string[]>([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
|
|
@ -87,14 +87,14 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
setGeminiModels([]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsLoadingModels(true);
|
||||
setModelFetchError(null);
|
||||
|
||||
|
||||
const models = await fetchGeminiModels(apiKey);
|
||||
|
||||
|
||||
setIsLoadingModels(false);
|
||||
|
||||
|
||||
if (models.length > 0) {
|
||||
setGeminiModels(models);
|
||||
setModelFetchError(null);
|
||||
|
|
@ -136,14 +136,14 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const providers: LLMProvider[] = ['gemini', 'anthropic', 'azure-openai'];
|
||||
|
||||
const availableGeminiModels = geminiModels.length > 0
|
||||
? geminiModels
|
||||
const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai'];
|
||||
|
||||
const availableGeminiModels = geminiModels.length > 0
|
||||
? geminiModels
|
||||
: getAvailableModels('gemini');
|
||||
const currentGeminiModel = settings.gemini?.model ?? 'gemini-2.0-flash';
|
||||
const isCustomModelSelected = !availableGeminiModels.includes(currentGeminiModel);
|
||||
|
||||
|
||||
const availableAnthropicModels = getAvailableModels('anthropic');
|
||||
const currentAnthropicModel = settings.anthropic?.model ?? 'claude-sonnet-4-20250514';
|
||||
const isCustomAnthropicModelSelected = !availableAnthropicModels.includes(currentAnthropicModel);
|
||||
|
|
@ -201,7 +201,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
w-8 h-8 rounded-lg flex items-center justify-center text-lg
|
||||
${settings.activeProvider === provider ? 'bg-accent/20' : 'bg-surface'}
|
||||
`}>
|
||||
{provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : '☁️'}
|
||||
{provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : '☁️'}
|
||||
</div>
|
||||
<span className="font-medium">{getProviderDisplayName(provider)}</span>
|
||||
</button>
|
||||
|
|
@ -209,6 +209,84 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* OpenAI Settings */}
|
||||
{settings.activeProvider === 'openai' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-text-secondary">
|
||||
<Key className="w-4 h-4" />
|
||||
API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showApiKey['openai'] ? 'text' : 'password'}
|
||||
value={settings.openai?.apiKey ?? ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleApiKeyVisibility('openai')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
{showApiKey['openai'] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
Get your API key from{' '}
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
OpenAI Platform
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
<select
|
||||
value={settings.openai?.model ?? 'gpt-4o'}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
openai: { ...prev.openai!, model: e.target.value }
|
||||
}))}
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
{getAvailableModels('openai').map(model => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-text-secondary">
|
||||
<Server className="w-4 h-4" />
|
||||
Base URL <span className="text-text-muted font-normal">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={settings.openai?.baseUrl ?? ''}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
openai: { ...prev.openai!, baseUrl: e.target.value }
|
||||
}))}
|
||||
placeholder="https://api.openai.com/v1 (default)"
|
||||
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"
|
||||
/>
|
||||
<p className="text-xs text-text-muted">
|
||||
Leave empty to use the default OpenAI API. Set a custom URL for proxies or compatible APIs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gemini Settings */}
|
||||
{settings.activeProvider === 'gemini' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
|
|
@ -276,7 +354,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Model selector or manual input */}
|
||||
{(useCustomModel || isCustomModelSelected) ? (
|
||||
<div className="space-y-2">
|
||||
|
|
@ -329,7 +407,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{modelFetchError && !useCustomModel && (
|
||||
<p className="text-xs text-amber-400 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
|
|
@ -339,7 +417,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Anthropic Settings */}
|
||||
{settings.activeProvider === 'anthropic' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
|
|
@ -379,10 +457,10 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
|
||||
|
||||
{(useCustomAnthropicModel || isCustomAnthropicModelSelected) ? (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
|
|
@ -549,7 +627,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
🔒
|
||||
</div>
|
||||
<div className="text-xs text-text-muted leading-relaxed">
|
||||
<span className="text-text-secondary font-medium">Privacy:</span> Your API keys are stored only in your browser's local storage.
|
||||
<span className="text-text-secondary font-medium">Privacy:</span> 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.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
||||
import { SystemMessage } from '@langchain/core/messages';
|
||||
import { AzureChatOpenAI } from '@langchain/openai';
|
||||
import { ChatOpenAI, AzureChatOpenAI } from '@langchain/openai';
|
||||
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { createGraphRAGTools } from './tools';
|
||||
import type {
|
||||
ProviderConfig,
|
||||
OpenAIConfig,
|
||||
AzureOpenAIConfig,
|
||||
GeminiConfig,
|
||||
AnthropicConfig,
|
||||
|
|
@ -109,6 +110,20 @@ GOOD: A["User Data"] --> B["Process and Save"]
|
|||
`;
|
||||
export const createChatModel = (config: ProviderConfig): BaseChatModel => {
|
||||
switch (config.provider) {
|
||||
case 'openai': {
|
||||
const openaiConfig = config as OpenAIConfig;
|
||||
return new ChatOpenAI({
|
||||
openAIApiKey: openaiConfig.apiKey,
|
||||
modelName: openaiConfig.model,
|
||||
temperature: openaiConfig.temperature ?? 0.1,
|
||||
maxTokens: openaiConfig.maxTokens,
|
||||
configuration: openaiConfig.baseUrl ? {
|
||||
baseURL: openaiConfig.baseUrl,
|
||||
} : undefined,
|
||||
streaming: true,
|
||||
});
|
||||
}
|
||||
|
||||
case 'azure-openai': {
|
||||
const azureConfig = config as AzureOpenAIConfig;
|
||||
return new AzureChatOpenAI({
|
||||
|
|
|
|||
|
|
@ -261,6 +261,8 @@ export const getProviderDisplayName = (provider: LLMProvider): string => {
|
|||
*/
|
||||
export const getAvailableModels = (provider: LLMProvider): string[] => {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return ['gpt-4.5-preview', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'];
|
||||
case 'azure-openai':
|
||||
// Azure models depend on deployment, so we show common ones
|
||||
return ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-35-turbo'];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue