fix(chat): add saved AI defaults and harden suggestions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from '../$types';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
import type { ImmichIntegration, User } from '$lib/types';
|
||||
import type { ImmichIntegration, User, UserAISettings } from '$lib/types';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
@@ -95,6 +95,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
|
||||
let apiKeys: UserAPIKey[] = [];
|
||||
let apiKeysConfigError: string | null = null;
|
||||
let aiSettings: UserAISettings | null = null;
|
||||
let apiKeysFetch = await fetch(`${endpoint}/api/integrations/api-keys/`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
@@ -108,6 +109,17 @@ export const load: PageServerLoad = async (event) => {
|
||||
apiKeysConfigError = errorBody.detail ?? 'API key storage is currently unavailable.';
|
||||
}
|
||||
|
||||
let aiSettingsFetch = await fetch(`${endpoint}/api/integrations/ai-settings/`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
|
||||
if (aiSettingsFetch.ok) {
|
||||
const aiSettingsResponse = (await aiSettingsFetch.json()) as UserAISettings[];
|
||||
aiSettings = aiSettingsResponse[0] ?? null;
|
||||
}
|
||||
|
||||
let publicUrlFetch = await fetch(`${endpoint}/public-url/`);
|
||||
let publicUrl = '';
|
||||
if (!publicUrlFetch.ok) {
|
||||
@@ -131,6 +143,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
stravaUserEnabled,
|
||||
apiKeys,
|
||||
apiKeysConfigError,
|
||||
aiSettings,
|
||||
wandererEnabled,
|
||||
wandererExpired
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
let apiKeysConfigError: string | null = data.props.apiKeysConfigError ?? null;
|
||||
let newApiKeyProvider = 'anthropic';
|
||||
let providerCatalog: ChatProviderCatalogEntry[] = [];
|
||||
let defaultAiProvider = (data.props.aiSettings?.preferred_provider ?? '').trim().toLowerCase();
|
||||
let defaultAiModel = (data.props.aiSettings?.preferred_model ?? '').trim();
|
||||
let defaultAiModels: string[] = [];
|
||||
let defaultAiModelsLoading = false;
|
||||
let isSavingDefaultAiSettings = false;
|
||||
let newApiKeyValue = '';
|
||||
let isSavingApiKey = false;
|
||||
let deletingApiKeyId: string | null = null;
|
||||
@@ -70,6 +75,104 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getConfiguredChatProviders() {
|
||||
return providerCatalog.filter(
|
||||
(provider) =>
|
||||
provider.available_for_chat && (provider.user_configured || provider.instance_configured)
|
||||
);
|
||||
}
|
||||
|
||||
async function loadDefaultAiModels(providerId: string) {
|
||||
if (!providerId) {
|
||||
defaultAiModels = [];
|
||||
return;
|
||||
}
|
||||
|
||||
defaultAiModelsLoading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/chat/providers/${providerId}/models/`);
|
||||
if (!res.ok) {
|
||||
defaultAiModels = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
defaultAiModels = Array.isArray(payload.models) ? (payload.models as string[]) : [];
|
||||
} catch {
|
||||
defaultAiModels = [];
|
||||
} finally {
|
||||
defaultAiModelsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeDefaultAiSettings() {
|
||||
const configuredProviders = getConfiguredChatProviders();
|
||||
if (!configuredProviders.length) {
|
||||
defaultAiProvider = '';
|
||||
defaultAiModel = '';
|
||||
defaultAiModels = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!defaultAiProvider ||
|
||||
!configuredProviders.some((provider) => provider.id === defaultAiProvider)
|
||||
) {
|
||||
defaultAiProvider = configuredProviders[0].id;
|
||||
defaultAiModel = '';
|
||||
}
|
||||
|
||||
await loadDefaultAiModels(defaultAiProvider);
|
||||
if (defaultAiModel && !defaultAiModels.includes(defaultAiModel)) {
|
||||
defaultAiModel = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function onDefaultAiProviderChange() {
|
||||
defaultAiModel = '';
|
||||
await loadDefaultAiModels(defaultAiProvider);
|
||||
}
|
||||
|
||||
async function saveDefaultAiSettings(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (!defaultAiProvider) {
|
||||
addToast('error', $t('settings.default_ai_provider_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
isSavingDefaultAiSettings = true;
|
||||
try {
|
||||
const res = await fetch('/api/integrations/ai-settings/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
preferred_provider: defaultAiProvider,
|
||||
preferred_model: defaultAiModel || null
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
addToast('error', $t('settings.default_ai_settings_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = await res.json();
|
||||
defaultAiProvider = (saved.preferred_provider ?? '').trim().toLowerCase();
|
||||
defaultAiModel = (saved.preferred_model ?? '').trim();
|
||||
await loadDefaultAiModels(defaultAiProvider);
|
||||
if (defaultAiModel && !defaultAiModels.includes(defaultAiModel)) {
|
||||
defaultAiModel = '';
|
||||
}
|
||||
addToast('success', $t('settings.default_ai_settings_saved'));
|
||||
} catch {
|
||||
addToast('error', $t('settings.default_ai_settings_error'));
|
||||
} finally {
|
||||
isSavingDefaultAiSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getApiKeyProviderLabel(provider: string): string {
|
||||
const catalogProvider = providerCatalog.find((entry) => entry.id === provider);
|
||||
if (catalogProvider) {
|
||||
@@ -133,7 +236,8 @@
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
void loadProviderCatalog();
|
||||
await loadProviderCatalog();
|
||||
await initializeDefaultAiSettings();
|
||||
|
||||
if (browser) {
|
||||
const queryParams = new URLSearchParams($page.url.search);
|
||||
@@ -1570,6 +1674,71 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">
|
||||
{$t('settings.default_ai_settings_title')}
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
{$t('settings.default_ai_settings_desc')}
|
||||
</p>
|
||||
|
||||
{#if getConfiguredChatProviders().length === 0}
|
||||
<div class="alert alert-warning">
|
||||
<span>{$t('settings.default_ai_no_providers')}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<form class="space-y-4" on:submit={saveDefaultAiSettings}>
|
||||
<div class="form-control">
|
||||
<label class="label" for="default-ai-provider">
|
||||
<span class="label-text font-medium">{$t('settings.provider')}</span>
|
||||
</label>
|
||||
<select
|
||||
id="default-ai-provider"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={defaultAiProvider}
|
||||
on:change={onDefaultAiProviderChange}
|
||||
>
|
||||
{#each getConfiguredChatProviders() as provider}
|
||||
<option value={provider.id}>{provider.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="default-ai-model">
|
||||
<span class="label-text font-medium">{$t('chat.model_label')}</span>
|
||||
</label>
|
||||
<select
|
||||
id="default-ai-model"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={defaultAiModel}
|
||||
disabled={defaultAiModelsLoading}
|
||||
>
|
||||
<option value="">{$t('chat.model_placeholder')}</option>
|
||||
{#if defaultAiModelsLoading}
|
||||
<option value="" disabled>Loading...</option>
|
||||
{:else}
|
||||
{#each defaultAiModels as model}
|
||||
<option value={model}>{model}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={isSavingDefaultAiSettings}
|
||||
>
|
||||
{#if isSavingDefaultAiSettings}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
{$t('settings.default_ai_save')}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">MCP Access Token</h3>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
|
||||
Reference in New Issue
Block a user