feat(settings): let users set their own AI parsing model

Adds an "AI parsing" section under Settings -> Integrations where a user can choose the LLM provider, model, base URL, API key and multimodal option used for booking extraction. This per-user config applies when an admin has not configured an instance-wide model. Reuses the existing encrypted user settings: the API key is stored encrypted, never prefilled, and a blank field keeps the stored one. Adds settings.aiParsing.* across all 20 locales.
This commit is contained in:
Maurice
2026-06-26 19:12:54 +02:00
committed by Maurice
parent 01e5859564
commit fc1f29bb29
22 changed files with 454 additions and 1 deletions
@@ -7,6 +7,7 @@ import { authApi, oauthApi } from '../../api/client'
import { useAddonStore } from '../../store/addonStore'
import PhotoProvidersSection from './PhotoProvidersSection'
import AirTrailConnectionSection from './AirTrailConnectionSection'
import LlmConnectionSection from './LlmConnectionSection'
import { ALL_SCOPES } from '../../api/oauthScopes'
import ScopeGroupPicker from '../OAuth/ScopeGroupPicker'
@@ -99,6 +100,7 @@ export default function IntegrationsTab(): React.ReactElement {
<>
<PhotoProvidersSection />
{S.airtrailEnabled && <AirTrailConnectionSection />}
{S.llmEnabled && <LlmConnectionSection />}
{S.mcpEnabled && <IntegrationsMcpSection {...S} />}
<McpTokenModals {...S} />
<OAuthClientModals {...S} />
@@ -112,6 +114,7 @@ function useIntegrations() {
const { isEnabled: addonEnabled, loadAddons } = useAddonStore()
const mcpEnabled = addonEnabled('mcp')
const airtrailEnabled = addonEnabled('airtrail')
const llmEnabled = addonEnabled('llm_parsing')
useEffect(() => {
loadAddons()
@@ -292,7 +295,7 @@ function useIntegrations() {
return {
t, locale, toast, mcpEnabled, airtrailEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession,
t, locale, toast, mcpEnabled, airtrailEnabled, llmEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession,
}
}
@@ -0,0 +1,148 @@
import React, { useEffect, useState } from 'react'
import { Sparkles, Save } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { useToast } from '../shared/Toast'
import { useSettingsStore } from '../../store/settingsStore'
import type { Settings } from '../../types'
import Section from './Section'
import ToggleSwitch from './ToggleSwitch'
type Provider = NonNullable<Settings['llm_provider']>
/**
* Settings → Integrations → AI parsing. Per-user model used to extract bookings
* from uploaded files. It only takes effect when the admin has not configured an
* instance-wide model on the addon — the server resolves the admin config first.
* The API key is stored encrypted and never prefilled: a blank field keeps the
* stored key (mirrors the AirTrail connection layout).
*/
export default function LlmConnectionSection(): React.ReactElement {
const { t } = useTranslation()
const toast = useToast()
const settings = useSettingsStore(s => s.settings)
const isLoaded = useSettingsStore(s => s.isLoaded)
const updateSettings = useSettingsStore(s => s.updateSettings)
const [provider, setProvider] = useState<Provider>('local')
const [model, setModel] = useState('')
const [baseUrl, setBaseUrl] = useState('')
const [apiKey, setApiKey] = useState('')
const [multimodal, setMultimodal] = useState(false)
const [hasStoredKey, setHasStoredKey] = useState(false)
const [saving, setSaving] = useState(false)
// Hydrate from the loaded settings. llm_api_key arrives masked, so we only use
// its presence to drive the placeholder — never the value itself.
useEffect(() => {
if (!isLoaded) return
setProvider(settings.llm_provider || 'local')
setModel(settings.llm_model || '')
setBaseUrl(settings.llm_base_url || '')
setMultimodal(settings.llm_multimodal === true)
setHasStoredKey(!!settings.llm_api_key)
}, [isLoaded, settings.llm_provider, settings.llm_model, settings.llm_base_url, settings.llm_multimodal, settings.llm_api_key])
const needsKey = provider !== 'local'
const showBaseUrl = provider === 'local' || provider === 'openai'
const handleSave = async () => {
setSaving(true)
try {
const payload: Partial<Settings> = {
llm_provider: provider,
llm_model: model.trim(),
llm_base_url: showBaseUrl ? baseUrl.trim() : '',
llm_multimodal: multimodal,
}
// Send the key only when the user typed a new one — a blank field means
// "keep the stored key".
const key = apiKey.trim()
if (key) payload.llm_api_key = key
await updateSettings(payload)
setApiKey('')
if (key) setHasStoredKey(true)
toast.success(t('settings.aiParsing.toast.saved'))
} catch {
toast.error(t('settings.aiParsing.toast.saveError'))
} finally {
setSaving(false)
}
}
return (
<Section title={t('settings.aiParsing.title')} icon={Sparkles}>
<div className="space-y-3">
<p className="text-xs text-content-secondary">{t('settings.aiParsing.hint')}</p>
<div>
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.aiParsing.provider')}</label>
<select
value={provider}
onChange={e => setProvider(e.target.value as Provider)}
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content"
>
<option value="local">{t('settings.aiParsing.providerLocal')}</option>
<option value="openai">{t('settings.aiParsing.providerOpenai')}</option>
<option value="anthropic">{t('settings.aiParsing.providerAnthropic')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.aiParsing.model')}</label>
<input
type="text"
value={model}
onChange={e => setModel(e.target.value)}
placeholder="qwen3:8b"
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content"
/>
</div>
{showBaseUrl && (
<div>
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.aiParsing.baseUrl')}</label>
<input
type="url"
value={baseUrl}
onChange={e => setBaseUrl(e.target.value)}
placeholder="http://localhost:11434"
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content"
/>
<p className="mt-1 text-xs text-content-faint">{t('settings.aiParsing.baseUrlHint')}</p>
</div>
)}
{needsKey && (
<div>
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.aiParsing.apiKey')}</label>
<input
type="password"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
autoComplete="off"
placeholder={hasStoredKey && !apiKey ? '••••••••' : t('settings.aiParsing.apiKey')}
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content"
/>
<p className="mt-1 text-xs text-content-faint">{t('settings.aiParsing.apiKeyHint')}</p>
</div>
)}
<div>
<div className="flex items-center gap-3">
<ToggleSwitch on={multimodal} onToggle={() => setMultimodal(v => !v)} />
<span className="text-sm font-medium text-content-secondary">{t('settings.aiParsing.multimodal')}</span>
</div>
<p className="mt-1 text-xs text-content-faint">{t('settings.aiParsing.multimodalHint')}</p>
</div>
<button
onClick={handleSave}
disabled={saving || !isLoaded}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700 disabled:opacity-50"
>
<Save className="w-4 h-4" /> {t('common.save')}
</button>
</div>
</Section>
)
}
+15
View File
@@ -317,6 +317,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'اختبار الاتصال',
'settings.airtrail.test.success': 'متصل — تم العثور على {count} رحلة/رحلات',
'settings.airtrail.test.failed': 'فشل الاتصال',
'settings.aiParsing.title': 'التحليل بالذكاء الاصطناعي',
'settings.aiParsing.hint': 'استخدم نموذج الذكاء الاصطناعي الخاص بك لاستخراج الحجوزات من الملفات المرفوعة. لا يسري هذا إلا عندما لا يكون المسؤول قد أعدّ نموذجًا للنظام بأكمله.',
'settings.aiParsing.provider': 'المزوّد',
'settings.aiParsing.providerLocal': 'محلي (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'النموذج',
'settings.aiParsing.baseUrl': 'عنوان URL الأساسي',
'settings.aiParsing.baseUrlHint': 'المكان الذي يعمل فيه النموذج — خادم Ollama محلي أو نقطة نهاية متوافقة مع OpenAI.',
'settings.aiParsing.apiKey': 'مفتاح API',
'settings.aiParsing.apiKeyHint': 'يُخزَّن مشفّرًا. اتركه فارغًا للإبقاء على المفتاح الحالي.',
'settings.aiParsing.multimodal': 'إرسال المستندات كصور',
'settings.aiParsing.multimodalHint': 'للنماذج القادرة على الرؤية — يرسل ملف PDF الأصلي بدلًا من النص المستخرج.',
'settings.aiParsing.toast.saved': 'تم حفظ إعدادات الذكاء الاصطناعي',
'settings.aiParsing.toast.saveError': 'تعذّر حفظ إعدادات الذكاء الاصطناعي',
};
export default settings;
+15
View File
@@ -327,6 +327,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Testar conexão',
'settings.airtrail.test.success': 'Conectado — {count} voo(s) encontrado(s)',
'settings.airtrail.test.failed': 'Falha na conexão',
'settings.aiParsing.title': 'Análise por IA',
'settings.aiParsing.hint': 'Use seu próprio modelo de IA para extrair reservas dos arquivos enviados. Isso se aplica apenas quando o administrador não configurou um modelo para toda a instância.',
'settings.aiParsing.provider': 'Provedor',
'settings.aiParsing.providerLocal': 'Local (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Modelo',
'settings.aiParsing.baseUrl': 'URL base',
'settings.aiParsing.baseUrlHint': 'Onde o modelo é executado — um servidor Ollama local ou um endpoint compatível com OpenAI.',
'settings.aiParsing.apiKey': 'Chave de API',
'settings.aiParsing.apiKeyHint': 'Armazenada de forma criptografada. Deixe em branco para manter a chave atual.',
'settings.aiParsing.multimodal': 'Enviar documentos como imagens',
'settings.aiParsing.multimodalHint': 'Para modelos com capacidade de visão — envia o PDF original em vez do texto extraído.',
'settings.aiParsing.toast.saved': 'Configurações de IA salvas',
'settings.aiParsing.toast.saveError': 'Não foi possível salvar as configurações de IA',
};
export default settings;
+15
View File
@@ -324,6 +324,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Otestovat připojení',
'settings.airtrail.test.success': 'Připojeno nalezeno letů: {count}',
'settings.airtrail.test.failed': 'Připojení selhalo',
'settings.aiParsing.title': 'Zpracování pomocí AI',
'settings.aiParsing.hint': 'Použijte vlastní model AI k získání rezervací z nahraných souborů. Platí pouze tehdy, když správce nenastavil model pro celou instanci.',
'settings.aiParsing.provider': 'Poskytovatel',
'settings.aiParsing.providerLocal': 'Místní (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Model',
'settings.aiParsing.baseUrl': 'Základní URL',
'settings.aiParsing.baseUrlHint': 'Kde model běží — místní server Ollama nebo koncový bod kompatibilní s OpenAI.',
'settings.aiParsing.apiKey': 'Klíč API',
'settings.aiParsing.apiKeyHint': 'Ukládá se šifrovaně. Ponechte prázdné pro zachování aktuálního klíče.',
'settings.aiParsing.multimodal': 'Odesílat dokumenty jako obrázky',
'settings.aiParsing.multimodalHint': 'Pro modely se schopností zpracovat obraz — odešle původní PDF místo extrahovaného textu.',
'settings.aiParsing.toast.saved': 'Nastavení AI uloženo',
'settings.aiParsing.toast.saveError': 'Nastavení AI se nepodařilo uložit',
};
export default settings;
+16
View File
@@ -330,6 +330,22 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Verbindung testen',
'settings.airtrail.test.success': 'Verbunden — {count} Flug/Flüge gefunden',
'settings.airtrail.test.failed': 'Verbindung fehlgeschlagen',
'settings.aiParsing.title': 'KI-Verarbeitung',
'settings.aiParsing.hint':
'Nutze dein eigenes KI-Modell, um Buchungen aus hochgeladenen Dateien auszulesen. Greift nur, wenn dein Administrator kein Modell für die gesamte Instanz konfiguriert hat.',
'settings.aiParsing.provider': 'Anbieter',
'settings.aiParsing.providerLocal': 'Lokal (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Modell',
'settings.aiParsing.baseUrl': 'Basis-URL',
'settings.aiParsing.baseUrlHint': 'Wo das Modell läuft — ein lokaler Ollama-Server oder ein OpenAI-kompatibler Endpunkt.',
'settings.aiParsing.apiKey': 'API-Schlüssel',
'settings.aiParsing.apiKeyHint': 'Verschlüsselt gespeichert. Leer lassen, um den aktuellen Schlüssel zu behalten.',
'settings.aiParsing.multimodal': 'Dokumente als Bilder senden',
'settings.aiParsing.multimodalHint': 'Für Modelle mit Bildverständnis — sendet das Original-PDF statt extrahiertem Text.',
'settings.aiParsing.toast.saved': 'KI-Einstellungen gespeichert',
'settings.aiParsing.toast.saveError': 'KI-Einstellungen konnten nicht gespeichert werden',
};
export default settings;
+16
View File
@@ -324,6 +324,22 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Test connection',
'settings.airtrail.test.success': 'Connected — {count} flight(s) found',
'settings.airtrail.test.failed': 'Connection failed',
'settings.aiParsing.title': 'AI parsing',
'settings.aiParsing.hint':
'Use your own AI model to extract bookings from uploaded files. This applies only when your administrator has not configured a model for the whole instance.',
'settings.aiParsing.provider': 'Provider',
'settings.aiParsing.providerLocal': 'Local (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Model',
'settings.aiParsing.baseUrl': 'Base URL',
'settings.aiParsing.baseUrlHint': 'Where the model runs — a local Ollama server or an OpenAI-compatible endpoint.',
'settings.aiParsing.apiKey': 'API key',
'settings.aiParsing.apiKeyHint': 'Stored encrypted. Leave blank to keep the current key.',
'settings.aiParsing.multimodal': 'Send documents as images',
'settings.aiParsing.multimodalHint': 'For vision-capable models — sends the original PDF instead of extracted text.',
'settings.aiParsing.toast.saved': 'AI settings saved',
'settings.aiParsing.toast.saveError': 'Could not save AI settings',
};
export default settings;
+15
View File
@@ -330,6 +330,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Probar conexión',
'settings.airtrail.test.success': 'Conectado: {count} vuelo(s) encontrado(s)',
'settings.airtrail.test.failed': 'Error de conexión',
'settings.aiParsing.title': 'Análisis con IA',
'settings.aiParsing.hint': 'Usa tu propio modelo de IA para extraer reservas de los archivos subidos. Esto solo se aplica cuando tu administrador no ha configurado un modelo para toda la instancia.',
'settings.aiParsing.provider': 'Proveedor',
'settings.aiParsing.providerLocal': 'Local (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Modelo',
'settings.aiParsing.baseUrl': 'URL base',
'settings.aiParsing.baseUrlHint': 'Donde se ejecuta el modelo: un servidor Ollama local o un endpoint compatible con OpenAI.',
'settings.aiParsing.apiKey': 'Clave de API',
'settings.aiParsing.apiKeyHint': 'Se almacena cifrada. Déjalo en blanco para mantener la clave actual.',
'settings.aiParsing.multimodal': 'Enviar documentos como imágenes',
'settings.aiParsing.multimodalHint': 'Para modelos con visión: envía el PDF original en lugar del texto extraído.',
'settings.aiParsing.toast.saved': 'Ajustes de IA guardados',
'settings.aiParsing.toast.saveError': 'No se han podido guardar los ajustes de IA',
};
export default settings;
+15
View File
@@ -335,6 +335,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Tester la connexion',
'settings.airtrail.test.success': 'Connecté — {count} vol(s) trouvé(s)',
'settings.airtrail.test.failed': 'Échec de la connexion',
'settings.aiParsing.title': 'Analyse par IA',
'settings.aiParsing.hint': 'Utilisez votre propre modèle d\'IA pour extraire les réservations des fichiers importés. Cela ne s\'applique que si votre administrateur n\'a pas configuré de modèle pour l\'ensemble de l\'instance.',
'settings.aiParsing.provider': 'Fournisseur',
'settings.aiParsing.providerLocal': 'Local (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Modèle',
'settings.aiParsing.baseUrl': 'URL de base',
'settings.aiParsing.baseUrlHint': 'Emplacement d\'exécution du modèle — un serveur Ollama local ou un point de terminaison compatible OpenAI.',
'settings.aiParsing.apiKey': 'Clé API',
'settings.aiParsing.apiKeyHint': 'Stockée de façon chiffrée. Laissez vide pour conserver la clé actuelle.',
'settings.aiParsing.multimodal': 'Envoyer les documents sous forme d\'images',
'settings.aiParsing.multimodalHint': 'Pour les modèles capables d\'analyser des images — envoie le PDF d\'origine au lieu du texte extrait.',
'settings.aiParsing.toast.saved': 'Paramètres d\'IA enregistrés',
'settings.aiParsing.toast.saveError': 'Impossible d\'enregistrer les paramètres d\'IA',
};
export default settings;
+15
View File
@@ -336,6 +336,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Δοκιμή σύνδεσης',
'settings.airtrail.test.success': 'Συνδέθηκε — βρέθηκαν {count} πτήση/πτήσεις',
'settings.airtrail.test.failed': 'Η σύνδεση απέτυχε',
'settings.aiParsing.title': 'Ανάλυση με AI',
'settings.aiParsing.hint': 'Χρησιμοποιήστε το δικό σας μοντέλο AI για την εξαγωγή κρατήσεων από τα αρχεία που ανεβάζετε. Ισχύει μόνο όταν ο διαχειριστής σας δεν έχει ρυθμίσει μοντέλο για ολόκληρη την εγκατάσταση.',
'settings.aiParsing.provider': 'Πάροχος',
'settings.aiParsing.providerLocal': 'Τοπικό (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Μοντέλο',
'settings.aiParsing.baseUrl': 'Βασικό URL',
'settings.aiParsing.baseUrlHint': 'Πού εκτελείται το μοντέλο — ένας τοπικός διακομιστής Ollama ή ένα τελικό σημείο συμβατό με OpenAI.',
'settings.aiParsing.apiKey': 'Κλειδί API',
'settings.aiParsing.apiKeyHint': 'Αποθηκεύεται κρυπτογραφημένο. Αφήστε το κενό για να διατηρήσετε το τρέχον κλειδί.',
'settings.aiParsing.multimodal': 'Αποστολή εγγράφων ως εικόνες',
'settings.aiParsing.multimodalHint': 'Για μοντέλα με δυνατότητα όρασης — στέλνει το αρχικό PDF αντί για το εξαγόμενο κείμενο.',
'settings.aiParsing.toast.saved': 'Οι ρυθμίσεις AI αποθηκεύτηκαν',
'settings.aiParsing.toast.saveError': 'Δεν ήταν δυνατή η αποθήκευση των ρυθμίσεων AI',
};
export default settings;
+15
View File
@@ -329,6 +329,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Kapcsolat tesztelése',
'settings.airtrail.test.success': 'Csatlakoztatva — {count} járat található',
'settings.airtrail.test.failed': 'A kapcsolat sikertelen',
'settings.aiParsing.title': 'AI-feldolgozás',
'settings.aiParsing.hint': 'Használd a saját AI-modelledet a foglalások kinyeréséhez a feltöltött fájlokból. Ez csak akkor érvényes, ha a rendszergazda nem állított be modellt az egész példányhoz.',
'settings.aiParsing.provider': 'Szolgáltató',
'settings.aiParsing.providerLocal': 'Helyi (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Modell',
'settings.aiParsing.baseUrl': 'Alap-URL',
'settings.aiParsing.baseUrlHint': 'Ahol a modell fut — helyi Ollama-kiszolgáló vagy OpenAI-kompatibilis végpont.',
'settings.aiParsing.apiKey': 'API-kulcs',
'settings.aiParsing.apiKeyHint': 'Titkosítva tárolva. Hagyd üresen a jelenlegi kulcs megtartásához.',
'settings.aiParsing.multimodal': 'Dokumentumok küldése képként',
'settings.aiParsing.multimodalHint': 'Képfelismerésre képes modellekhez — az eredeti PDF-et küldi a kinyert szöveg helyett.',
'settings.aiParsing.toast.saved': 'AI-beállítások elmentve',
'settings.aiParsing.toast.saveError': 'Az AI-beállítások mentése nem sikerült',
};
export default settings;
+15
View File
@@ -328,6 +328,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Uji koneksi',
'settings.airtrail.test.success': 'Terhubung — {count} penerbangan ditemukan',
'settings.airtrail.test.failed': 'Koneksi gagal',
'settings.aiParsing.title': 'Penguraian AI',
'settings.aiParsing.hint': 'Gunakan model AI milikmu sendiri untuk mengekstrak pemesanan dari file yang diunggah. Ini hanya berlaku jika administrator belum mengonfigurasi model untuk seluruh instance.',
'settings.aiParsing.provider': 'Penyedia',
'settings.aiParsing.providerLocal': 'Lokal (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Model',
'settings.aiParsing.baseUrl': 'URL Dasar',
'settings.aiParsing.baseUrlHint': 'Tempat model berjalan — server Ollama lokal atau endpoint yang kompatibel dengan OpenAI.',
'settings.aiParsing.apiKey': 'Kunci API',
'settings.aiParsing.apiKeyHint': 'Disimpan secara terenkripsi. Biarkan kosong untuk mempertahankan kunci saat ini.',
'settings.aiParsing.multimodal': 'Kirim dokumen sebagai gambar',
'settings.aiParsing.multimodalHint': 'Untuk model yang mendukung visi — mengirim PDF asli alih-alih teks yang diekstrak.',
'settings.aiParsing.toast.saved': 'Pengaturan AI disimpan',
'settings.aiParsing.toast.saveError': 'Tidak dapat menyimpan pengaturan AI',
};
export default settings;
+15
View File
@@ -328,6 +328,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Prova connessione',
'settings.airtrail.test.success': 'Connesso — {count} volo/i trovato/i',
'settings.airtrail.test.failed': 'Connessione fallita',
'settings.aiParsing.title': 'Analisi AI',
'settings.aiParsing.hint': 'Usa il tuo modello AI per estrarre le prenotazioni dai file caricati. Vale solo se l\'amministratore non ha configurato un modello per l\'intera istanza.',
'settings.aiParsing.provider': 'Provider',
'settings.aiParsing.providerLocal': 'Locale (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Modello',
'settings.aiParsing.baseUrl': 'URL di base',
'settings.aiParsing.baseUrlHint': 'Dove gira il modello: un server Ollama locale o un endpoint compatibile con OpenAI.',
'settings.aiParsing.apiKey': 'Chiave API',
'settings.aiParsing.apiKeyHint': 'Memorizzata in forma cifrata. Lascia vuoto per mantenere la chiave attuale.',
'settings.aiParsing.multimodal': 'Invia i documenti come immagini',
'settings.aiParsing.multimodalHint': 'Per i modelli con capacità visive: invia il PDF originale invece del testo estratto.',
'settings.aiParsing.toast.saved': 'Impostazioni AI salvate',
'settings.aiParsing.toast.saveError': 'Impossibile salvare le impostazioni AI',
};
export default settings;
+15
View File
@@ -305,6 +305,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': '接続をテスト',
'settings.airtrail.test.success': '接続成功 — {count} 件のフライトが見つかりました',
'settings.airtrail.test.failed': '接続に失敗しました',
'settings.aiParsing.title': 'AI解析',
'settings.aiParsing.hint': 'アップロードしたファイルから予約情報を抽出するために、自分のAIモデルを使用します。これは、管理者がインスタンス全体のモデルを設定していない場合にのみ適用されます。',
'settings.aiParsing.provider': 'プロバイダー',
'settings.aiParsing.providerLocal': 'ローカル (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'モデル',
'settings.aiParsing.baseUrl': 'ベースURL',
'settings.aiParsing.baseUrlHint': 'モデルの実行場所 — ローカルのOllamaサーバー、またはOpenAI互換のエンドポイント。',
'settings.aiParsing.apiKey': 'APIキー',
'settings.aiParsing.apiKeyHint': '暗号化して保存されます。現在のキーを保持する場合は空欄のままにしてください。',
'settings.aiParsing.multimodal': 'ドキュメントを画像として送信',
'settings.aiParsing.multimodalHint': '画像認識対応モデル向け — 抽出したテキストの代わりに元のPDFを送信します。',
'settings.aiParsing.toast.saved': 'AI設定を保存しました',
'settings.aiParsing.toast.saveError': 'AI設定を保存できませんでした',
};
export default settings;
+15
View File
@@ -320,6 +320,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': '연결 테스트',
'settings.airtrail.test.success': '연결됨 — {count}개 항공편을 찾았습니다',
'settings.airtrail.test.failed': '연결에 실패했습니다',
'settings.aiParsing.title': 'AI 분석',
'settings.aiParsing.hint': '업로드한 파일에서 예약 정보를 추출할 때 직접 지정한 AI 모델을 사용하세요. 이 설정은 관리자가 인스턴스 전체에 모델을 설정하지 않은 경우에만 적용됩니다.',
'settings.aiParsing.provider': '제공자',
'settings.aiParsing.providerLocal': '로컬 (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': '모델',
'settings.aiParsing.baseUrl': '기본 URL',
'settings.aiParsing.baseUrlHint': '모델이 실행되는 위치 — 로컬 Ollama 서버 또는 OpenAI 호환 엔드포인트입니다.',
'settings.aiParsing.apiKey': 'API 키',
'settings.aiParsing.apiKeyHint': '암호화되어 저장됩니다. 현재 키를 유지하려면 비워 두세요.',
'settings.aiParsing.multimodal': '문서를 이미지로 전송',
'settings.aiParsing.multimodalHint': '비전 지원 모델용 — 추출된 텍스트 대신 원본 PDF를 전송합니다.',
'settings.aiParsing.toast.saved': 'AI 설정이 저장되었습니다',
'settings.aiParsing.toast.saveError': 'AI 설정을 저장할 수 없습니다',
};
export default settings;
+15
View File
@@ -328,6 +328,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Verbinding testen',
'settings.airtrail.test.success': 'Verbonden — {count} vlucht(en) gevonden',
'settings.airtrail.test.failed': 'Verbinding mislukt',
'settings.aiParsing.title': 'AI-verwerking',
'settings.aiParsing.hint': 'Gebruik je eigen AI-model om boekingen uit geüploade bestanden te halen. Dit geldt alleen als je beheerder geen model voor de hele instantie heeft ingesteld.',
'settings.aiParsing.provider': 'Provider',
'settings.aiParsing.providerLocal': 'Lokaal (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Model',
'settings.aiParsing.baseUrl': 'Basis-URL',
'settings.aiParsing.baseUrlHint': 'Waar het model draait — een lokale Ollama-server of een OpenAI-compatibel endpoint.',
'settings.aiParsing.apiKey': 'API-sleutel',
'settings.aiParsing.apiKeyHint': 'Versleuteld opgeslagen. Laat leeg om de huidige sleutel te behouden.',
'settings.aiParsing.multimodal': 'Documenten als afbeeldingen versturen',
'settings.aiParsing.multimodalHint': 'Voor modellen met beeldherkenning — verstuurt de originele PDF in plaats van geëxtraheerde tekst.',
'settings.aiParsing.toast.saved': 'AI-instellingen opgeslagen',
'settings.aiParsing.toast.saveError': 'AI-instellingen konden niet worden opgeslagen',
};
export default settings;
+15
View File
@@ -329,6 +329,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Testuj połączenie',
'settings.airtrail.test.success': 'Połączono — znaleziono {count} lot(y/ów)',
'settings.airtrail.test.failed': 'Połączenie nieudane',
'settings.aiParsing.title': 'Analiza AI',
'settings.aiParsing.hint': 'Użyj własnego modelu AI, aby wyodrębniać rezerwacje z przesłanych plików. Ma to zastosowanie tylko wtedy, gdy administrator nie skonfigurował modelu dla całej instancji.',
'settings.aiParsing.provider': 'Dostawca',
'settings.aiParsing.providerLocal': 'Lokalny (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Model',
'settings.aiParsing.baseUrl': 'Bazowy adres URL',
'settings.aiParsing.baseUrlHint': 'Miejsce, w którym działa model — lokalny serwer Ollama lub punkt końcowy zgodny z OpenAI.',
'settings.aiParsing.apiKey': 'Klucz API',
'settings.aiParsing.apiKeyHint': 'Przechowywany w postaci zaszyfrowanej. Pozostaw puste, aby zachować bieżący klucz.',
'settings.aiParsing.multimodal': 'Wysyłaj dokumenty jako obrazy',
'settings.aiParsing.multimodalHint': 'Dla modeli obsługujących obraz — wysyła oryginalny plik PDF zamiast wyodrębnionego tekstu.',
'settings.aiParsing.toast.saved': 'Zapisano ustawienia AI',
'settings.aiParsing.toast.saveError': 'Nie udało się zapisać ustawień AI',
};
export default settings;
+15
View File
@@ -330,6 +330,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Проверить подключение',
'settings.airtrail.test.success': 'Подключено — найдено рейсов: {count}',
'settings.airtrail.test.failed': 'Не удалось подключиться',
'settings.aiParsing.title': 'Распознавание с помощью ИИ',
'settings.aiParsing.hint': 'Используйте собственную модель ИИ для извлечения бронирований из загруженных файлов. Это работает только в том случае, если администратор не настроил модель для всего экземпляра.',
'settings.aiParsing.provider': 'Провайдер',
'settings.aiParsing.providerLocal': 'Локально (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Модель',
'settings.aiParsing.baseUrl': 'Базовый URL',
'settings.aiParsing.baseUrlHint': 'Где работает модель — локальный сервер Ollama или конечная точка, совместимая с OpenAI.',
'settings.aiParsing.apiKey': 'Ключ API',
'settings.aiParsing.apiKeyHint': 'Хранится в зашифрованном виде. Оставьте поле пустым, чтобы сохранить текущий ключ.',
'settings.aiParsing.multimodal': 'Отправлять документы как изображения',
'settings.aiParsing.multimodalHint': 'Для моделей с поддержкой изображений — отправляет исходный PDF вместо извлечённого текста.',
'settings.aiParsing.toast.saved': 'Настройки ИИ сохранены',
'settings.aiParsing.toast.saveError': 'Не удалось сохранить настройки ИИ',
};
export default settings;
+15
View File
@@ -325,6 +325,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Bağlantıyı test et',
'settings.airtrail.test.success': 'Bağlandı — {count} uçuş bulundu',
'settings.airtrail.test.failed': 'Bağlantı başarısız',
'settings.aiParsing.title': 'Yapay zekâ ayrıştırma',
'settings.aiParsing.hint': 'Yüklenen dosyalardan rezervasyonları çıkarmak için kendi yapay zekâ modelini kullan. Bu yalnızca yöneticin tüm uygulama geneli için bir model yapılandırmadığında geçerlidir.',
'settings.aiParsing.provider': 'Sağlayıcı',
'settings.aiParsing.providerLocal': 'Yerel (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Model',
'settings.aiParsing.baseUrl': 'Temel URL',
'settings.aiParsing.baseUrlHint': 'Modelin çalıştığı yer — yerel bir Ollama sunucusu veya OpenAI uyumlu bir uç nokta.',
'settings.aiParsing.apiKey': 'API anahtarı',
'settings.aiParsing.apiKeyHint': 'Şifrelenmiş olarak saklanır. Mevcut anahtarı korumak için boş bırak.',
'settings.aiParsing.multimodal': 'Belgeleri görsel olarak gönder',
'settings.aiParsing.multimodalHint': 'Görüntü işleyebilen modeller için — çıkarılan metin yerine orijinal PDF\'yi gönderir.',
'settings.aiParsing.toast.saved': 'Yapay zekâ ayarları kaydedildi',
'settings.aiParsing.toast.saveError': 'Yapay zekâ ayarları kaydedilemedi',
};
export default settings;
+15
View File
@@ -329,6 +329,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': 'Перевірити підключення',
'settings.airtrail.test.success': 'Підключено — знайдено {count} рейс(ів)',
'settings.airtrail.test.failed': 'Не вдалося підключитися',
'settings.aiParsing.title': 'Розпізнавання ШІ',
'settings.aiParsing.hint': 'Використовуйте власну модель ШІ для вилучення бронювань із завантажених файлів. Це діє лише тоді, коли адміністратор не налаштував модель для всього екземпляра.',
'settings.aiParsing.provider': 'Постачальник',
'settings.aiParsing.providerLocal': 'Локальний (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': 'Модель',
'settings.aiParsing.baseUrl': 'Базова URL-адреса',
'settings.aiParsing.baseUrlHint': 'Де працює модель — локальний сервер Ollama або сумісна з OpenAI кінцева точка.',
'settings.aiParsing.apiKey': 'Ключ API',
'settings.aiParsing.apiKeyHint': 'Зберігається в зашифрованому вигляді. Залиште порожнім, щоб зберегти поточний ключ.',
'settings.aiParsing.multimodal': 'Надсилати документи як зображення',
'settings.aiParsing.multimodalHint': 'Для моделей із підтримкою зображень — надсилає оригінальний PDF замість вилученого тексту.',
'settings.aiParsing.toast.saved': 'Налаштування ШІ збережено',
'settings.aiParsing.toast.saveError': 'Не вдалося зберегти налаштування ШІ',
};
export default settings;
+15
View File
@@ -309,6 +309,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': '測試連接',
'settings.airtrail.test.success': '已連接——找到 {count} 筆航班',
'settings.airtrail.test.failed': '連接失敗',
'settings.aiParsing.title': 'AI 解析',
'settings.aiParsing.hint': '使用你自己的 AI 模型,從上傳的檔案中擷取預訂資訊。此設定僅在管理員尚未為整個執行個體設定模型時才會生效。',
'settings.aiParsing.provider': '供應商',
'settings.aiParsing.providerLocal': '本機 (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': '模型',
'settings.aiParsing.baseUrl': '基礎網址',
'settings.aiParsing.baseUrlHint': '模型執行的位置 — 本機 Ollama 伺服器,或相容於 OpenAI 的端點。',
'settings.aiParsing.apiKey': 'API 金鑰',
'settings.aiParsing.apiKeyHint': '以加密方式儲存。留空即可保留目前的金鑰。',
'settings.aiParsing.multimodal': '以圖片形式傳送文件',
'settings.aiParsing.multimodalHint': '適用於具備視覺能力的模型 — 傳送原始 PDF,而非擷取出的文字。',
'settings.aiParsing.toast.saved': 'AI 設定已儲存',
'settings.aiParsing.toast.saveError': '無法儲存 AI 設定',
};
export default settings;
+15
View File
@@ -307,6 +307,21 @@ const settings: TranslationStrings = {
'settings.airtrail.test.button': '测试连接',
'settings.airtrail.test.success': '已连接——找到 {count} 个航班',
'settings.airtrail.test.failed': '连接失败',
'settings.aiParsing.title': 'AI 解析',
'settings.aiParsing.hint': '使用你自己的 AI 模型从上传的文件中提取预订信息。仅当管理员未为整个实例配置模型时才会生效。',
'settings.aiParsing.provider': '服务商',
'settings.aiParsing.providerLocal': '本地 (Ollama)',
'settings.aiParsing.providerOpenai': 'OpenAI',
'settings.aiParsing.providerAnthropic': 'Anthropic',
'settings.aiParsing.model': '模型',
'settings.aiParsing.baseUrl': '基础 URL',
'settings.aiParsing.baseUrlHint': '模型运行的位置——本地 Ollama 服务器或兼容 OpenAI 的接口。',
'settings.aiParsing.apiKey': 'API 密钥',
'settings.aiParsing.apiKeyHint': '加密存储。留空则保留当前密钥。',
'settings.aiParsing.multimodal': '以图像形式发送文档',
'settings.aiParsing.multimodalHint': '适用于支持视觉的模型——发送原始 PDF 而非提取的文本。',
'settings.aiParsing.toast.saved': 'AI 设置已保存',
'settings.aiParsing.toast.saveError': '无法保存 AI 设置',
};
export default settings;