diff --git a/apps/frontend/src/renderer/components/onboarding/MemoryStep.tsx b/apps/frontend/src/renderer/components/onboarding/MemoryStep.tsx index 0c792c6c..dacf92f8 100644 --- a/apps/frontend/src/renderer/components/onboarding/MemoryStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/MemoryStep.tsx @@ -4,9 +4,7 @@ import { Database, Info, Loader2, - Eye, - EyeOff, - ExternalLink, + ExternalLink } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; @@ -20,9 +18,11 @@ import { SelectTrigger, SelectValue } from '../ui/select'; -import { OllamaModelSelector } from './OllamaModelSelector'; +import { InfrastructureStatus } from '../project-settings/InfrastructureStatus'; +import { PasswordInput } from '../project-settings/PasswordInput'; import { useSettingsStore } from '../../stores/settings-store'; -import type { GraphitiEmbeddingProvider, AppSettings } from '../../../shared/types'; +import type { GraphitiEmbeddingProvider, AppSettings, InfrastructureStatus as InfrastructureStatusType } from '../../../shared/types'; +import { OllamaModelSelector } from './OllamaModelSelector'; interface MemoryStepProps { onNext: () => void; @@ -42,23 +42,26 @@ interface MemoryConfig { azureOpenaiEmbeddingDeployment: string; // Voyage voyageApiKey: string; + voyageEmbeddingModel: string; // Google googleApiKey: string; // Ollama + ollamaBaseUrl: string; ollamaEmbeddingModel: string; ollamaEmbeddingDim: number; } + + /** * Memory configuration step for the onboarding wizard. * - * Matches the settings page Memory section structure: + * Matches the settings page MemoryBackendSection structure: * - Enable Memory toggle (enabled by default) + * - Infrastructure Status * - Enable Agent Memory Access toggle * - Embedding Provider selection (Ollama default) * - Provider-specific configuration - * - * Note: LLM provider is not configurable - Claude SDK is used throughout. */ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { const { t } = useTranslation('onboarding'); @@ -75,24 +78,31 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { azureOpenaiBaseUrl: '', azureOpenaiEmbeddingDeployment: '', voyageApiKey: '', + voyageEmbeddingModel: settings.memoryVoyageEmbeddingModel || '', googleApiKey: settings.globalGoogleApiKey || '', - ollamaEmbeddingModel: 'qwen3-embedding:4b', - ollamaEmbeddingDim: 2560, + ollamaBaseUrl: settings.ollamaBaseUrl || 'http://localhost:11434', + ollamaEmbeddingModel: settings.memoryOllamaEmbeddingModel || 'qwen3-embedding:4b', + ollamaEmbeddingDim: settings.memoryOllamaEmbeddingDim ?? 2560, }); - const [showApiKey, setShowApiKey] = useState>({}); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); + const [infrastructureStatus, setInfrastructureStatus] = useState(null); const [isCheckingInfra, setIsCheckingInfra] = useState(true); + + // Check LadybugDB/Kuzu availability on mount useEffect(() => { const checkInfrastructure = async () => { setIsCheckingInfra(true); try { - await window.electronAPI.getMemoryInfrastructureStatus(); - } catch { - // Infrastructure will be created automatically when needed + const result = await window.electronAPI.getMemoryInfrastructureStatus(); + if (result.success && result.data) { + setInfrastructureStatus(result.data); + } + } catch (err) { + console.error('Failed to check infrastructure:', err); } finally { setIsCheckingInfra(false); } @@ -101,9 +111,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { checkInfrastructure(); }, []); - const toggleShowApiKey = (key: string) => { - setShowApiKey(prev => ({ ...prev, [key]: !prev[key] })); - }; + // Check if we have valid configuration const isConfigValid = (): boolean => { @@ -140,6 +148,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { // Core memory settings memoryEnabled: config.enabled, memoryEmbeddingProvider: config.embeddingProvider, + ollamaBaseUrl: config.ollamaBaseUrl || undefined, memoryOllamaEmbeddingModel: config.ollamaEmbeddingModel || undefined, memoryOllamaEmbeddingDim: config.ollamaEmbeddingDim || undefined, // Agent memory access (MCP) @@ -150,6 +159,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { globalGoogleApiKey: config.googleApiKey.trim() || undefined, // Provider-specific keys for memory memoryVoyageApiKey: config.voyageApiKey.trim() || undefined, + memoryVoyageEmbeddingModel: config.voyageEmbeddingModel.trim() || undefined, memoryAzureApiKey: config.azureOpenaiApiKey.trim() || undefined, memoryAzureBaseUrl: config.azureOpenaiBaseUrl.trim() || undefined, memoryAzureEmbeddingDeployment: config.azureOpenaiEmbeddingDeployment.trim() || undefined, @@ -162,6 +172,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { const storeUpdate: Partial = { memoryEnabled: config.enabled, memoryEmbeddingProvider: config.embeddingProvider, + ollamaBaseUrl: config.ollamaBaseUrl || undefined, memoryOllamaEmbeddingModel: config.ollamaEmbeddingModel || undefined, memoryOllamaEmbeddingDim: config.ollamaEmbeddingDim || undefined, graphitiMcpEnabled: config.agentMemoryEnabled, @@ -169,6 +180,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { globalOpenAIApiKey: config.openaiApiKey.trim() || undefined, globalGoogleApiKey: config.googleApiKey.trim() || undefined, memoryVoyageApiKey: config.voyageApiKey.trim() || undefined, + memoryVoyageEmbeddingModel: config.voyageEmbeddingModel.trim() || undefined, memoryAzureApiKey: config.azureOpenaiApiKey.trim() || undefined, memoryAzureBaseUrl: config.azureOpenaiBaseUrl.trim() || undefined, memoryAzureEmbeddingDeployment: config.azureOpenaiEmbeddingDeployment.trim() || undefined, @@ -185,178 +197,6 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { } }; - const handleOllamaModelSelect = (modelName: string, dim: number) => { - setConfig(prev => ({ - ...prev, - ollamaEmbeddingModel: modelName, - ollamaEmbeddingDim: dim, - })); - }; - - // Render provider-specific configuration fields - const renderProviderFields = () => { - const { embeddingProvider } = config; - - if (embeddingProvider === 'ollama') { - return ( -
- - -
- ); - } - - if (embeddingProvider === 'openai') { - return ( -
- -

{t('memory.openaiApiKeyDescription')}

-
- setConfig(prev => ({ ...prev, openaiApiKey: e.target.value }))} - placeholder="sk-..." - className="pr-10 font-mono text-sm" - disabled={isSaving} - /> - -
-

- {t('memory.openaiGetKey')}{' '} - - OpenAI - -

-
- ); - } - - if (embeddingProvider === 'voyage') { - return ( -
- -

{t('memory.voyageApiKeyDescription')}

-
- setConfig(prev => ({ ...prev, voyageApiKey: e.target.value }))} - placeholder="pa-..." - className="pr-10 font-mono text-sm" - disabled={isSaving} - /> - -
-

- {t('memory.openaiGetKey')}{' '} - - Voyage AI - -

-
- ); - } - - if (embeddingProvider === 'google') { - return ( -
- -

{t('memory.googleApiKeyDescription')}

-
- setConfig(prev => ({ ...prev, googleApiKey: e.target.value }))} - placeholder="AIza..." - className="pr-10 font-mono text-sm" - disabled={isSaving} - /> - -
-

- {t('memory.openaiGetKey')}{' '} - - Google AI Studio - -

-
- ); - } - - if (embeddingProvider === 'azure_openai') { - return ( -
- -
- -
- setConfig(prev => ({ ...prev, azureOpenaiApiKey: e.target.value }))} - placeholder="Azure API Key" - className="pr-10 font-mono text-sm" - disabled={isSaving} - /> - -
-
-
- - setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))} - className="font-mono text-sm" - disabled={isSaving} - /> -
-
- - setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))} - className="font-mono text-sm" - disabled={isSaving} - /> -
-
- ); - } - - return null; - }; - return (
@@ -425,6 +265,12 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { {/* Memory Enabled Configuration */} {config.enabled && ( <> + {/* Infrastructure Status */} + + {/* Agent Memory Access Toggle */}
@@ -473,7 +319,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) { disabled={isSaving} > - + {t('memory.providers.ollama')} @@ -486,7 +332,146 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
{/* Provider-specific fields */} - {renderProviderFields()} + {/* OpenAI */} + {config.embeddingProvider === 'openai' && ( +
+ +

+ {t('memory.openaiApiKeyDescription')} +

+ setConfig(prev => ({ ...prev, openaiApiKey: value }))} + placeholder="sk-..." + /> +

+ {t('memory.openaiGetKey')}{' '} + + OpenAI + +

+
+ )} + + {/* Voyage AI */} + {config.embeddingProvider === 'voyage' && ( +
+ +

+ {t('memory.voyageApiKeyDescription')} +

+ setConfig(prev => ({ ...prev, voyageApiKey: value }))} + placeholder="pa-..." + /> +
+ + setConfig(prev => ({ ...prev, voyageEmbeddingModel: e.target.value }))} + /> +
+

+ {t('memory.openaiGetKey')}{' '} + + Voyage AI + +

+
+ )} + + {/* Google AI */} + {config.embeddingProvider === 'google' && ( +
+ +

+ {t('memory.googleApiKeyDescription')} +

+ setConfig(prev => ({ ...prev, googleApiKey: value }))} + placeholder="AIza..." + /> +

+ {t('memory.openaiGetKey')}{' '} + + Google AI Studio + +

+
+ )} + + {/* Azure OpenAI */} + {config.embeddingProvider === 'azure_openai' && ( +
+ +
+ + setConfig(prev => ({ ...prev, azureOpenaiApiKey: value }))} + placeholder="Azure API Key" + /> +
+
+ + setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))} + className="font-mono text-sm" + disabled={isSaving} + /> +
+
+ + setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))} + className="font-mono text-sm" + disabled={isSaving} + /> +
+
+ )} + + {/* Ollama (Local) */} + {/* Ollama (Local) */} + {config.embeddingProvider === 'ollama' && ( +
+
+ +
+ +
+ + setConfig(prev => ({ ...prev, ollamaBaseUrl: e.target.value }))} + /> +
+ +
+ + { + setConfig(prev => ({ + ...prev, + ollamaEmbeddingModel: model, + ollamaEmbeddingDim: dim + })); + }} + disabled={isSaving} + /> +
+
+ )} {/* Info about Learn More */}
diff --git a/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx b/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx index 7b71f54c..be2331a0 100644 --- a/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx @@ -28,6 +28,7 @@ interface OllamaModelSelectorProps { onModelSelect: (model: string, dim: number) => void; disabled?: boolean; className?: string; + baseUrl?: string; } // Recommended embedding models for Auto Claude Memory @@ -73,7 +74,6 @@ const RECOMMENDED_MODELS: OllamaModel[] = [ }, ]; - /** * OllamaModelSelector Component * @@ -107,6 +107,7 @@ export function OllamaModelSelector({ onModelSelect, disabled = false, className, + baseUrl, }: OllamaModelSelectorProps) { const { t } = useTranslation('onboarding'); const [models, setModels] = useState(RECOMMENDED_MODELS); @@ -149,7 +150,7 @@ export function OllamaModelSelector({ } // Ollama is installed, now check if it's running - const statusResult = await window.electronAPI.checkOllamaStatus(); + const statusResult = await window.electronAPI.checkOllamaStatus(baseUrl); if (abortSignal?.aborted) return; if (!statusResult?.success || !statusResult?.data?.running) { @@ -161,7 +162,7 @@ export function OllamaModelSelector({ setOllamaState('available'); // Get list of installed embedding models - const result = await window.electronAPI.listOllamaEmbeddingModels(); + const result = await window.electronAPI.listOllamaEmbeddingModels(baseUrl); if (abortSignal?.aborted) return; if (result?.success && result?.data?.embedding_models) { @@ -258,7 +259,7 @@ export function OllamaModelSelector({ clearTimeout(installCheckTimeoutRef.current); } }; - }, []); + }, [baseUrl]); // Progress is now handled globally by the download store listener initialized in App.tsx diff --git a/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx b/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx index 5eb00c07..8f36ce0f 100644 --- a/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx @@ -17,7 +17,7 @@ import { OAuthStep } from './OAuthStep'; import { ClaudeCodeStep } from './ClaudeCodeStep'; import { DevToolsStep } from './DevToolsStep'; import { PrivacyStep } from './PrivacyStep'; -import { GraphitiStep } from './GraphitiStep'; +import { MemoryStep } from './MemoryStep'; import { CompletionStep } from './CompletionStep'; import { useSettingsStore } from '../../stores/settings-store'; @@ -29,7 +29,7 @@ interface OnboardingWizardProps { } // Wizard step identifiers -type WizardStepId = 'welcome' | 'auth-choice' | 'oauth' | 'claude-code' | 'devtools' | 'privacy' | 'graphiti' | 'completion'; +type WizardStepId = 'welcome' | 'auth-choice' | 'oauth' | 'claude-code' | 'devtools' | 'privacy' | 'memory' | 'completion'; // Step configuration with translation keys const WIZARD_STEPS: { id: WizardStepId; labelKey: string }[] = [ @@ -39,7 +39,7 @@ const WIZARD_STEPS: { id: WizardStepId; labelKey: string }[] = [ { id: 'claude-code', labelKey: 'steps.claudeCode' }, { id: 'devtools', labelKey: 'steps.devtools' }, { id: 'privacy', labelKey: 'steps.privacy' }, - { id: 'graphiti', labelKey: 'steps.memory' }, + { id: 'memory', labelKey: 'steps.memory' }, { id: 'completion', labelKey: 'steps.done' } ]; @@ -93,8 +93,8 @@ export function OnboardingWizard({ }, [currentStepIndex, currentStepId]); const goToPreviousStep = useCallback(() => { - // If going back from graphiti and oauth was bypassed, go back to auth-choice (skip oauth) - if (currentStepId === 'graphiti' && oauthBypassed) { + // If going back from memory and oauth was bypassed, go back to auth-choice (skip oauth) + if (currentStepId === 'memory' && oauthBypassed) { // Find index of auth-choice step const authChoiceIndex = WIZARD_STEPS.findIndex(step => step.id === 'auth-choice'); setCurrentStepIndex(authChoiceIndex); @@ -108,13 +108,13 @@ export function OnboardingWizard({ }, [currentStepIndex, currentStepId, oauthBypassed]); // Handler for when API key path is chosen - skips oauth step - const handleSkipToGraphiti = useCallback(() => { + const handleSkipToMemory = useCallback(() => { setOauthBypassed(true); setCompletedSteps(prev => new Set(prev).add('auth-choice')); - // Find index of graphiti step - const graphitiIndex = WIZARD_STEPS.findIndex(step => step.id === 'graphiti'); - setCurrentStepIndex(graphitiIndex); + // Find index of memory step + const memoryIndex = WIZARD_STEPS.findIndex(step => step.id === 'memory'); + setCurrentStepIndex(memoryIndex); }, []); // Reset wizard state (for re-running) - defined before skipWizard/finishWizard that use it @@ -124,7 +124,7 @@ export function OnboardingWizard({ setOauthBypassed(false); }, []); - const skipWizard = useCallback(async () => { + const completeWizard = useCallback(async () => { // Mark onboarding as completed and close - save to disk AND update local state try { const result = await window.electronAPI.saveSettings({ onboardingCompleted: true }); @@ -139,21 +139,6 @@ export function OnboardingWizard({ resetWizard(); }, [updateSettings, onOpenChange, resetWizard]); - const finishWizard = useCallback(async () => { - // Mark onboarding as completed - save to disk AND update local state - try { - const result = await window.electronAPI.saveSettings({ onboardingCompleted: true }); - if (!result?.success) { - console.error('Failed to save onboarding completion:', result?.error); - } - } catch (err) { - console.error('Error saving onboarding completion:', err); - } - updateSettings({ onboardingCompleted: true }); - onOpenChange(false); - resetWizard(); - }, [updateSettings, onOpenChange, resetWizard]); - // Handle opening task creator from within wizard const handleOpenTaskCreator = useCallback(() => { if (onOpenTaskCreator) { @@ -167,10 +152,10 @@ export function OnboardingWizard({ const handleOpenSettings = useCallback(() => { if (onOpenSettings) { // Finish wizard first, then open settings - finishWizard(); + completeWizard(); onOpenSettings(); } - }, [onOpenSettings, finishWizard]); + }, [onOpenSettings, completeWizard]); // Render current step content const renderStepContent = () => { @@ -179,7 +164,7 @@ export function OnboardingWizard({ return ( ); case 'auth-choice': @@ -187,8 +172,8 @@ export function OnboardingWizard({ ); case 'oauth': @@ -196,7 +181,7 @@ export function OnboardingWizard({ ); case 'claude-code': @@ -204,7 +189,7 @@ export function OnboardingWizard({ ); case 'devtools': @@ -221,18 +206,17 @@ export function OnboardingWizard({ onBack={goToPreviousStep} /> ); - case 'graphiti': + case 'memory': return ( - ); case 'completion': return ( @@ -246,11 +230,11 @@ export function OnboardingWizard({ const handleOpenChange = useCallback((newOpen: boolean) => { if (!newOpen) { // If closing before completion, skip the wizard - skipWizard(); + completeWizard(); } else { onOpenChange(newOpen); } - }, [skipWizard, onOpenChange]); + }, [completeWizard, onOpenChange]); return ( diff --git a/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx b/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx index 2e762348..f0b477e1 100644 --- a/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx @@ -300,12 +300,30 @@ export function SecuritySettings({ // Ollama (Local) - uses OllamaModelSelector component if (embeddingProvider === 'ollama') { return ( -
- - +
+
+ + updateEnvConfig({ + graphitiProviderConfig: { + ...envConfig.graphitiProviderConfig, + embeddingProvider: 'ollama', + ollamaBaseUrl: e.target.value, + } + })} + /> +
+ +
+ + +
); } diff --git a/apps/frontend/src/shared/i18n/locales/en/onboarding.json b/apps/frontend/src/shared/i18n/locales/en/onboarding.json index 7da4d386..d76bba73 100644 --- a/apps/frontend/src/shared/i18n/locales/en/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/en/onboarding.json @@ -100,6 +100,7 @@ "openaiGetKey": "Get your key from", "voyageApiKey": "Voyage AI API Key", "voyageApiKeyDescription": "Required for Voyage AI embeddings", + "voyageEmbeddingModel": "Embedding Model", "googleApiKey": "Google AI API Key", "googleApiKeyDescription": "Required for Google AI embeddings", "azureConfig": "Azure OpenAI Configuration", @@ -118,7 +119,16 @@ "voyage": "Voyage AI", "google": "Google AI", "azure": "Azure OpenAI" - } + }, + "ollamaConfig": "Ollama Configuration", + "checking": "Checking...", + "connected": "Connected", + "notRunning": "Not running", + "baseUrl": "Base URL", + "embeddingModel": "Embedding Model", + "embeddingDim": "Embedding Dimension", + "embeddingDimDescription": "Required for Ollama embeddings (e.g., 768 for nomic-embed-text)", + "modelRecommendation": "Recommended: qwen3-embedding:4b (balanced), :8b (quality), :0.6b (fast)" }, "completion": { "title": "You're All Set!", diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index 6acf1aaa..f165b8e7 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -245,6 +245,7 @@ export interface AppSettings { memoryOllamaEmbeddingModel?: string; memoryOllamaEmbeddingDim?: number; memoryVoyageApiKey?: string; + memoryVoyageEmbeddingModel?: string; memoryAzureApiKey?: string; memoryAzureBaseUrl?: string; memoryAzureEmbeddingDeployment?: string;