fix(onboarding): align MemoryStep layout with Settings MemoryBackendSection (#1445)
* fix(onboarding): align MemoryStep layout with Settings MemoryBackendSection * fix(onboarding): address PR feedback - restore ollama installer, fix i18n, add persistence * refactor(onboarding): cleanup unused ollama state and error UI * fix(onboarding): restore i18n support and jsdoc * fix(pr): resolve code duplication and add missing ollama config * fix(pr): resolve code rabbit findings (unused imports, i18n, state init) * fix(onboarding): initialize ollama settings from saved values Initialize ollamaEmbeddingModel and ollamaEmbeddingDim from settings instead of hardcoding defaults. This prevents overwriting user's saved configuration when re-running the onboarding wizard. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
@@ -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<Record<string, boolean>>({});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [infrastructureStatus, setInfrastructureStatus] = useState<InfrastructureStatusType | null>(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<AppSettings> = {
|
||||
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 (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.selectEmbeddingModel')}</Label>
|
||||
<OllamaModelSelector
|
||||
selectedModel={config.ollamaEmbeddingModel}
|
||||
onModelSelect={handleOllamaModelSelect}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (embeddingProvider === 'openai') {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.openaiApiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('memory.openaiApiKeyDescription')}</p>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey['openai'] ? 'text' : 'password'}
|
||||
value={config.openaiApiKey}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, openaiApiKey: e.target.value }))}
|
||||
placeholder="sk-..."
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowApiKey('openai')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey['openai'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.openaiGetKey')}{' '}
|
||||
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||
OpenAI
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (embeddingProvider === 'voyage') {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.voyageApiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('memory.voyageApiKeyDescription')}</p>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey['voyage'] ? 'text' : 'password'}
|
||||
value={config.voyageApiKey}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, voyageApiKey: e.target.value }))}
|
||||
placeholder="pa-..."
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowApiKey('voyage')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey['voyage'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.openaiGetKey')}{' '}
|
||||
<a href="https://dash.voyageai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||
Voyage AI
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (embeddingProvider === 'google') {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.googleApiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('memory.googleApiKeyDescription')}</p>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey['google'] ? 'text' : 'password'}
|
||||
value={config.googleApiKey}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, googleApiKey: e.target.value }))}
|
||||
placeholder="AIza..."
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowApiKey('google')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey['google'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.openaiGetKey')}{' '}
|
||||
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||
Google AI Studio
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (embeddingProvider === 'azure_openai') {
|
||||
return (
|
||||
<div className="space-y-3 p-3 rounded-md bg-muted/50">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.azureConfig')}</Label>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.azureApiKey')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey['azure'] ? 'text' : 'password'}
|
||||
value={config.azureOpenaiApiKey}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiApiKey: e.target.value }))}
|
||||
placeholder="Azure API Key"
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowApiKey('azure')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey['azure'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.azureBaseUrl')}</Label>
|
||||
<Input
|
||||
placeholder="https://your-resource.openai.azure.com"
|
||||
value={config.azureOpenaiBaseUrl}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))}
|
||||
className="font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.azureEmbeddingDeployment')}</Label>
|
||||
<Input
|
||||
placeholder="text-embedding-ada-002"
|
||||
value={config.azureOpenaiEmbeddingDeployment}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))}
|
||||
className="font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center px-8 py-6">
|
||||
<div className="w-full max-w-2xl">
|
||||
@@ -425,6 +265,12 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
|
||||
{/* Memory Enabled Configuration */}
|
||||
{config.enabled && (
|
||||
<>
|
||||
{/* Infrastructure Status */}
|
||||
<InfrastructureStatus
|
||||
infrastructureStatus={infrastructureStatus}
|
||||
isCheckingInfrastructure={isCheckingInfra}
|
||||
/>
|
||||
|
||||
{/* Agent Memory Access Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
@@ -473,7 +319,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
|
||||
disabled={isSaving}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('memory.embeddingProvider')} />
|
||||
<SelectValue placeholder={t('memory.selectEmbeddingModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ollama">{t('memory.providers.ollama')}</SelectItem>
|
||||
@@ -486,7 +332,146 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
|
||||
</div>
|
||||
|
||||
{/* Provider-specific fields */}
|
||||
{renderProviderFields()}
|
||||
{/* OpenAI */}
|
||||
{config.embeddingProvider === 'openai' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.openaiApiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.openaiApiKeyDescription')}
|
||||
</p>
|
||||
<PasswordInput
|
||||
value={config.openaiApiKey}
|
||||
onChange={(value) => setConfig(prev => ({ ...prev, openaiApiKey: value }))}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.openaiGetKey')}{' '}
|
||||
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||
OpenAI
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Voyage AI */}
|
||||
{config.embeddingProvider === 'voyage' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.voyageApiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.voyageApiKeyDescription')}
|
||||
</p>
|
||||
<PasswordInput
|
||||
value={config.voyageApiKey}
|
||||
onChange={(value) => setConfig(prev => ({ ...prev, voyageApiKey: value }))}
|
||||
placeholder="pa-..."
|
||||
/>
|
||||
<div className="space-y-1 mt-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.embeddingModel')}</Label>
|
||||
<Input
|
||||
placeholder="voyage-3"
|
||||
value={config.voyageEmbeddingModel}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, voyageEmbeddingModel: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('memory.openaiGetKey')}{' '}
|
||||
<a href="https://dash.voyageai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||
Voyage AI
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Google AI */}
|
||||
{config.embeddingProvider === 'google' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.googleApiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.googleApiKeyDescription')}
|
||||
</p>
|
||||
<PasswordInput
|
||||
value={config.googleApiKey}
|
||||
onChange={(value) => setConfig(prev => ({ ...prev, googleApiKey: value }))}
|
||||
placeholder="AIza..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('memory.openaiGetKey')}{' '}
|
||||
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||
Google AI Studio
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Azure OpenAI */}
|
||||
{config.embeddingProvider === 'azure_openai' && (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.azureConfig')}</Label>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.azureApiKey')}</Label>
|
||||
<PasswordInput
|
||||
value={config.azureOpenaiApiKey}
|
||||
onChange={(value) => setConfig(prev => ({ ...prev, azureOpenaiApiKey: value }))}
|
||||
placeholder="Azure API Key"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.azureBaseUrl')}</Label>
|
||||
<Input
|
||||
placeholder="https://your-resource.openai.azure.com"
|
||||
value={config.azureOpenaiBaseUrl}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))}
|
||||
className="font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.azureEmbeddingDeployment')}</Label>
|
||||
<Input
|
||||
placeholder="text-embedding-ada-002"
|
||||
value={config.azureOpenaiEmbeddingDeployment}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))}
|
||||
className="font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ollama (Local) */}
|
||||
{/* Ollama (Local) */}
|
||||
{config.embeddingProvider === 'ollama' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium text-foreground">{t('memory.ollamaConfig')}</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.baseUrl')}</Label>
|
||||
<Input
|
||||
placeholder="http://localhost:11434"
|
||||
value={config.ollamaBaseUrl}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, ollamaBaseUrl: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('memory.embeddingModel')}</Label>
|
||||
<OllamaModelSelector
|
||||
selectedModel={config.ollamaEmbeddingModel}
|
||||
baseUrl={config.ollamaBaseUrl}
|
||||
onModelSelect={(model, dim) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
ollamaEmbeddingModel: model,
|
||||
ollamaEmbeddingDim: dim
|
||||
}));
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info about Learn More */}
|
||||
<div className="rounded-lg border border-info/30 bg-info/10 p-4">
|
||||
|
||||
@@ -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<OllamaModel[]>(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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<WelcomeStep
|
||||
onGetStarted={goToNextStep}
|
||||
onSkip={skipWizard}
|
||||
onSkip={completeWizard}
|
||||
/>
|
||||
);
|
||||
case 'auth-choice':
|
||||
@@ -187,8 +172,8 @@ export function OnboardingWizard({
|
||||
<AuthChoiceStep
|
||||
onNext={goToNextStep}
|
||||
onBack={goToPreviousStep}
|
||||
onSkip={skipWizard}
|
||||
onAPIKeyPathComplete={handleSkipToGraphiti}
|
||||
onSkip={completeWizard}
|
||||
onAPIKeyPathComplete={handleSkipToMemory}
|
||||
/>
|
||||
);
|
||||
case 'oauth':
|
||||
@@ -196,7 +181,7 @@ export function OnboardingWizard({
|
||||
<OAuthStep
|
||||
onNext={goToNextStep}
|
||||
onBack={goToPreviousStep}
|
||||
onSkip={skipWizard}
|
||||
onSkip={completeWizard}
|
||||
/>
|
||||
);
|
||||
case 'claude-code':
|
||||
@@ -204,7 +189,7 @@ export function OnboardingWizard({
|
||||
<ClaudeCodeStep
|
||||
onNext={goToNextStep}
|
||||
onBack={goToPreviousStep}
|
||||
onSkip={skipWizard}
|
||||
onSkip={completeWizard}
|
||||
/>
|
||||
);
|
||||
case 'devtools':
|
||||
@@ -221,18 +206,17 @@ export function OnboardingWizard({
|
||||
onBack={goToPreviousStep}
|
||||
/>
|
||||
);
|
||||
case 'graphiti':
|
||||
case 'memory':
|
||||
return (
|
||||
<GraphitiStep
|
||||
<MemoryStep
|
||||
onNext={goToNextStep}
|
||||
onBack={goToPreviousStep}
|
||||
onSkip={skipWizard}
|
||||
/>
|
||||
);
|
||||
case 'completion':
|
||||
return (
|
||||
<CompletionStep
|
||||
onFinish={finishWizard}
|
||||
onFinish={completeWizard}
|
||||
onOpenTaskCreator={handleOpenTaskCreator}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
/>
|
||||
@@ -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 (
|
||||
<FullScreenDialog open={open} onOpenChange={handleOpenChange}>
|
||||
|
||||
@@ -300,12 +300,30 @@ export function SecuritySettings({
|
||||
// Ollama (Local) - uses OllamaModelSelector component
|
||||
if (embeddingProvider === 'ollama') {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-foreground">Select Embedding Model</Label>
|
||||
<OllamaModelSelector
|
||||
selectedModel={envConfig.graphitiProviderConfig?.ollamaEmbeddingModel || ''}
|
||||
onModelSelect={handleOllamaModelSelect}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">Base URL</Label>
|
||||
<Input
|
||||
placeholder="http://localhost:11434"
|
||||
value={envConfig.graphitiProviderConfig?.ollamaBaseUrl || 'http://localhost:11434'}
|
||||
onChange={(e) => updateEnvConfig({
|
||||
graphitiProviderConfig: {
|
||||
...envConfig.graphitiProviderConfig,
|
||||
embeddingProvider: 'ollama',
|
||||
ollamaBaseUrl: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">Select Embedding Model</Label>
|
||||
<OllamaModelSelector
|
||||
selectedModel={envConfig.graphitiProviderConfig?.ollamaEmbeddingModel || ''}
|
||||
baseUrl={envConfig.graphitiProviderConfig?.ollamaBaseUrl}
|
||||
onModelSelect={handleOllamaModelSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -245,6 +245,7 @@ export interface AppSettings {
|
||||
memoryOllamaEmbeddingModel?: string;
|
||||
memoryOllamaEmbeddingDim?: number;
|
||||
memoryVoyageApiKey?: string;
|
||||
memoryVoyageEmbeddingModel?: string;
|
||||
memoryAzureApiKey?: string;
|
||||
memoryAzureBaseUrl?: string;
|
||||
memoryAzureEmbeddingDeployment?: string;
|
||||
|
||||
Reference in New Issue
Block a user