Files
yourpart3/frontend/src/views/settings/LanguageAssistantView.vue
Torsten Schulz (local) d8d2430329 feat(MiniCalendarWidget): enhance localization and refactor calendar logic
- Updated the MiniCalendarWidget to utilize localized strings for month names, weekdays, and loading messages, improving user experience across multiple languages.
- Refactored the calendar logic to use constants for month keys and weekday order, ensuring consistency with backend configurations.
- Added new translation entries for the mini widget in Cebuano, German, English, Spanish, and French, enhancing accessibility for users.
2026-04-17 16:44:27 +02:00

321 lines
11 KiB
Vue

<template>
<div class="language-assistant-settings">
<section class="language-assistant-settings__hero surface-card">
<span class="language-assistant-settings__eyebrow">{{ $t('settings.languageAssistant.eyebrow') }}</span>
<h2>{{ $t('settings.languageAssistant.title') }}</h2>
<p class="language-assistant-settings__intro">{{ $t('settings.languageAssistant.intro') }}</p>
<ul class="language-assistant-settings__links">
<li>
<a href="https://platform.openai.com/signup" target="_blank" rel="noopener noreferrer">
{{ $t('settings.languageAssistant.linkSignup') }}
</a>
</li>
<li>
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
{{ $t('settings.languageAssistant.linkApiKeys') }}
</a>
</li>
</ul>
</section>
<section v-if="loadError" class="surface-card language-assistant-settings__panel">
<p class="form-error">{{ loadError }}</p>
</section>
<section v-else class="language-assistant-settings__panel surface-card">
<label class="language-assistant-settings__toggle">
<input type="checkbox" v-model="form.enabled" />
<span>{{ $t('settings.languageAssistant.enabled') }}</span>
</label>
<div class="language-assistant-settings__presets">
<p class="language-assistant-settings__presets-title">{{ $t('settings.languageAssistant.presetsTitle') }}</p>
<div class="language-assistant-settings__preset-actions">
<button type="button" class="preset-btn" @click="applyOllamaPreset">
{{ $t('settings.languageAssistant.presetOllama') }}
</button>
<button type="button" class="preset-btn preset-btn--secondary" @click="applyOpenAiPreset">
{{ $t('settings.languageAssistant.presetOpenAi') }}
</button>
</div>
<p class="language-assistant-settings__preset-hint">
{{ $t('settings.languageAssistant.presetHintBefore') }}<code>{{ ollamaPresetBaseUrl }}</code>{{ $t('settings.languageAssistant.presetHintBetween') }}<code>{{ ollamaPresetModel }}</code>{{ $t('settings.languageAssistant.presetHintAfter') }}
</p>
</div>
<div class="language-assistant-settings__grid">
<label class="language-assistant-settings__field">
<span>{{ $t('settings.languageAssistant.baseUrl') }}</span>
<input
v-model="form.baseUrl"
type="url"
autocomplete="off"
:placeholder="$t('settings.languageAssistant.baseUrlPlaceholder')"
/>
</label>
<label class="language-assistant-settings__field">
<span>{{ $t('settings.languageAssistant.model') }}</span>
<input v-model="form.model" type="text" autocomplete="off" :placeholder="$t('settings.languageAssistant.modelPlaceholder')" />
</label>
<label class="language-assistant-settings__field language-assistant-settings__field--full">
<span>{{ $t('settings.languageAssistant.apiKey') }}</span>
<input
v-model="form.apiKey"
type="password"
autocomplete="new-password"
:placeholder="apiKeyPlaceholder"
/>
<span class="language-assistant-settings__hint">{{ $t('settings.languageAssistant.apiKeyHint') }}</span>
<span class="language-assistant-settings__hint" :class="`language-assistant-settings__key-status language-assistant-settings__key-status--${keyStatus}`">
{{ keyStatusLabel }}
</span>
</label>
</div>
<label class="language-assistant-settings__toggle">
<input type="checkbox" v-model="form.clearKey" />
<span>{{ $t('settings.languageAssistant.clearKey') }}</span>
</label>
<div class="language-assistant-settings__actions">
<button type="button" :disabled="saving" @click="save">{{ $t('settings.languageAssistant.save') }}</button>
</div>
</section>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
import { mapGetters } from 'vuex';
import { showApiError, showError, showSuccess } from '@/utils/feedback.js';
const OLLAMA_DEFAULT_BASE_URL = 'http://127.0.0.1:11434/v1';
const OLLAMA_DEFAULT_MODEL = 'qwen2.5:3b-instruct';
const DEFAULT_OPENAI_MODEL = 'gpt-4o-mini';
export default {
name: 'LanguageAssistantSettingsView',
data() {
return {
form: {
enabled: true,
baseUrl: '',
model: 'gpt-4o-mini',
apiKey: '',
clearKey: false
},
hasKey: false,
keyLast4: null,
keyStatus: 'missing',
saving: false,
loadError: null
};
},
computed: {
...mapGetters(['user']),
ollamaPresetBaseUrl() {
return OLLAMA_DEFAULT_BASE_URL;
},
ollamaPresetModel() {
return OLLAMA_DEFAULT_MODEL;
},
apiKeyPlaceholder() {
if (this.form.clearKey) {
return this.$t('settings.languageAssistant.apiKeyPlaceholderClear');
}
if (this.hasKey) {
return this.$t('settings.languageAssistant.apiKeyPlaceholderHasKey', {
last4: this.keyLast4 || '••••'
});
}
return this.$t('settings.languageAssistant.apiKeyPlaceholderNew');
},
keyStatusLabel() {
const statusKey = {
stored: 'settings.languageAssistant.keyStatusStored',
invalid: 'settings.languageAssistant.keyStatusInvalid',
missing: 'settings.languageAssistant.keyStatusMissing'
}[this.keyStatus] || 'settings.languageAssistant.keyStatusMissing';
return this.$t(statusKey);
}
},
async mounted() {
await this.load();
},
methods: {
applyOllamaPreset() {
this.form.enabled = true;
this.form.baseUrl = OLLAMA_DEFAULT_BASE_URL;
this.form.model = OLLAMA_DEFAULT_MODEL;
this.form.apiKey = '';
this.form.clearKey = false;
},
applyOpenAiPreset() {
this.form.enabled = true;
this.form.baseUrl = '';
this.form.model = DEFAULT_OPENAI_MODEL;
this.form.clearKey = false;
},
async load() {
this.loadError = null;
try {
const { data } = await apiClient.get('/api/settings/llm');
this.form.enabled = data.enabled !== false;
this.form.baseUrl = data.baseUrl || '';
this.form.model = data.model || DEFAULT_OPENAI_MODEL;
this.hasKey = data.hasKey;
this.keyLast4 = data.keyLast4;
this.keyStatus = data.keyStatus || (data.hasKey ? 'stored' : 'missing');
this.form.apiKey = '';
this.form.clearKey = false;
} catch (e) {
this.loadError = e.response?.data?.error || e.message || 'Error';
}
},
async save() {
if (this.form.clearKey && !window.confirm(this.$t('settings.languageAssistant.confirmClear'))) {
return;
}
this.saving = true;
try {
await apiClient.post('/api/settings/llm', {
enabled: this.form.enabled,
baseUrl: this.form.baseUrl,
model: this.form.model,
apiKey: this.form.apiKey,
clearKey: this.form.clearKey
});
showSuccess(this, this.$t('settings.languageAssistant.saved'));
this.form.apiKey = '';
this.form.clearKey = false;
await this.load();
} catch (e) {
showApiError(this, e, () => showError(this, this.$t('settings.languageAssistant.saveError')));
} finally {
this.saving = false;
}
}
}
};
</script>
<style scoped>
.language-assistant-settings__hero {
padding: 1.25rem 1.5rem;
margin-bottom: 1rem;
}
.language-assistant-settings__eyebrow {
display: block;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.75;
margin-bottom: 0.35rem;
}
.language-assistant-settings__intro {
margin: 0.5rem 0 1rem;
line-height: 1.5;
}
.language-assistant-settings__links {
margin: 0;
padding-left: 1.25rem;
}
.language-assistant-settings__links a {
color: var(--color-link, #3b82f6);
}
.language-assistant-settings__panel {
padding: 1.25rem 1.5rem;
}
.language-assistant-settings__presets {
margin-top: 1rem;
padding: 0.85rem 1rem;
border: 1px solid var(--color-border, #ccc);
border-radius: 8px;
background: color-mix(in srgb, var(--color-input-bg, #fff) 85%, #f6f8ff);
}
.language-assistant-settings__presets-title {
margin: 0 0 0.5rem;
font-weight: 700;
}
.language-assistant-settings__preset-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.preset-btn {
border: 1px solid var(--color-border, #ccc);
border-radius: 6px;
padding: 0.45rem 0.75rem;
cursor: pointer;
background: var(--color-input-bg, #fff);
color: inherit;
font-weight: 600;
}
.preset-btn--secondary {
opacity: 0.9;
}
.language-assistant-settings__preset-hint {
margin: 0.55rem 0 0;
font-size: 0.82rem;
opacity: 0.85;
}
.language-assistant-settings__grid {
display: grid;
gap: 1rem;
margin-top: 1rem;
}
@media (min-width: 640px) {
.language-assistant-settings__grid {
grid-template-columns: 1fr 1fr;
}
.language-assistant-settings__field--full {
grid-column: 1 / -1;
}
}
.language-assistant-settings__field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.language-assistant-settings__field span:first-child {
font-weight: 600;
font-size: 0.9rem;
}
.language-assistant-settings__field input {
padding: 0.5rem 0.65rem;
border-radius: 6px;
border: 1px solid var(--color-border, #ccc);
background: var(--color-input-bg, #fff);
color: inherit;
}
.language-assistant-settings__hint {
font-size: 0.8rem;
opacity: 0.8;
}
.language-assistant-settings__key-status {
font-weight: 600;
}
.language-assistant-settings__key-status--stored {
color: #2e7d32;
}
.language-assistant-settings__key-status--invalid {
color: #c62828;
}
.language-assistant-settings__key-status--missing {
color: #8a6d3b;
}
.language-assistant-settings__toggle {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
cursor: pointer;
}
.language-assistant-settings__actions {
margin-top: 1.25rem;
}
.form-error {
color: #c62828;
}
</style>