added embedding model selection
This commit is contained in:
96
src/App.jsx
96
src/App.jsx
@@ -127,6 +127,7 @@ const COLOR_SCHEME_KEY = 'colorScheme';
|
||||
const WEBSEARCH_URL_KEY = 'websearch.searxUrl';
|
||||
const WEBSEARCH_ENGINES_KEY = 'websearch.engines';
|
||||
const CHAT_LIBRARY_MAP_KEY = 'chat.libraryBySession';
|
||||
const DEFAULT_SEARX_URL = 'http://127.0.0.1:8888';
|
||||
|
||||
// Initial API value will be set by useEffect after settings are loaded
|
||||
let API = import.meta.env.VITE_API_URL ?? 'http://127.0.0.1:8000';
|
||||
@@ -137,6 +138,13 @@ function resolveBackendApiUrl(settings) {
|
||||
return settings.backendApiUrl || settings.ollamaApiUrl || API;
|
||||
}
|
||||
|
||||
function migrateLegacySearxUrl(value) {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (!trimmed) return DEFAULT_SEARX_URL;
|
||||
if (trimmed === 'http://localhost:8888') return DEFAULT_SEARX_URL;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [chatSessions, setChatSessions] = useState([])
|
||||
const [activeSessionId, setActiveSessionId] = useState(null)
|
||||
@@ -169,7 +177,8 @@ export default function App() {
|
||||
const [backendApiUrl, setBackendApiUrl] = useState(API); // State for Heimgeist backend URL
|
||||
const [colorScheme, setColorScheme] = useState('Default'); // State for color scheme
|
||||
const [streamOutput, setStreamOutput] = useState(false);
|
||||
const [searxUrl, setSearxUrl] = useState(localStorage.getItem(WEBSEARCH_URL_KEY) || 'http://localhost:8888');
|
||||
const [startupTaskMessage, setStartupTaskMessage] = useState('');
|
||||
const [searxUrl, setSearxUrl] = useState(() => migrateLegacySearxUrl(localStorage.getItem(WEBSEARCH_URL_KEY)));
|
||||
const [searxEngines, setSearxEngines] = useState(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(WEBSEARCH_ENGINES_KEY);
|
||||
@@ -191,6 +200,8 @@ export default function App() {
|
||||
const [loading, setLoading] = useState(true); // Loading state for initial session fetch
|
||||
const [unreadSessions, setUnreadSessions] = useState([]); // Track unread messages
|
||||
const [scrollPositions, setScrollPositions] = useState({}); // Store scroll positions for each session
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||
const startupOllamaCheckRanRef = useRef(false);
|
||||
// Editing state for user messages
|
||||
const [editingMessageIndex, setEditingMessageIndex] = useState(null);
|
||||
const [editText, setEditText] = useState('');
|
||||
@@ -253,6 +264,20 @@ export default function App() {
|
||||
return String(error)
|
||||
}
|
||||
|
||||
async function expectBackendJson(response) {
|
||||
const data = await response.json().catch(() => null)
|
||||
if (response.ok) return data
|
||||
const detail = typeof data?.detail === 'string'
|
||||
? data.detail
|
||||
: (typeof data?.message === 'string' ? data.message : '')
|
||||
throw new Error(detail || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
async function fetchStartupOllamaStatus() {
|
||||
const response = await fetch(`${backendApiUrl}/ollama/startup-status`)
|
||||
return expectBackendJson(response)
|
||||
}
|
||||
|
||||
async function fetchLocalLibraryContext(slug, prompt, signal) {
|
||||
if (!slug) return { contextBlock: null, sources: [] }
|
||||
|
||||
@@ -705,6 +730,8 @@ async function regenerateFromIndex(index, overrideUserText = null) {
|
||||
setStreamOutput(settings.streamOutput || false);
|
||||
setScrollPositions(settings.scrollPositions || {}); // Load scroll positions
|
||||
applyColorScheme(settings.colorScheme || 'Default'); // Apply initial scheme
|
||||
}).finally(() => {
|
||||
setSettingsLoaded(true);
|
||||
});
|
||||
|
||||
const handleFocus = () => {
|
||||
@@ -724,6 +751,68 @@ async function regenerateFromIndex(index, overrideUserText = null) {
|
||||
};
|
||||
}, [activeSidebarMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded || !backendApiUrl || startupOllamaCheckRanRef.current) return
|
||||
startupOllamaCheckRanRef.current = true
|
||||
|
||||
let cancelled = false
|
||||
|
||||
;(async () => {
|
||||
let actionStarted = false
|
||||
try {
|
||||
let status = await fetchStartupOllamaStatus()
|
||||
if (cancelled) return
|
||||
|
||||
if (!status?.ollama_running && status?.can_manage_locally) {
|
||||
const confirmed = window.confirm(
|
||||
`Ollama is not running at ${status.ollama_url}. Start it in the background now with "ollama serve"?`
|
||||
)
|
||||
if (cancelled) return
|
||||
if (confirmed) {
|
||||
actionStarted = true
|
||||
setStartupTaskMessage('Starting Ollama in the background...')
|
||||
const response = await fetch(`${backendApiUrl}/ollama/start`, { method: 'POST' })
|
||||
status = await expectBackendJson(response)
|
||||
if (cancelled) return
|
||||
}
|
||||
}
|
||||
|
||||
if (status?.ollama_running && !status?.embedding_model_available && status?.can_manage_locally) {
|
||||
const confirmed = window.confirm(
|
||||
`The selected embedding model "${status.selected_embed_model}" is not installed in Ollama. Pull it now?`
|
||||
)
|
||||
if (cancelled) return
|
||||
if (confirmed) {
|
||||
actionStarted = true
|
||||
setStartupTaskMessage(`Pulling ${status.selected_embed_model} in Ollama...`)
|
||||
const response = await fetch(`${backendApiUrl}/ollama/pull`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: status.selected_embed_model })
|
||||
})
|
||||
await expectBackendJson(response)
|
||||
if (cancelled) return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
console.warn('startup Ollama check failed', error)
|
||||
if (actionStarted) {
|
||||
window.alert(`Startup action failed: ${getErrorText(error)}`)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setStartupTaskMessage('')
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [backendApiUrl, settingsLoaded]);
|
||||
|
||||
// Apply color scheme whenever it changes
|
||||
useEffect(() => {
|
||||
applyColorScheme(colorScheme);
|
||||
@@ -1785,6 +1874,11 @@ async function createNewChat() {
|
||||
<div className="resizer" onMouseDown={startResizing}></div>
|
||||
</div>
|
||||
<div className="main-content">
|
||||
{startupTaskMessage && (
|
||||
<div className="startup-task-banner" role="status" aria-live="polite">
|
||||
{startupTaskMessage}
|
||||
</div>
|
||||
)}
|
||||
{activeSidebarMode === 'chats' && (
|
||||
<>
|
||||
<div className="header">
|
||||
|
||||
@@ -2,10 +2,13 @@ import React, { useState, useEffect } from 'react';
|
||||
|
||||
const BACKEND_API_URL_KEY = 'backendApiUrl';
|
||||
const OLLAMA_API_URL_KEY = 'ollamaApiUrl';
|
||||
const EMBED_MODEL_KEY = 'embedModel';
|
||||
const MODEL_KEY = 'chatModel';
|
||||
const STREAM_KEY = 'streamOutput';
|
||||
const DEFAULT_BACKEND_API_URL = 'http://127.0.0.1:8000';
|
||||
const DEFAULT_OLLAMA_API_URL = 'http://127.0.0.1:11434';
|
||||
const DEFAULT_EMBED_MODEL = 'nomic-embed-text:latest';
|
||||
const BGE_EMBED_MODEL = 'bge-m3:latest';
|
||||
const DEFAULT_UPDATE_STATUS = {
|
||||
state: 'idle',
|
||||
message: '',
|
||||
@@ -32,6 +35,7 @@ function getStatusTone(state) {
|
||||
export default function GeneralSettings({ onModelChange, onStreamOutputChange, onLibrariesPurged }) {
|
||||
const [backendApiUrl, setBackendApiUrl] = useState('');
|
||||
const [ollamaApiUrl, setOllamaApiUrl] = useState('');
|
||||
const [embedModel, setEmbedModel] = useState(DEFAULT_EMBED_MODEL);
|
||||
const [models, setModels] = useState([]);
|
||||
const [selectedModel, setSelectedModel] = useState('');
|
||||
const [streamOutput, setStreamOutput] = useState(false);
|
||||
@@ -53,6 +57,7 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange, o
|
||||
|
||||
setBackendApiUrl(resolveBackendApiUrl(settings));
|
||||
setOllamaApiUrl(settings.ollamaApiUrl || DEFAULT_OLLAMA_API_URL);
|
||||
setEmbedModel(settings.embedModel || DEFAULT_EMBED_MODEL);
|
||||
setSelectedModel(settings.chatModel || '');
|
||||
setStreamOutput(settings.streamOutput || false);
|
||||
setUpdateStatus(status || DEFAULT_UPDATE_STATUS);
|
||||
@@ -102,6 +107,12 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange, o
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmbedModelToggle = () => {
|
||||
const nextModel = embedModel === BGE_EMBED_MODEL ? DEFAULT_EMBED_MODEL : BGE_EMBED_MODEL;
|
||||
setEmbedModel(nextModel);
|
||||
window.electronAPI.setSetting(EMBED_MODEL_KEY, nextModel);
|
||||
};
|
||||
|
||||
const handleStreamToggle = () => {
|
||||
const newStreamValue = !streamOutput;
|
||||
setStreamOutput(newStreamValue);
|
||||
@@ -199,6 +210,28 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange, o
|
||||
/>
|
||||
<p className="setting-description">Heimgeist uses this URL to talk to Ollama for models and chat generation.</p>
|
||||
</div>
|
||||
<div className="setting-section">
|
||||
<h3>Embedding Model</h3>
|
||||
<div className="setting-switch-row">
|
||||
<span className={"setting-switch-label" + (embedModel !== BGE_EMBED_MODEL ? " active" : "")}>
|
||||
nomic
|
||||
</span>
|
||||
<label className="toggle-switch toggle-switch--binary-select">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={embedModel === BGE_EMBED_MODEL}
|
||||
onChange={handleEmbedModelToggle}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
<span className={"setting-switch-label" + (embedModel === BGE_EMBED_MODEL ? " active" : "")}>
|
||||
bge-m3
|
||||
</span>
|
||||
</div>
|
||||
<p className="setting-description">
|
||||
Heimgeist uses this model for web-search reranking and for building or rebuilding local database embeddings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="setting-section">
|
||||
<h3>Chat Model</h3>
|
||||
<select
|
||||
|
||||
@@ -38,7 +38,7 @@ return (
|
||||
className="input"
|
||||
value={searxUrl}
|
||||
onChange={e => setSearxUrl(e.target.value)}
|
||||
placeholder="e.g., http://localhost:8888"
|
||||
placeholder="e.g., http://127.0.0.1:8888"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -59,4 +59,4 @@ return (
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +546,42 @@ textarea.input {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.setting-switch-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.setting-switch-label {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.setting-switch-label.active {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toggle-switch--binary-select .slider {
|
||||
background-color: var(--input-bg);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.toggle-switch--binary-select .slider:before {
|
||||
background-color: var(--text);
|
||||
}
|
||||
|
||||
.toggle-switch--binary-select input:checked + .slider {
|
||||
background-color: var(--input-bg);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.toggle-switch--binary-select input:checked + .slider:before {
|
||||
background-color: var(--text);
|
||||
}
|
||||
|
||||
.range-input {
|
||||
width: min(360px, 100%);
|
||||
accent-color: var(--accent);
|
||||
@@ -798,6 +834,16 @@ input:checked + .slider:before {
|
||||
background-color: var(--panel);
|
||||
}
|
||||
|
||||
.startup-task-banner {
|
||||
margin: 16px 16px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Spinner Styles */
|
||||
.spinner {
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
Reference in New Issue
Block a user