diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 12ab765c3..1fe99cacd 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -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
-
+
{/* Search Results Dropdown */}
{isSearchOpen && searchQuery.trim() && (
@@ -154,14 +154,13 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
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 */}
-
@@ -208,7 +207,11 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
{/* Icon buttons */}
-
+ 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"
+ >
@@ -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'
}
`}
diff --git a/src/components/RightPanel.tsx b/src/components/RightPanel.tsx
index 5c2e18604..ff8ea0d11 100644
--- a/src/components/RightPanel.tsx
+++ b/src/components/RightPanel.tsx
@@ -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 = () => {
Connecting
)}
- setSettingsPanelOpen(true)}
- className="p-1.5 rounded-md text-text-muted hover:text-text-primary hover:bg-hover transition-colors"
- title="AI Settings"
- >
-
-
diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx
index dc11630bc..25e8e266c 100644
--- a/src/components/SettingsPanel.tsx
+++ b/src/components/SettingsPanel.tsx
@@ -22,13 +22,13 @@ const fetchGeminiModels = async (apiKey: string): Promise => {
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 => {
};
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(loadSettings);
const [showApiKey, setShowApiKey] = useState>({});
const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle');
-
+
// Gemini model fetching state
const [geminiModels, setGeminiModels] = useState([]);
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' ? '🧠' : '☁️'}
{getProviderDisplayName(provider)}
@@ -209,6 +209,84 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
+ {/* OpenAI Settings */}
+ {settings.activeProvider === 'openai' && (
+
+
+
+
+ API Key
+
+
+ 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"
+ />
+ 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'] ? : }
+
+
+
+ Get your API key from{' '}
+
+ OpenAI Platform
+
+
+
+
+
+ Model
+ 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 => (
+ {model}
+ ))}
+
+
+
+
+
+
+ Base URL (optional)
+
+
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"
+ />
+
+ Leave empty to use the default OpenAI API. Set a custom URL for proxies or compatible APIs.
+
+
+
+ )}
+
{/* Gemini Settings */}
{settings.activeProvider === 'gemini' && (
@@ -276,7 +354,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
)}
-
+
{/* Model selector or manual input */}
{(useCustomModel || isCustomModelSelected) ? (
@@ -329,7 +407,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
)}
-
+
{modelFetchError && !useCustomModel && (
@@ -339,7 +417,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
)}
-
+
{/* Anthropic Settings */}
{settings.activeProvider === 'anthropic' && (
@@ -379,10 +457,10 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
-
+
Model
-
+
{(useCustomAnthropicModel || isCustomAnthropicModelSelected) ? (
diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts
index f54a15c43..dcb43278a 100644
--- a/src/core/llm/agent.ts
+++ b/src/core/llm/agent.ts
@@ -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({
diff --git a/src/core/llm/settings-service.ts b/src/core/llm/settings-service.ts
index a0218aae6..333463b95 100644
--- a/src/core/llm/settings-service.ts
+++ b/src/core/llm/settings-service.ts
@@ -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'];