From 8cd34c8aaaa366166460abe0ca84f290985fbab9 Mon Sep 17 00:00:00 2001 From: Jair Goh <42672930+lilozz2@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:28:18 +0800 Subject: [PATCH] feat(assistant): Add embedding model option to AIPanel (#3090) * Add embedding model option to AIPanel * Minor code reformatting * Edit tests to account for embedding model * Minor reformatting --- .../src/__tests__/ai/providers.test.ts | 20 +++++-- .../src/components/settings/AIPanel.tsx | 57 ++++++++++++++----- .../services/ai/providers/OllamaProvider.ts | 6 +- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/apps/readest-app/src/__tests__/ai/providers.test.ts b/apps/readest-app/src/__tests__/ai/providers.test.ts index 5ab30a00..220906b8 100644 --- a/apps/readest-app/src/__tests__/ai/providers.test.ts +++ b/apps/readest-app/src/__tests__/ai/providers.test.ts @@ -65,9 +65,15 @@ describe('OllamaProvider', () => { test('healthCheck should verify model exists', async () => { mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve({ models: [{ name: 'llama3.2:latest' }] }), + json: () => + Promise.resolve({ models: [{ name: 'llama3.2:latest' }, { name: 'nomic-embed:latest' }] }), }); - const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true, ollamaModel: 'llama3.2' }; + const settings: AISettings = { + ...DEFAULT_AI_SETTINGS, + enabled: true, + ollamaModel: 'llama3.2', + ollamaEmbeddingModel: 'nomic-embed', + }; const provider = new OllamaProvider(settings); const result = await provider.healthCheck(); @@ -77,9 +83,15 @@ describe('OllamaProvider', () => { test('healthCheck should return false if model not found', async () => { mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve({ models: [{ name: 'other-model' }] }), + json: () => + Promise.resolve({ models: [{ name: 'other-model' }, { name: 'nomic-embed:latest' }] }), }); - const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true, ollamaModel: 'llama3.2' }; + const settings: AISettings = { + ...DEFAULT_AI_SETTINGS, + enabled: true, + ollamaModel: 'llama3.2', + ollamaEmbeddingModel: 'nomic-embed', + }; const provider = new OllamaProvider(settings); const result = await provider.healthCheck(); diff --git a/apps/readest-app/src/components/settings/AIPanel.tsx b/apps/readest-app/src/components/settings/AIPanel.tsx index e78cf384..51a595a8 100644 --- a/apps/readest-app/src/components/settings/AIPanel.tsx +++ b/apps/readest-app/src/components/settings/AIPanel.tsx @@ -71,6 +71,7 @@ const AIPanel: React.FC = () => { const [provider, setProvider] = useState(aiSettings.provider); const [ollamaUrl, setOllamaUrl] = useState(aiSettings.ollamaBaseUrl); const [ollamaModel, setOllamaModel] = useState(aiSettings.ollamaModel); + const [ollamaEmbeddingModel, setOllamaEmbeddingModel] = useState(aiSettings.ollamaEmbeddingModel); const [ollamaModels, setOllamaModels] = useState([]); const [fetchingModels, setFetchingModels] = useState(false); const [gatewayKey, setGatewayKey] = useState(aiSettings.aiGatewayApiKey ?? ''); @@ -181,6 +182,14 @@ const AIPanel: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [ollamaModel]); + useEffect(() => { + if (!isMounted.current) return; + if (ollamaEmbeddingModel !== aiSettings.ollamaEmbeddingModel) { + saveAiSetting('ollamaEmbeddingModel', ollamaEmbeddingModel); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ollamaEmbeddingModel]); + useEffect(() => { if (!isMounted.current) return; if (gatewayKey !== (aiSettings.aiGatewayApiKey ?? '')) { @@ -276,6 +285,7 @@ const AIPanel: React.FC = () => { provider, ollamaBaseUrl: ollamaUrl, ollamaModel, + ollamaEmbeddingModel, aiGatewayApiKey: gatewayKey, aiGatewayModel: effectiveModel, }; @@ -375,21 +385,38 @@ const AIPanel: React.FC = () => { /> {ollamaModels.length > 0 ? ( -
- {_('AI Model')} - -
+ <> +
+ {_('AI Model')} + +
+
+ {_('Embedding Model')} + +
+ ) : !fetchingModels ? (
{_('No models detected')} diff --git a/apps/readest-app/src/services/ai/providers/OllamaProvider.ts b/apps/readest-app/src/services/ai/providers/OllamaProvider.ts index 6f2d370e..4514c7c2 100644 --- a/apps/readest-app/src/services/ai/providers/OllamaProvider.ts +++ b/apps/readest-app/src/services/ai/providers/OllamaProvider.ts @@ -53,7 +53,11 @@ export class OllamaProvider implements AIProvider { if (!response.ok) return false; const data = await response.json(); const modelName = this.settings.ollamaModel?.split(':')[0] ?? ''; - return data.models?.some((m: { name: string }) => m.name.includes(modelName)); + const embeddingModelName = this.settings.ollamaEmbeddingModel?.split(':')[0] ?? ''; + return ( + data.models?.some((m: { name: string }) => m.name.includes(modelName)) && + data.models?.some((m: { name: string }) => m.name.includes(embeddingModelName)) + ); } catch (e) { aiLogger.provider.error('ollama', (e as Error).message); return false;