import React, { useState, useEffect } from 'react'; const BACKEND_API_URL_KEY = 'backendApiUrl'; const OLLAMA_API_URL_KEY = 'ollamaApiUrl'; 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_UPDATE_STATUS = { state: 'idle', message: '', checkedAt: null, localCommit: null, remoteCommit: null, }; function resolveBackendApiUrl(settings) { return settings.backendApiUrl || settings.ollamaApiUrl || DEFAULT_BACKEND_API_URL; } function shortCommit(commit) { return typeof commit === 'string' && commit.length > 7 ? commit.slice(0, 7) : commit || '—'; } function getStatusTone(state) { if (state === 'error') return 'error'; if (state === 'updated' || state === 'up-to-date') return 'success'; if (state === 'skipped' || state === 'unavailable') return 'warning'; return 'neutral'; } export default function GeneralSettings({ onModelChange, onStreamOutputChange }) { const [backendApiUrl, setBackendApiUrl] = useState(''); const [ollamaApiUrl, setOllamaApiUrl] = useState(''); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [streamOutput, setStreamOutput] = useState(false); const [updateStatus, setUpdateStatus] = useState(DEFAULT_UPDATE_STATUS); const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false); useEffect(() => { let cancelled = false; Promise.all([ window.electronAPI.getSettings(), window.electronAPI.getUpdateStatus(), ]).then(([settings, status]) => { if (cancelled) { return; } setBackendApiUrl(resolveBackendApiUrl(settings)); setOllamaApiUrl(settings.ollamaApiUrl || DEFAULT_OLLAMA_API_URL); setSelectedModel(settings.chatModel || ''); setStreamOutput(settings.streamOutput || false); setUpdateStatus(status || DEFAULT_UPDATE_STATUS); }); return () => { cancelled = true; }; }, []); useEffect(() => { if (backendApiUrl) { fetch(backendApiUrl + '/models') .then(r => r.json()) .then(data => { const names = data.models?.map(m => m.name) || []; setModels(names); // If no model is selected or the selected model is no longer available, select the first one if (!selectedModel || !names.includes(selectedModel)) { const defaultModel = names[0] || ''; setSelectedModel(defaultModel); window.electronAPI.setSetting(MODEL_KEY, defaultModel); } }) .catch(err => console.error('Failed to load models', err)); } }, [backendApiUrl, ollamaApiUrl, selectedModel]); // Depend on selectedModel to re-evaluate default selection const handleBackendUrlChange = (e) => { const newUrl = e.target.value; setBackendApiUrl(newUrl); window.electronAPI.setSetting(BACKEND_API_URL_KEY, newUrl); }; const handleOllamaUrlChange = (e) => { const newUrl = e.target.value; setOllamaApiUrl(newUrl); window.electronAPI.setSetting(OLLAMA_API_URL_KEY, newUrl); }; const handleModelChange = (e) => { const newModel = e.target.value; setSelectedModel(newModel); window.electronAPI.setSetting(MODEL_KEY, newModel); if (onModelChange) { onModelChange(newModel); } }; const handleStreamToggle = () => { const newStreamValue = !streamOutput; setStreamOutput(newStreamValue); window.electronAPI.setSetting(STREAM_KEY, newStreamValue); if (onStreamOutputChange) { onStreamOutputChange(newStreamValue); } }; const handleCheckForUpdates = async () => { setIsCheckingForUpdates(true); try { const status = await window.electronAPI.checkForUpdates(); setUpdateStatus(status || DEFAULT_UPDATE_STATUS); } catch (error) { setUpdateStatus({ state: 'error', message: `Update check failed: ${error.message || String(error)}`, checkedAt: new Date().toISOString(), localCommit: null, remoteCommit: null, }); } finally { setIsCheckingForUpdates(false); } }; const updateCheckedAtLabel = updateStatus.checkedAt ? new Date(updateStatus.checkedAt).toLocaleString() : null; return (

Heimgeist Backend URL

Internal UI requests like chats, sessions, and databases go to this URL.

Ollama URL

Heimgeist uses this URL to talk to Ollama for models and chat generation.

Chat Model

Stream Output

Updates

Compares the local Git commit with remote master, pulls changes when needed, and restarts Heimgeist automatically. The same check also runs on every startup.

{updateStatus.message && (

{updateStatus.message}

)} {(updateStatus.localCommit || updateStatus.remoteCommit || updateCheckedAtLabel) && (
{updateStatus.localCommit &&
Local: {shortCommit(updateStatus.localCommit)}
} {updateStatus.remoteCommit &&
Remote: {shortCommit(updateStatus.remoteCommit)}
} {updateCheckedAtLabel &&
Last checked: {updateCheckedAtLabel}
}
)}
); }