From 91d907204ab6c7a119ba436e597f8563710de72f Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 9 Mar 2026 00:20:11 +0000 Subject: [PATCH] fix(ai): critical fixes for agent-redesign - provider selection and auto-learn Fix 1: Provider/Model Selection (Critical - unblocks LLM) - Add /api/chat/providers/{id}/models/ endpoint to fetch available models - Auto-select first configured provider instead of hardcoded 'openai' - Add model dropdown populated from provider API - Filter provider list to only show configured providers - Show helpful error when no providers configured Fix 2: Auto-Learn Preferences (Replaces manual input) - Create auto_profile.py utility to infer preferences from user data - Learn interests from Activity sport types and Location categories - Learn trip style from Lodging types (hostel=budget, resort=luxury, etc.) - Learn geographic preferences from VisitedRegion/VisitedCity - Call auto-learn on every chat start (send_message) - System prompt now indicates preferences are auto-inferred Fix 3: Remove Manual Preference UI - Remove travel_preferences section from Settings - Remove preference form fields and save logic - Remove preference fetch from server-side load - Keep UserRecommendationPreferenceProfile type for backend use The LLM should now work correctly: - Users with any configured provider will have it auto-selected - Model list is fetched dynamically from provider API - Preferences are learned from actual travel history --- backend/server/chat/llm_client.py | 31 +- backend/server/chat/views/__init__.py | 99 ++++ .../{utils.py => utils/__init__.py} | 5 +- .../server/integrations/utils/auto_profile.py | 168 ++++++ .../src/lib/components/AITravelChat.svelte | 515 ++++++++++-------- frontend/src/lib/types.ts | 2 + frontend/src/routes/settings/+page.server.ts | 27 +- frontend/src/routes/settings/+page.svelte | 148 ----- 8 files changed, 587 insertions(+), 408 deletions(-) rename backend/server/integrations/{utils.py => utils/__init__.py} (67%) create mode 100644 backend/server/integrations/utils/auto_profile.py diff --git a/backend/server/chat/llm_client.py b/backend/server/chat/llm_client.py index 2549df58..a4939e5e 100644 --- a/backend/server/chat/llm_client.py +++ b/backend/server/chat/llm_client.py @@ -335,25 +335,22 @@ Be conversational, helpful, and enthusiastic about travel. Keep responses concis else: try: profile = UserRecommendationPreferenceProfile.objects.get(user=user) - preference_lines = [] - if profile.cuisines: - preference_lines.append( - f"🍽️ **Cuisine Preferences**: {profile.cuisines}" - ) - if profile.interests: - preference_lines.append( - f"🎯 **Interests**: {_format_interests(profile.interests)}" - ) - if profile.trip_style: - preference_lines.append(f"✈️ **Travel Style**: {profile.trip_style}") - if profile.notes: - preference_lines.append(f"πŸ“ **Additional Notes**: {profile.notes}") + if profile.interests or profile.trip_style or profile.notes: + base_prompt += "\n\n## Traveler Preferences\n" + base_prompt += "*(Automatically inferred from travel history)*\n\n" - if preference_lines: - base_prompt += "\n\n## Traveler Preferences\n" + "\n".join( - preference_lines - ) + if profile.interests: + interests_str = ( + ", ".join(profile.interests) + if isinstance(profile.interests, list) + else str(profile.interests) + ) + base_prompt += f"🎯 **Interests**: {interests_str}\n" + if profile.trip_style: + base_prompt += f"✈️ **Travel Style**: {profile.trip_style}\n" + if profile.notes: + base_prompt += f"πŸ“ **Patterns**: {profile.notes}\n" except UserRecommendationPreferenceProfile.DoesNotExist: pass diff --git a/backend/server/chat/views/__init__.py b/backend/server/chat/views/__init__.py index cde13be9..9729c260 100644 --- a/backend/server/chat/views/__init__.py +++ b/backend/server/chat/views/__init__.py @@ -1,5 +1,6 @@ import asyncio import json +import logging from asgiref.sync import sync_to_async from adventures.models import Collection @@ -19,6 +20,8 @@ from ..llm_client import ( from ..models import ChatConversation, ChatMessage from ..serializers import ChatConversationSerializer +logger = logging.getLogger(__name__) + class ChatViewSet(viewsets.ModelViewSet): serializer_class = ChatConversationSerializer @@ -108,6 +111,15 @@ class ChatViewSet(viewsets.ModelViewSet): @action(detail=True, methods=["post"]) def send_message(self, request, pk=None): + # Auto-learn preferences from user's travel history + from integrations.utils.auto_profile import update_auto_preference_profile + + try: + update_auto_preference_profile(request.user) + except Exception as exc: + logger.warning("Auto-profile update failed: %s", exc) + # Continue anyway - not critical + conversation = self.get_object() user_content = (request.data.get("message") or "").strip() if not user_content: @@ -323,6 +335,93 @@ class ChatProviderCatalogViewSet(viewsets.ViewSet): def list(self, request): return Response(get_provider_catalog(user=request.user)) + @action(detail=True, methods=["get"]) + def models(self, request, pk=None): + """Fetch available models from a provider's API.""" + from chat.llm_client import get_llm_api_key + + provider = (pk or "").lower() + + api_key = get_llm_api_key(request.user, provider) + if not api_key: + return Response( + {"error": "No API key configured for this provider"}, + status=status.HTTP_403_FORBIDDEN, + ) + + try: + if provider == "openai": + import openai + + client = openai.OpenAI(api_key=api_key) + models = client.models.list() + chat_models = [ + model.id + for model in models + if any(prefix in model.id for prefix in ["gpt-", "o1-", "chatgpt"]) + ] + return Response({"models": sorted(set(chat_models), reverse=True)}) + + if provider in ["anthropic", "claude"]: + return Response( + { + "models": [ + "claude-sonnet-4-20250514", + "claude-opus-4-20250514", + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + "claude-3-haiku-20240307", + ] + } + ) + + if provider in ["gemini", "google"]: + return Response( + { + "models": [ + "gemini-2.0-flash", + "gemini-1.5-pro", + "gemini-1.5-flash", + "gemini-1.5-flash-8b", + ] + } + ) + + if provider in ["groq"]: + return Response( + { + "models": [ + "llama-3.3-70b-versatile", + "llama-3.1-70b-versatile", + "llama-3.1-8b-instant", + "mixtral-8x7b-32768", + ] + } + ) + + if provider in ["ollama"]: + import requests + + try: + response = requests.get( + "http://localhost:11434/api/tags", timeout=5 + ) + if response.ok: + data = response.json() + models = [item["name"] for item in data.get("models", [])] + return Response({"models": models}) + except Exception: + pass + return Response({"models": []}) + + return Response({"models": []}) + except Exception as exc: + logger.error("Failed to fetch models for %s: %s", provider, exc) + return Response( + {"error": f"Failed to fetch models: {str(exc)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + from .capabilities import CapabilitiesView from .day_suggestions import DaySuggestionsView diff --git a/backend/server/integrations/utils.py b/backend/server/integrations/utils/__init__.py similarity index 67% rename from backend/server/integrations/utils.py rename to backend/server/integrations/utils/__init__.py index 43371904..c5cb264f 100644 --- a/backend/server/integrations/utils.py +++ b/backend/server/integrations/utils/__init__.py @@ -1,6 +1,7 @@ from rest_framework.pagination import PageNumberPagination + class StandardResultsSetPagination(PageNumberPagination): page_size = 25 - page_size_query_param = 'page_size' - max_page_size = 1000 \ No newline at end of file + page_size_query_param = "page_size" + max_page_size = 1000 diff --git a/backend/server/integrations/utils/auto_profile.py b/backend/server/integrations/utils/auto_profile.py new file mode 100644 index 00000000..829c5596 --- /dev/null +++ b/backend/server/integrations/utils/auto_profile.py @@ -0,0 +1,168 @@ +""" +Auto-learn user preferences from their travel history. +""" + +import logging + +from django.db.models import Count + +from adventures.models import Activity, Location, Lodging +from integrations.models import UserRecommendationPreferenceProfile +from worldtravel.models import VisitedCity, VisitedRegion + +logger = logging.getLogger(__name__) + + +# Mapping of lodging types to travel styles +LODGING_STYLE_MAP = { + "hostel": "budget", + "campground": "outdoor", + "cabin": "outdoor", + "camping": "outdoor", + "resort": "luxury", + "villa": "luxury", + "hotel": "comfort", + "apartment": "independent", + "bnb": "local", + "boat": "adventure", +} + +# Activity sport types to interest categories +ACTIVITY_INTEREST_MAP = { + "hiking": "hiking & nature", + "walking": "walking tours", + "running": "fitness", + "cycling": "cycling", + "swimming": "water sports", + "surfing": "water sports", + "kayaking": "water sports", + "skiing": "winter sports", + "snowboarding": "winter sports", + "climbing": "adventure sports", +} + + +def build_auto_preference_profile(user) -> dict: + """ + Automatically build preference profile from user's existing data. + + Analyzes: + - Activities (sport types) β†’ interests + - Location categories β†’ interests + - Lodging types β†’ trip style + - Visited regions/cities β†’ geographic preferences + + Returns dict with: cuisines, interests, trip_style, notes + """ + profile = { + "cuisines": None, + "interests": [], + "trip_style": None, + "notes": None, + } + + try: + activity_interests = ( + Activity.objects.filter(user=user) + .values("sport_type") + .annotate(count=Count("id")) + .exclude(sport_type__isnull=True) + .exclude(sport_type="") + .order_by("-count")[:5] + ) + + for activity in activity_interests: + sport = activity["sport_type"] + if sport: + interest = ACTIVITY_INTEREST_MAP.get( + sport.lower(), sport.replace("_", " ") + ) + if interest not in profile["interests"]: + profile["interests"].append(interest) + + category_interests = ( + Location.objects.filter(user=user) + .values("category__name") + .annotate(count=Count("id")) + .exclude(category__name__isnull=True) + .exclude(category__name="") + .order_by("-count")[:5] + ) + + for category in category_interests: + category_name = category["category__name"] + if category_name and category_name.lower() not in [ + i.lower() for i in profile["interests"] + ]: + profile["interests"].append(category_name) + + top_lodging = ( + Lodging.objects.filter(user=user) + .values("type") + .annotate(count=Count("id")) + .exclude(type__isnull=True) + .exclude(type="") + .order_by("-count") + .first() + ) + + if top_lodging and top_lodging["type"]: + lodging_type = top_lodging["type"].lower() + profile["trip_style"] = LODGING_STYLE_MAP.get(lodging_type, lodging_type) + + top_regions = list( + VisitedRegion.objects.filter(user=user) + .values("region__name") + .annotate(count=Count("id")) + .exclude(region__name__isnull=True) + .order_by("-count")[:3] + ) + + if top_regions: + region_names = [r["region__name"] for r in top_regions if r["region__name"]] + if region_names: + profile["notes"] = f"Frequently visits: {', '.join(region_names)}" + + if not profile["notes"]: + top_cities = list( + VisitedCity.objects.filter(user=user) + .values("city__name") + .annotate(count=Count("id")) + .exclude(city__name__isnull=True) + .order_by("-count")[:3] + ) + if top_cities: + city_names = [c["city__name"] for c in top_cities if c["city__name"]] + if city_names: + profile["notes"] = f"Frequently visits: {', '.join(city_names)}" + + profile["interests"] = profile["interests"][:8] + except Exception as exc: + logger.error("Error building auto profile for user %s: %s", user.id, exc) + + return profile + + +def update_auto_preference_profile(user) -> UserRecommendationPreferenceProfile: + """ + Build and save auto-learned profile to database. + Called automatically when chat starts. + """ + auto_data = build_auto_preference_profile(user) + + profile, created = UserRecommendationPreferenceProfile.objects.update_or_create( + user=user, + defaults={ + "cuisines": auto_data["cuisines"], + "interests": auto_data["interests"], + "trip_style": auto_data["trip_style"], + "notes": auto_data["notes"], + }, + ) + + logger.info( + "%s auto profile for user %s", + "Created" if created else "Updated", + user.id, + ) + return profile diff --git a/frontend/src/lib/components/AITravelChat.svelte b/frontend/src/lib/components/AITravelChat.svelte index e20c4fd7..41c13c0b 100644 --- a/frontend/src/lib/components/AITravelChat.svelte +++ b/frontend/src/lib/components/AITravelChat.svelte @@ -31,6 +31,11 @@ tool_results?: ToolResultEntry[]; }; + type ChatProviderCatalogConfiguredEntry = ChatProviderCatalogEntry & { + instance_configured: boolean; + user_configured: boolean; + }; + export let embedded = false; export let collectionId: string | undefined = undefined; export let collectionName: string | undefined = undefined; @@ -46,15 +51,15 @@ let sidebarOpen = true; let streamingContent = ''; - let selectedProvider = 'openai'; + let selectedProvider = ''; let selectedModel = ''; - let providerCatalog: ChatProviderCatalogEntry[] = []; + let availableModels: string[] = []; + let chatProviders: ChatProviderCatalogConfiguredEntry[] = []; + let providerError = ''; + let selectedProviderDefaultModel = ''; let showDateSelector = false; let selectedPlaceToAdd: PlaceResult | null = null; let selectedDate = ''; - $: chatProviders = providerCatalog.filter((provider) => provider.available_for_chat); - $: selectedProviderEntry = - chatProviders.find((provider) => provider.id === selectedProvider) ?? null; const dispatch = createEventDispatcher<{ close: void; @@ -68,21 +73,67 @@ await Promise.all([loadConversations(), loadProviderCatalog()]); }); - async function loadProviderCatalog() { - const res = await fetch('/api/chat/providers/'); - if (!res.ok) { + async function loadProviderCatalog(): Promise { + try { + const res = await fetch('/api/chat/providers/', { + credentials: 'include' + }); + if (!res.ok) { + providerError = 'Failed to load AI providers'; + return; + } + + const data = await res.json(); + const providers = Array.isArray(data) + ? (data as ChatProviderCatalogConfiguredEntry[]) + : ((data.providers || []) as ChatProviderCatalogConfiguredEntry[]); + + const usable = providers.filter( + (provider) => + provider.available_for_chat && (provider.user_configured || provider.instance_configured) + ); + chatProviders = usable; + + if (usable.length > 0) { + providerError = ''; + if (!selectedProvider || !usable.some((provider) => provider.id === selectedProvider)) { + const userConfigured = usable.find((provider) => provider.user_configured); + selectedProvider = (userConfigured || usable[0]).id; + } + } else { + selectedProvider = ''; + availableModels = []; + providerError = 'No AI providers configured. Add an API key in Settings.'; + } + } catch (e) { + console.error('Failed to load provider catalog:', e); + providerError = 'Failed to load AI providers'; + } + } + + async function loadModelsForProvider() { + if (!selectedProvider) { + availableModels = []; return; } - const catalog = (await res.json()) as ChatProviderCatalogEntry[]; - providerCatalog = catalog; - const availableProviders = catalog.filter((provider) => provider.available_for_chat); - if (!availableProviders.length) { - return; - } + try { + const res = await fetch(`/api/chat/providers/${selectedProvider}/models/`, { + credentials: 'include' + }); + const data = await res.json(); - if (!availableProviders.some((provider) => provider.id === selectedProvider)) { - selectedProvider = availableProviders[0].id; + if (data.models && data.models.length > 0) { + availableModels = data.models; + if (!selectedModel || !availableModels.includes(selectedModel)) { + selectedModel = availableModels[0]; + } + } else { + availableModels = []; + } + } catch (e) { + console.error('Failed to load models:', e); + availableModels = []; } } @@ -120,16 +171,22 @@ } } - $: if (selectedProviderEntry && initializedModelProvider !== selectedProvider) { - selectedModel = - loadModelPref(selectedProvider) || (selectedProviderEntry.default_model ?? '') || ''; + $: if (selectedProvider && initializedModelProvider !== selectedProvider) { + selectedModel = loadModelPref(selectedProvider) || selectedProviderDefaultModel || ''; initializedModelProvider = selectedProvider; } - $: if (selectedProviderEntry && initializedModelProvider === selectedProvider) { + $: if (selectedProvider && initializedModelProvider === selectedProvider) { saveModelPref(selectedProvider, selectedModel); } + $: selectedProviderDefaultModel = + chatProviders.find((provider) => provider.id === selectedProvider)?.default_model ?? ''; + + $: if (selectedProvider) { + void loadModelsForProvider(); + } + async function loadConversations() { const res = await fetch('/api/chat/conversations/'); if (res.ok) { @@ -199,7 +256,7 @@ body: JSON.stringify({ message: msgText, provider: selectedProvider, - model: selectedModel.trim() || undefined, + model: selectedModel || undefined, collection_id: collectionId, collection_name: collectionName, start_date: startDate, @@ -525,233 +582,251 @@
- - +
-
- {#if messages.length === 0 && !activeConversation} -
-
🌍
-

{$t('chat.welcome_title')}

-

{$t('chat.welcome_message')}

+ {#if chatProviders.length === 0} +
+
+ {providerError || 'No AI providers configured.'} + Add an API key in Settings
- {:else} - {#each messages as msg} -
- {#if msg.role === 'tool'} -
-
-
πŸ—ΊοΈ {msg.name}
- {#each parseToolResults(msg) as result} - {#if hasPlaceResults(result)} -
- {#each getPlaceResults(result) as place} -
-

{place.name}

- {#if place.address} -

{place.address}

- {/if} - {#if place.rating} -
- ⭐ - {place.rating} -
- {/if} - {#if collectionId} - - {/if} -
+
+ {:else} +
+ {#if messages.length === 0 && !activeConversation} +
+
🌍
+

{$t('chat.welcome_title')}

+

{$t('chat.welcome_message')}

+
+ {:else} + {#each messages as msg} +
+ {#if msg.role === 'tool'} +
+
+
πŸ—ΊοΈ {msg.name}
+ {#each parseToolResults(msg) as result} + {#if hasPlaceResults(result)} +
+ {#each getPlaceResults(result) as place} +
+

{place.name}

+ {#if place.address} +

{place.address}

+ {/if} + {#if place.rating} +
+ ⭐ + {place.rating} +
+ {/if} + {#if collectionId} + + {/if} +
+ {/each} +
+ {:else if hasWebSearchResults(result)} +
+ {#each getWebSearchResults(result) as item} + + +

+ {item.snippet} +

+
+ {/each} +
+ {:else} +
+
{JSON.stringify(result.result, null, 2)}
+
+ {/if} + {/each} +
+
+ {:else} +
+
+
{msg.content}
+ {#if msg.role === 'assistant' && msg.tool_results} +
+ {#each msg.tool_results as result} + {#if hasPlaceResults(result)} +
+ {#each getPlaceResults(result) as place} +
+

{place.name}

+ {#if place.address} +

{place.address}

+ {/if} + {#if place.rating} +
+ ⭐ + {place.rating} +
+ {/if} + {#if collectionId} + + {/if} +
+ {/each} +
+ {:else if hasWebSearchResults(result)} +
+ {#each getWebSearchResults(result) as item} + + +

+ {item.snippet} +

+
+ {/each} +
+ {:else} +
+
{JSON.stringify(result.result, null, 2)}
+
+ {/if} {/each}
- {:else if hasWebSearchResults(result)} -
- {#each getWebSearchResults(result) as item} - - -

- {item.snippet} -

-
- {/each} -
- {:else} -
-
{JSON.stringify(result.result, null, 2)}
-
{/if} - {/each} + {#if msg.role === 'assistant' && isStreaming && msg.id === messages[messages.length - 1]?.id && !msg.content} + + {/if} +
-
- {:else} -
-
-
{msg.content}
- {#if msg.role === 'assistant' && msg.tool_results} -
- {#each msg.tool_results as result} - {#if hasPlaceResults(result)} -
- {#each getPlaceResults(result) as place} -
-

{place.name}

- {#if place.address} -

{place.address}

- {/if} - {#if place.rating} -
- ⭐ - {place.rating} -
- {/if} - {#if collectionId} - - {/if} -
- {/each} -
- {:else if hasWebSearchResults(result)} -
- {#each getWebSearchResults(result) as item} - - -

- {item.snippet} -

-
- {/each} -
- {:else} -
-
{JSON.stringify(result.result, null, 2)}
-
- {/if} - {/each} -
- {/if} - {#if msg.role === 'assistant' && isStreaming && msg.id === messages[messages.length - 1]?.id && !msg.content} - - {/if} -
-
- {/if} -
- {/each} - {/if} -
+ {/if} +
+ {/each} + {/if} +
-
-
-
- {#if destination} +
+
+
+ {#if destination} + + + {/if} + {#if startDate && endDate} + + {/if} - - {/if} - {#if startDate && endDate} - - {/if} +
+
+
+
-
- - -
-
+ {/if}
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index c364098b..8435b6c4 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -573,6 +573,8 @@ export type ChatProviderCatalogEntry = { needs_api_key: boolean | null; default_model: string | null; api_base: string | null; + instance_configured: boolean; + user_configured: boolean; }; export type UserRecommendationPreferenceProfile = { diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 58e54339..2f6249de 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -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, UserRecommendationPreferenceProfile } from '$lib/types'; +import type { ImmichIntegration, User } from '$lib/types'; import { fetchCSRFToken } from '$lib/index.server'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -95,25 +95,11 @@ export const load: PageServerLoad = async (event) => { let apiKeys: UserAPIKey[] = []; let apiKeysConfigError: string | null = null; - let [apiKeysFetch, recommendationPreferencesFetch] = await Promise.all([ - fetch(`${endpoint}/api/integrations/api-keys/`, { - headers: { - Cookie: `sessionid=${sessionId}` - } - }), - fetch(`${endpoint}/api/integrations/recommendation-preferences/`, { - headers: { - Cookie: `sessionid=${sessionId}` - } - }) - ]); - - let recommendationProfile: UserRecommendationPreferenceProfile | null = null; - if (recommendationPreferencesFetch.ok) { - const recommendationProfiles = - (await recommendationPreferencesFetch.json()) as UserRecommendationPreferenceProfile[]; - recommendationProfile = recommendationProfiles[0] ?? null; - } + let apiKeysFetch = await fetch(`${endpoint}/api/integrations/api-keys/`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); if (apiKeysFetch.ok) { apiKeys = (await apiKeysFetch.json()) as UserAPIKey[]; @@ -145,7 +131,6 @@ export const load: PageServerLoad = async (event) => { stravaUserEnabled, apiKeys, apiKeysConfigError, - recommendationProfile, wandererEnabled, wandererExpired } diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 338e0302..7865b56b 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -28,16 +28,6 @@ usage_required: boolean; }; - type UserRecommendationPreferenceProfile = { - id: string; - cuisines: string | null; - interests: string[]; - trip_style: string | null; - notes: string | null; - created_at: string; - updated_at: string; - }; - let new_email: string = ''; let public_url: string = data.props.publicUrl; let immichIntegration = data.props.immichIntegration; @@ -60,13 +50,6 @@ let newApiKeyValue = ''; let isSavingApiKey = false; let deletingApiKeyId: string | null = null; - let recommendationProfile: UserRecommendationPreferenceProfile | null = null; - let cuisinesValue = ''; - let interestsValue = ''; - let tripStyleValue = ''; - let notesValue = ''; - let isSavingPreferences = false; - let savePreferencesError = ''; let mcpToken: string | null = null; let isLoadingMcpToken = false; let activeSection: string = 'profile'; @@ -144,23 +127,12 @@ { id: 'emails', icon: 'πŸ“§', label: () => $t('settings.emails') }, { id: 'integrations', icon: 'πŸ”—', label: () => $t('settings.integrations') }, { id: 'ai_api_keys', icon: 'πŸ€–', label: () => $t('settings.ai_api_keys') }, - { id: 'travel_preferences', icon: '🧭', label: () => $t('settings.travel_preferences') }, { id: 'import_export', icon: 'πŸ“¦', label: () => $t('settings.backup_restore') }, { id: 'admin', icon: 'βš™οΈ', label: () => $t('settings.admin') }, { id: 'advanced', icon: 'πŸ› οΈ', label: () => $t('settings.advanced') } ]; onMount(async () => { - recommendationProfile = - (data.props as { recommendationProfile?: UserRecommendationPreferenceProfile | null }) - .recommendationProfile ?? null; - if (recommendationProfile) { - cuisinesValue = recommendationProfile.cuisines ?? ''; - interestsValue = (recommendationProfile.interests || []).join(', '); - tripStyleValue = recommendationProfile.trip_style ?? ''; - notesValue = recommendationProfile.notes ?? ''; - } - void loadProviderCatalog(); if (browser) { @@ -583,45 +555,6 @@ } } - async function savePreferences(event: SubmitEvent) { - event.preventDefault(); - savePreferencesError = ''; - isSavingPreferences = true; - - try { - const res = await fetch('/api/integrations/recommendation-preferences/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - cuisines: cuisinesValue.trim() || null, - interests: interestsValue - .split(',') - .map((s) => s.trim()) - .filter(Boolean), - trip_style: tripStyleValue.trim() || null, - notes: notesValue.trim() || null - }) - }); - - if (!res.ok) { - savePreferencesError = $t('settings.preferences_save_error'); - addToast('error', $t('settings.preferences_save_error')); - return; - } - - recommendationProfile = (await res.json()) as UserRecommendationPreferenceProfile; - interestsValue = (recommendationProfile.interests || []).join(', '); - addToast('success', $t('settings.preferences_saved')); - } catch { - savePreferencesError = $t('settings.preferences_save_error'); - addToast('error', $t('settings.preferences_save_error')); - } finally { - isSavingPreferences = false; - } - } - function getMaskedMcpToken(token: string): string { if (token.length <= 8) { return 'β€’β€’β€’β€’β€’β€’β€’β€’'; @@ -1755,87 +1688,6 @@
{/if} - - {#if activeSection === 'travel_preferences'} -
-
-
- 🧭 -
-
-

{$t('settings.travel_preferences')}

-

- {$t('settings.travel_preferences_desc')} -

-
-
- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- - {#if savePreferencesError} -
- {savePreferencesError} -
- {/if} - - -
-
- {/if} - {#if activeSection === 'import_export'}