- 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.
321 lines
11 KiB
Vue
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>
|