feat: ollama support
This commit is contained in:
@@ -532,3 +532,36 @@
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Ollama model capabilities table */
|
||||
.ollama-model-capabilities {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ollama-model-capabilities .setting-description {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ollama-caps-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.ollama-caps-table th,
|
||||
.ollama-caps-table td {
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color, #ccc);
|
||||
}
|
||||
|
||||
.ollama-caps-table th:not(:first-child),
|
||||
.ollama-caps-table td:not(:first-child) {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.ollama-caps-table input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -245,6 +245,9 @@ export const SettingsView: React.FC = () => {
|
||||
const [aiHasMistralKey, setAiHasMistralKey] = useState(false);
|
||||
const [aiMistralKeyMasked, setAiMistralKeyMasked] = useState('');
|
||||
const [newMistralKey, setNewMistralKey] = useState('');
|
||||
const [ollamaEnabled, setOllamaEnabled] = useState(false);
|
||||
const [ollamaCapabilities, setOllamaCapabilities] = useState<Record<string, { tools: boolean; vision: boolean }>>({});
|
||||
const [ollamaModels, setOllamaModels] = useState<{id: string; name: string}[]>([]);
|
||||
const [titleModel, setTitleModel] = useState('claude-haiku-4-5');
|
||||
const [imageAnalysisModel, setImageAnalysisModel] = useState('claude-sonnet-4-5');
|
||||
const [availableModels, setAvailableModels] = useState<{id: string; name: string; provider?: string; vision?: boolean}[]>([]);
|
||||
@@ -415,6 +418,20 @@ export const SettingsView: React.FC = () => {
|
||||
setAiMistralKeyMasked(mistralKeyResult.maskedKey || '');
|
||||
}
|
||||
|
||||
// Load Ollama enabled state
|
||||
const ollamaState = await window.electronAPI?.chat.getOllamaEnabled();
|
||||
setOllamaEnabled(!!ollamaState);
|
||||
|
||||
// Load Ollama model capabilities and models list
|
||||
if (ollamaState) {
|
||||
const [caps, models] = await Promise.all([
|
||||
window.electronAPI?.chat.getOllamaModelCapabilities(),
|
||||
window.electronAPI?.chat.getOllamaModels(),
|
||||
]);
|
||||
if (caps) setOllamaCapabilities(caps);
|
||||
if (models) setOllamaModels(models.map(m => ({ id: m.id, name: m.name })));
|
||||
}
|
||||
|
||||
// Load per-purpose model preferences
|
||||
const titleModelResult = await window.electronAPI?.chat.getTitleModel();
|
||||
if (titleModelResult?.success && titleModelResult.modelId) {
|
||||
@@ -536,7 +553,7 @@ export const SettingsView: React.FC = () => {
|
||||
const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'url', 'public', 'path', 'folder', 'location', 'data', 'language', 'author', 'default', 'preview', 'max', 'posts', 'page', 'bookmarklet', 'blogmark'];
|
||||
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
||||
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
||||
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode'];
|
||||
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode', 'ollama', 'local'];
|
||||
const technologyKeywords = ['technology', 'python', 'runtime', 'worker', 'webworker', 'main thread', 'execution'];
|
||||
const publishingKeywords = ['publishing', 'ssh', 'deploy', 'server', 'host', 'upload', 'scp', 'rsync'];
|
||||
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'scripts', 'links', 'folder', 'filesystem'];
|
||||
@@ -1144,6 +1161,55 @@ export const SettingsView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOllamaToggle = async (enabled: boolean) => {
|
||||
try {
|
||||
const result = await window.electronAPI?.chat.setOllamaEnabled(enabled);
|
||||
if (result?.success) {
|
||||
setOllamaEnabled(enabled);
|
||||
showToast.success(t(enabled ? 'settings.toast.ollamaEnabled' : 'settings.toast.ollamaDisabled'));
|
||||
|
||||
// Refresh models after toggle
|
||||
const modelsResult = await window.electronAPI?.chat.getAvailableModels();
|
||||
if (modelsResult?.success && modelsResult.models) {
|
||||
setAvailableModels(modelsResult.models);
|
||||
setSelectedModel(modelsResult.selectedModel || '');
|
||||
}
|
||||
|
||||
// Load Ollama models and capabilities when enabling
|
||||
if (enabled) {
|
||||
const [caps, ollamaModelsList] = await Promise.all([
|
||||
window.electronAPI?.chat.getOllamaModelCapabilities(),
|
||||
window.electronAPI?.chat.getOllamaModels(),
|
||||
]);
|
||||
if (caps) setOllamaCapabilities(caps);
|
||||
if (ollamaModelsList) setOllamaModels(ollamaModelsList.map(m => ({ id: m.id, name: m.name })));
|
||||
} else {
|
||||
setOllamaModels([]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle Ollama:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOllamaCapabilityToggle = async (modelId: string, field: 'tools' | 'vision', value: boolean) => {
|
||||
const current = ollamaCapabilities[modelId] ?? { tools: false, vision: false };
|
||||
const updated = { ...current, [field]: value };
|
||||
try {
|
||||
const result = await window.electronAPI?.chat.setOllamaModelCapabilities(modelId, updated);
|
||||
if (result?.success) {
|
||||
setOllamaCapabilities(prev => ({ ...prev, [modelId]: updated }));
|
||||
// Refresh available models to reflect vision change
|
||||
const modelsResult = await window.electronAPI?.chat.getAvailableModels();
|
||||
if (modelsResult?.success && modelsResult.models) {
|
||||
setAvailableModels(modelsResult.models);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update Ollama model capabilities:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTitleModelChange = async (modelId: string) => {
|
||||
try {
|
||||
const result = await window.electronAPI?.chat.setTitleModel(modelId);
|
||||
@@ -1236,6 +1302,7 @@ export const SettingsView: React.FC = () => {
|
||||
const providerLabel = (provider: string) => {
|
||||
if (provider === 'anthropic' || provider === 'openai' || provider === 'google' || provider === 'other') return t('settings.ai.providerOpenCode');
|
||||
if (provider === 'mistral') return t('settings.ai.providerMistral');
|
||||
if (provider === 'ollama') return t('settings.ai.providerOllama');
|
||||
return provider;
|
||||
};
|
||||
|
||||
@@ -1346,17 +1413,76 @@ export const SettingsView: React.FC = () => {
|
||||
)}
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
id="ai-ollama"
|
||||
label={t('settings.ai.ollamaLabel')}
|
||||
description={t('settings.ai.ollamaDescription')}
|
||||
>
|
||||
<div className="setting-input-group">
|
||||
<label className="toggle-label">
|
||||
<input
|
||||
id="ai-ollama"
|
||||
type="checkbox"
|
||||
checked={ollamaEnabled}
|
||||
onChange={(e) => handleOllamaToggle(e.target.checked)}
|
||||
/>
|
||||
{t('settings.ai.ollamaEnable')}
|
||||
</label>
|
||||
{ollamaEnabled && (
|
||||
<span className="setting-status-badge success">{t('settings.ai.configured')}</span>
|
||||
)}
|
||||
</div>
|
||||
{ollamaEnabled && ollamaModels.length > 0 && (
|
||||
<div className="ollama-model-capabilities">
|
||||
<small className="setting-description">{t('settings.ai.ollamaCapabilitiesDescription')}</small>
|
||||
<table className="ollama-caps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.ai.ollamaCapModel')}</th>
|
||||
<th>{t('settings.ai.ollamaCapTools')}</th>
|
||||
<th>{t('settings.ai.ollamaCapVision')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ollamaModels.map(m => {
|
||||
const caps = ollamaCapabilities[m.id] ?? { tools: false, vision: false };
|
||||
return (
|
||||
<tr key={m.id}>
|
||||
<td>{m.name}</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={caps.tools}
|
||||
onChange={(e) => handleOllamaCapabilityToggle(m.id, 'tools', e.target.checked)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={caps.vision}
|
||||
onChange={(e) => handleOllamaCapabilityToggle(m.id, 'vision', e.target.checked)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
id="ai-model"
|
||||
label={t('settings.ai.defaultModelLabel')}
|
||||
description={t('settings.ai.defaultModelDescription')}
|
||||
>
|
||||
<div className="setting-input-group">
|
||||
{renderModelSelect('ai-model', selectedModel, handleModelChange, !aiHasApiKey && !aiHasMistralKey)}
|
||||
{renderModelSelect('ai-model', selectedModel, handleModelChange, !aiHasApiKey && !aiHasMistralKey && !ollamaEnabled)}
|
||||
<button
|
||||
className="secondary"
|
||||
onClick={handleRefreshModelCatalog}
|
||||
disabled={refreshingCatalog || (!aiHasApiKey && !aiHasMistralKey)}
|
||||
disabled={refreshingCatalog || (!aiHasApiKey && !aiHasMistralKey && !ollamaEnabled)}
|
||||
title={t('settings.ai.refreshModelCatalog')}
|
||||
>
|
||||
{refreshingCatalog ? t('settings.ai.refreshing') : t('settings.ai.refreshModelCatalog')}
|
||||
@@ -1388,7 +1514,7 @@ export const SettingsView: React.FC = () => {
|
||||
label={t('settings.ai.titleModelLabel')}
|
||||
description={t('settings.ai.titleModelDescription')}
|
||||
>
|
||||
{renderModelSelect('ai-title-model', titleModel, handleTitleModelChange, !aiHasApiKey && !aiHasMistralKey)}
|
||||
{renderModelSelect('ai-title-model', titleModel, handleTitleModelChange, !aiHasApiKey && !aiHasMistralKey && !ollamaEnabled)}
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
@@ -1396,7 +1522,7 @@ export const SettingsView: React.FC = () => {
|
||||
label={t('settings.ai.imageAnalysisModelLabel')}
|
||||
description={t('settings.ai.imageAnalysisModelDescription')}
|
||||
>
|
||||
{renderModelSelect('ai-image-analysis-model', imageAnalysisModel, handleImageAnalysisModelChange, !aiHasApiKey && !aiHasMistralKey, groupedVisionModels)}
|
||||
{renderModelSelect('ai-image-analysis-model', imageAnalysisModel, handleImageAnalysisModelChange, !aiHasApiKey && !aiHasMistralKey && !ollamaEnabled, groupedVisionModels)}
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
|
||||
@@ -741,11 +741,21 @@
|
||||
"settings.ai.imageAnalysisModelDescription": "Modell für die automatische Bildanalyse (Titel, Alt-Text, Bildunterschrift).",
|
||||
"settings.ai.providerOpenCode": "OpenCode",
|
||||
"settings.ai.providerMistral": "Mistral",
|
||||
"settings.ai.providerOllama": "Ollama (Lokal)",
|
||||
"settings.ai.providerOther": "Andere",
|
||||
"settings.ai.ollamaLabel": "Ollama (Lokale Modelle)",
|
||||
"settings.ai.ollamaDescription": "Verbinde dich mit einer lokal laufenden Ollama-Instanz, um lokale KI-Modelle zu verwenden.",
|
||||
"settings.ai.ollamaEnable": "Ollama aktivieren",
|
||||
"settings.ai.ollamaCapabilitiesDescription": "Fähigkeiten für jedes Ollama-Modell konfigurieren. Tools für Funktionsaufrufe oder Vision für Bildanalyse aktivieren.",
|
||||
"settings.ai.ollamaCapModel": "Modell",
|
||||
"settings.ai.ollamaCapTools": "Tools",
|
||||
"settings.ai.ollamaCapVision": "Vision",
|
||||
"chat.providerKeyMissing": "Das Modell '{{model}}' benötigt einen {{provider}} API-Schlüssel. Konfiguriere ihn in den Einstellungen.",
|
||||
"settings.toast.modelCatalogRefreshed": "Modellkatalog aktualisiert ({{count}} Modelle)",
|
||||
"settings.toast.modelCatalogUpToDate": "Modellkatalog ist bereits aktuell",
|
||||
"settings.toast.modelCatalogRefreshFailed": "Modellkatalog konnte nicht aktualisiert werden",
|
||||
"settings.toast.ollamaEnabled": "Ollama aktiviert",
|
||||
"settings.toast.ollamaDisabled": "Ollama deaktiviert",
|
||||
"settings.publishing.sshHostDescription": "Hostname oder IP-Adresse des SSH-Servers.",
|
||||
"settings.publishing.sshUsernameDescription": "Benutzername deines SSH-Kontos.",
|
||||
"settings.publishing.sshRemotePathDescription": "Das Zielverzeichnis auf dem Remote-Server, in das dein Blog veröffentlicht wird.",
|
||||
|
||||
@@ -741,11 +741,21 @@
|
||||
"settings.ai.imageAnalysisModelDescription": "Model used for automatic image analysis (title, alt text, caption).",
|
||||
"settings.ai.providerOpenCode": "OpenCode",
|
||||
"settings.ai.providerMistral": "Mistral",
|
||||
"settings.ai.providerOllama": "Ollama (Local)",
|
||||
"settings.ai.providerOther": "Other",
|
||||
"settings.ai.ollamaLabel": "Ollama (Local Models)",
|
||||
"settings.ai.ollamaDescription": "Connect to a locally running Ollama instance to use local AI models.",
|
||||
"settings.ai.ollamaEnable": "Enable Ollama",
|
||||
"settings.ai.ollamaCapabilitiesDescription": "Configure capabilities for each Ollama model. Enable tools for function calling or vision for image analysis.",
|
||||
"settings.ai.ollamaCapModel": "Model",
|
||||
"settings.ai.ollamaCapTools": "Tools",
|
||||
"settings.ai.ollamaCapVision": "Vision",
|
||||
"chat.providerKeyMissing": "The model '{{model}}' requires a {{provider}} API key. Configure it in Settings.",
|
||||
"settings.toast.modelCatalogRefreshed": "Model catalog updated ({{count}} models)",
|
||||
"settings.toast.modelCatalogUpToDate": "Model catalog already up to date",
|
||||
"settings.toast.modelCatalogRefreshFailed": "Failed to refresh model catalog",
|
||||
"settings.toast.ollamaEnabled": "Ollama enabled",
|
||||
"settings.toast.ollamaDisabled": "Ollama disabled",
|
||||
"settings.publishing.sshHostDescription": "The SSH server hostname or IP address.",
|
||||
"settings.publishing.sshUsernameDescription": "Your SSH account username.",
|
||||
"settings.publishing.sshRemotePathDescription": "The destination directory on the remote server where your blog will be published.",
|
||||
|
||||
@@ -741,11 +741,21 @@
|
||||
"settings.ai.imageAnalysisModelDescription": "Modelo utilizado para el análisis automático de imágenes (título, texto alternativo, leyenda).",
|
||||
"settings.ai.providerOpenCode": "OpenCode",
|
||||
"settings.ai.providerMistral": "Mistral",
|
||||
"settings.ai.providerOllama": "Ollama (Local)",
|
||||
"settings.ai.providerOther": "Otro",
|
||||
"settings.ai.ollamaLabel": "Ollama (Modelos locales)",
|
||||
"settings.ai.ollamaDescription": "Conéctate a una instancia local de Ollama para usar modelos de IA locales.",
|
||||
"settings.ai.ollamaEnable": "Activar Ollama",
|
||||
"settings.ai.ollamaCapabilitiesDescription": "Configurar las capacidades de cada modelo Ollama. Activar herramientas para llamadas a funciones o visión para análisis de imágenes.",
|
||||
"settings.ai.ollamaCapModel": "Modelo",
|
||||
"settings.ai.ollamaCapTools": "Herramientas",
|
||||
"settings.ai.ollamaCapVision": "Visión",
|
||||
"chat.providerKeyMissing": "El modelo '{{model}}' requiere una clave API de {{provider}}. Configúrela en Ajustes.",
|
||||
"settings.toast.modelCatalogRefreshed": "Catálogo actualizado ({{count}} modelos)",
|
||||
"settings.toast.modelCatalogUpToDate": "El catálogo ya está actualizado",
|
||||
"settings.toast.modelCatalogRefreshFailed": "No se pudo actualizar el catálogo",
|
||||
"settings.toast.ollamaEnabled": "Ollama activado",
|
||||
"settings.toast.ollamaDisabled": "Ollama desactivado",
|
||||
"settings.publishing.sshHostDescription": "Nombre de host o IP del servidor SSH.",
|
||||
"settings.publishing.sshUsernameDescription": "Nombre de usuario de SSH.",
|
||||
"settings.publishing.sshRemotePathDescription": "El directorio de destino en el servidor remoto donde se publicará tu blog.",
|
||||
|
||||
@@ -739,11 +739,21 @@
|
||||
"settings.ai.imageAnalysisModelDescription": "Modèle utilisé pour l'analyse automatique d'images (titre, texte alternatif, légende).",
|
||||
"settings.ai.providerOpenCode": "OpenCode",
|
||||
"settings.ai.providerMistral": "Mistral",
|
||||
"settings.ai.providerOllama": "Ollama (Local)",
|
||||
"settings.ai.providerOther": "Autre",
|
||||
"settings.ai.ollamaLabel": "Ollama (Modèles locaux)",
|
||||
"settings.ai.ollamaDescription": "Connectez-vous à une instance Ollama locale pour utiliser des modèles d'IA locaux.",
|
||||
"settings.ai.ollamaEnable": "Activer Ollama",
|
||||
"settings.ai.ollamaCapabilitiesDescription": "Configurer les capacités de chaque modèle Ollama. Activer les outils pour les appels de fonctions ou la vision pour l'analyse d'images.",
|
||||
"settings.ai.ollamaCapModel": "Modèle",
|
||||
"settings.ai.ollamaCapTools": "Outils",
|
||||
"settings.ai.ollamaCapVision": "Vision",
|
||||
"chat.providerKeyMissing": "Le modèle '{{model}}' nécessite une clé API {{provider}}. Configurez-la dans les paramètres.",
|
||||
"settings.toast.modelCatalogRefreshed": "Catalogue mis à jour ({{count}} modèles)",
|
||||
"settings.toast.modelCatalogUpToDate": "Le catalogue est déjà à jour",
|
||||
"settings.toast.modelCatalogRefreshFailed": "Échec de l'actualisation du catalogue",
|
||||
"settings.toast.ollamaEnabled": "Ollama activé",
|
||||
"settings.toast.ollamaDisabled": "Ollama désactivé",
|
||||
"settings.publishing.sshHostDescription": "Nom d'hôte ou IP du serveur SSH.",
|
||||
"settings.publishing.sshUsernameDescription": "Nom d'utilisateur SSH.",
|
||||
"settings.publishing.sshRemotePathDescription": "Le répertoire de destination sur le serveur distant où votre blog sera publié.",
|
||||
|
||||
@@ -739,11 +739,21 @@
|
||||
"settings.ai.imageAnalysisModelDescription": "Modello utilizzato per l'analisi automatica delle immagini (titolo, testo alternativo, didascalia).",
|
||||
"settings.ai.providerOpenCode": "OpenCode",
|
||||
"settings.ai.providerMistral": "Mistral",
|
||||
"settings.ai.providerOllama": "Ollama (Locale)",
|
||||
"settings.ai.providerOther": "Altro",
|
||||
"settings.ai.ollamaLabel": "Ollama (Modelli locali)",
|
||||
"settings.ai.ollamaDescription": "Connettiti a un'istanza Ollama locale per utilizzare modelli IA locali.",
|
||||
"settings.ai.ollamaEnable": "Attiva Ollama",
|
||||
"settings.ai.ollamaCapabilitiesDescription": "Configura le capacità per ogni modello Ollama. Attiva gli strumenti per le chiamate a funzioni o la visione per l'analisi delle immagini.",
|
||||
"settings.ai.ollamaCapModel": "Modello",
|
||||
"settings.ai.ollamaCapTools": "Strumenti",
|
||||
"settings.ai.ollamaCapVision": "Visione",
|
||||
"chat.providerKeyMissing": "Il modello '{{model}}' richiede una chiave API {{provider}}. Configurala nelle Impostazioni.",
|
||||
"settings.toast.modelCatalogRefreshed": "Catalogo aggiornato ({{count}} modelli)",
|
||||
"settings.toast.modelCatalogUpToDate": "Il catalogo è già aggiornato",
|
||||
"settings.toast.modelCatalogRefreshFailed": "Aggiornamento del catalogo non riuscito",
|
||||
"settings.toast.ollamaEnabled": "Ollama attivato",
|
||||
"settings.toast.ollamaDisabled": "Ollama disattivato",
|
||||
"settings.publishing.sshHostDescription": "Hostname o IP del server SSH.",
|
||||
"settings.publishing.sshUsernameDescription": "Nome utente SSH.",
|
||||
"settings.publishing.sshRemotePathDescription": "La directory di destinazione sul server remoto in cui verrà pubblicato il tuo blog.",
|
||||
|
||||
Reference in New Issue
Block a user