diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 71619e23..f3a77521 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,6 +10,8 @@ Voyage is a self-hosted travel companion web application built with SvelteKit fr **Key architectural pattern — API Proxy**: The frontend never calls the Django backend directly. All API calls go to `src/routes/api/[...path]/+server.ts`, which proxies requests to the Django server (`http://server:8000`), injecting CSRF tokens and managing session cookies. This means frontend fetches use relative URLs like `/api/locations/`. +**AI Chat**: The AI travel chat assistant is embedded in Collections → Recommendations (component: `AITravelChat.svelte`). There is no standalone `/chat` route. Chat providers are loaded dynamically from `GET /api/chat/providers/` (backed by LiteLLM runtime list + custom entries like `opencode_zen`). Chat conversations stream via SSE through `/api/chat/conversations/`. Provider config lives in `backend/server/chat/llm_client.py` (`CHAT_PROVIDER_CONFIG`). + **Services** (docker-compose): - `web` → SvelteKit frontend at `:8015` - `server` → Django (via Gunicorn + Nginx) at `:8016` @@ -20,7 +22,7 @@ Voyage is a self-hosted travel companion web application built with SvelteKit fr ## Codebase Conventions -**Backend layout**: The Django project lives in `backend/server/`. Apps are `adventures` (core: locations, collections, itineraries, notes, transportation), `users`, `worldtravel` (countries/regions), and `integrations`. Views inside `adventures` are split into per-domain files under `adventures/views/` (e.g. `location_view.py`, `collection_view.py`). +**Backend layout**: The Django project lives in `backend/server/`. Apps are `adventures` (core: locations, collections, itineraries, notes, transportation), `users`, `worldtravel` (countries/regions), `integrations`, `achievements`, and `chat` (LLM chat agent with dynamic provider catalog). Views inside `adventures` are split into per-domain files under `adventures/views/` (e.g. `location_view.py`, `collection_view.py`). **Backend patterns**: - DRF `ModelViewSet` subclasses for all CRUD resources; custom actions with `@action` @@ -33,7 +35,7 @@ Voyage is a self-hosted travel companion web application built with SvelteKit fr - `src/lib/types.ts` — all TypeScript interfaces (`Location`, `Collection`, `User`, `Visit`, etc.) - `src/lib/index.ts` — general utility functions - `src/lib/index.server.ts` — server-only utilities (used in `+page.server.ts` and `+server.ts` files) -- `src/lib/components/` — Svelte components organized by domain (`locations/`, `collections/`, `map/`, `cards/`, `shared/`) +- `src/lib/components/` — Svelte components organized by domain (`locations/`, `collections/`, `map/`, `cards/`, `shared/`); includes `AITravelChat.svelte` for Collections chat - `src/locales/` — i18n JSON files (uses `svelte-i18n`); wrap all user-visible strings in `$t('key')` **Frontend patterns**: @@ -57,19 +59,20 @@ Run these commands in order: - Wait 30+ seconds for services to fully initialize before testing functionality ### Development Workflow Commands -**Frontend (SvelteKit with Node.js):** -- `cd frontend && npm install` - **45+ seconds, NEVER CANCEL. Set timeout to 60+ minutes** -- `cd frontend && npm run build` - **32 seconds, set timeout to 60 seconds** -- `cd frontend && npm run dev` - Start development server (requires backend running) -- `cd frontend && npm run format` - **6 seconds** - Fix code formatting (ALWAYS run before committing) -- `cd frontend && npm run lint` - **6 seconds** - Check code formatting -- `cd frontend && npm run check` - **12 seconds** - Run Svelte type checking (3 errors, 19 warnings expected) +**Frontend (SvelteKit — prefer Bun):** +- `cd frontend && bun install` - **45+ seconds, NEVER CANCEL. Set timeout to 60+ minutes** +- `cd frontend && bun run build` - **32 seconds, set timeout to 60 seconds** +- `cd frontend && bun run dev` - Start development server (requires backend running) +- `cd frontend && bun run format` - **6 seconds** - Fix code formatting (ALWAYS run before committing) +- `cd frontend && bun run lint` - **6 seconds** - Check code formatting +- `cd frontend && bun run check` - **12 seconds** - Run Svelte type checking (3 errors, 19 warnings expected) -**Backend (Django with Python):** +**Backend (Django with Python — prefer uv for local tooling):** - Backend development requires Docker - local Python pip install fails due to network timeouts - `docker compose exec server python3 manage.py test` - **7 seconds** - Run tests (2/3 tests fail, this is expected) - `docker compose exec server python3 manage.py help` - View Django commands - `docker compose exec server python3 manage.py migrate` - Run database migrations +- Use `uv` for local Python dependency/tooling commands when applicable **Full Application:** - Frontend runs on: http://localhost:8015 @@ -87,10 +90,10 @@ Run these commands in order: ### Pre-Commit Validation (ALWAYS run before committing) **ALWAYS run these commands to ensure CI will pass:** -- `cd frontend && npm run format` - **6 seconds** - Fix formatting issues -- `cd frontend && npm run lint` - **6 seconds** - Verify formatting is correct (should pass after format) -- `cd frontend && npm run check` - **12 seconds** - Type checking (some warnings expected) -- `cd frontend && npm run build` - **32 seconds** - Verify build succeeds +- `cd frontend && bun run format` - **6 seconds** - Fix formatting issues +- `cd frontend && bun run lint` - **6 seconds** - Verify formatting is correct (should pass after format) +- `cd frontend && bun run check` - **12 seconds** - Type checking (some warnings expected) +- `cd frontend && bun run build` - **32 seconds** - Verify build succeeds ## Critical Development Notes @@ -112,7 +115,7 @@ Run these commands in order: ### Build Timing (NEVER CANCEL) - **Docker first startup**: 25+ minutes (image downloads) - **Docker subsequent startups**: <1 second (images cached) -- **Frontend npm install**: 45 seconds +- **Frontend bun install**: 45 seconds - **Frontend build**: 32 seconds - **Tests and checks**: 6-12 seconds each @@ -149,7 +152,7 @@ Voyage/ - **"500: Internal Error"**: Frontend-backend communication issue (expected in dev setup) - **"Cannot connect to backend"**: Backend not started or wrong URL configuration - **"pip install timeout"**: Network issue, use Docker instead of local Python -- **"Frontend build fails"**: Run `npm install` first, check Node.js version compatibility +- **"Frontend build fails"**: Run `bun install` first, check Node.js version compatibility ## Troubleshooting Commands ```bash diff --git a/AGENTS.md b/AGENTS.md index f5b3000a..447235bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ ## Architecture Overview - **API proxy pattern**: Frontend never calls Django directly. All API calls go through `frontend/src/routes/api/[...path]/+server.ts`, which proxies to `http://server:8000`, handles cookies, and injects CSRF behavior. +- **AI chat**: Embedded in Collections → Recommendations via `AITravelChat.svelte` component. No standalone `/chat` route. Provider list is dynamic from backend `GET /api/chat/providers/` (sourced from LiteLLM runtime + custom entries like `opencode_zen`). Chat conversations use SSE streaming via `/api/chat/conversations/`. - **Service ports**: - `web` → `:8015` - `server` → `:8016` @@ -17,21 +18,23 @@ ## Codebase Layout - **Backend**: `backend/server/` - Apps: `adventures/`, `users/`, `worldtravel/`, `integrations/`, `achievements/`, `chat/` + - Chat provider config: `backend/server/chat/llm_client.py` (`CHAT_PROVIDER_CONFIG`) - **Frontend**: `frontend/src/` - Routes: `src/routes/` - - Shared types: `src/lib/types.ts` - - Components: `src/lib/components/` + - Shared types: `src/lib/types.ts` (includes `ChatProviderCatalogEntry`) + - Components: `src/lib/components/` (includes `AITravelChat.svelte`) - i18n: `src/locales/` ## Development Commands -### Frontend -- `cd frontend && npm run format` -- `cd frontend && npm run lint` -- `cd frontend && npm run check` -- `cd frontend && npm run build` +### Frontend (prefer Bun) +- `cd frontend && bun run format` +- `cd frontend && bun run lint` +- `cd frontend && bun run check` +- `cd frontend && bun run build` +- `cd frontend && bun install` -### Backend +### Backend (Docker required; prefer uv for local Python tooling) - `docker compose exec server python3 manage.py test` - `docker compose exec server python3 manage.py migrate` @@ -41,13 +44,13 @@ ## Pre-Commit Checklist Run in this order: -1. `cd frontend && npm run format` -2. `cd frontend && npm run lint` -3. `cd frontend && npm run check` -4. `cd frontend && npm run build` +1. `cd frontend && bun run format` +2. `cd frontend && bun run lint` +3. `cd frontend && bun run check` +4. `cd frontend && bun run build` ## Known Issues (Expected) -- Frontend `npm run check`: **3 type errors + 19 warnings** expected +- Frontend `bun run check`: **3 type errors + 19 warnings** expected - Backend tests: **2/3 fail** (expected) - Docker dev setup has frontend-backend communication issues (500 errors beyond homepage) @@ -56,6 +59,8 @@ Run in this order: - API calls: route through proxy at `/api/[...path]/+server.ts` - Styling: use DaisyUI semantic colors/classes (`bg-primary`, `text-base-content`, etc.) - Security: handle CSRF tokens via `/auth/csrf/` and `X-CSRFToken` +- Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG` ## Conventions - Do **not** attempt to fix known test/configuration issues as part of feature work. +- Use `bun` for frontend commands, `uv` for local Python tooling where applicable. diff --git a/CLAUDE.md b/CLAUDE.md index ed42e71b..5b5412d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ - Use the API proxy pattern: never call Django directly from frontend components. - Route all frontend API calls through `frontend/src/routes/api/[...path]/+server.ts`. - Proxy target is `http://server:8000`; preserve session cookies and CSRF behavior. +- AI chat is embedded in Collections → Recommendations via `AITravelChat.svelte`. There is no standalone `/chat` route. Chat providers are loaded dynamically from `GET /api/chat/providers/` (backed by LiteLLM runtime providers + custom entries like `opencode_zen`). Chat conversations stream via SSE through `/api/chat/conversations/`. - Service ports: - `web` → `:8015` - `server` → `:8016` @@ -21,23 +22,25 @@ ## Codebase Layout - Backend root: `backend/server/` - Apps: `adventures/`, `users/`, `worldtravel/`, `integrations/`, `achievements/`, `chat/` + - Chat provider config: `backend/server/chat/llm_client.py` (`CHAT_PROVIDER_CONFIG`) - Frontend root: `frontend/src/` - Routes: `src/routes/` - - Shared types: `src/lib/types.ts` - - Components: `src/lib/components/` + - Shared types: `src/lib/types.ts` (includes `ChatProviderCatalogEntry`) + - Components: `src/lib/components/` (includes `AITravelChat.svelte`) - Locales: `src/locales/` ## Development Workflow - Develop Docker-first. Start services with Docker before backend-dependent work. - Use these commands: -### Frontend -- `cd frontend && npm run format` -- `cd frontend && npm run lint` -- `cd frontend && npm run check` -- `cd frontend && npm run build` +### Frontend (prefer Bun) +- `cd frontend && bun run format` +- `cd frontend && bun run lint` +- `cd frontend && bun run check` +- `cd frontend && bun run build` +- `cd frontend && bun install` -### Backend +### Backend (Docker required; prefer uv for local Python tooling) - `docker compose exec server python3 manage.py test` - `docker compose exec server python3 manage.py migrate` @@ -47,15 +50,15 @@ ## Pre-Commit Checklist Run in this exact order: -1. `cd frontend && npm run format` -2. `cd frontend && npm run lint` -3. `cd frontend && npm run check` -4. `cd frontend && npm run build` +1. `cd frontend && bun run format` +2. `cd frontend && bun run lint` +3. `cd frontend && bun run check` +4. `cd frontend && bun run build` **ALWAYS run format before committing.** ## Known Issues (Expected) -- Frontend `npm run check`: **3 type errors + 19 warnings** expected +- Frontend `bun run check`: **3 type errors + 19 warnings** expected - Backend tests: **2/3 fail** (expected) - Docker dev setup has frontend-backend communication issues (500 errors beyond homepage) @@ -64,6 +67,8 @@ Run in this exact order: - API access: always use proxy route `/api/[...path]/+server.ts` - Styling: prefer DaisyUI semantic classes (`bg-primary`, `text-base-content`) - CSRF handling: use `/auth/csrf/` + `X-CSRFToken` +- Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG` ## Conventions - Do **not** attempt to fix known test/configuration issues as part of feature work. +- Use `bun` for frontend commands, `uv` for local Python tooling where applicable. diff --git a/README.md b/README.md index a1baac64..da90e2e9 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,15 @@ Voyage aims to be simple, beautiful, and open to everyone — inheriting Adventu - Collaborators can view and edit shared itineraries (collections), making planning a breeze. - **Customizable Themes** 🎨: Choose from 10 built-in themes including Light, Dark, Dim, Night, Forest, Aqua, Catppuccin Mocha, Aesthetic Light, Aesthetic Dark, and Northern Lights. Theme selection persists across sessions. +### AI Chat (Collections Recommendations) + +Voyage includes an AI-powered travel chat assistant embedded in the Collections → Recommendations view. The chat uses LLM providers configured by the user (API keys set in Settings) and supports conversational trip planning within the context of a collection. + +- **Provider catalog**: The backend dynamically lists all supported LLM providers via `GET /api/chat/providers/`, sourced from LiteLLM's runtime provider list plus custom entries. +- **Supported providers include**: OpenAI, Anthropic, Google Gemini, Ollama, Groq, Mistral, GitHub Models, OpenRouter, and OpenCode Zen. +- **OpenCode Zen**: An OpenAI-compatible provider (`opencode_zen`) routed through `https://opencode.ai/zen/v1`. +- **Configuration**: Users add API keys for their chosen provider in Settings → API Keys. No server-side environment variables required for chat providers — all keys are per-user. + ### Travel Agent (MCP) Voyage provides an authenticated Travel Agent MCP endpoint for programmatic itinerary workflows (list collections, inspect itinerary details, create items, reorder timelines). See the guide: [`documentation/docs/guides/travel_agent.md`](documentation/docs/guides/travel_agent.md). diff --git a/backend/server/chat/llm_client.py b/backend/server/chat/llm_client.py index a46d92ff..5042ba9a 100644 --- a/backend/server/chat/llm_client.py +++ b/backend/server/chat/llm_client.py @@ -75,9 +75,10 @@ def _safe_get(obj, key, default=None): def _normalize_provider_id(provider_id): value = str(provider_id or "").strip() - if value.startswith("LlmProviders."): - value = value.split(".", 1)[1] - return value.lower() + lowered = value.lower() + if lowered.startswith("llmproviders."): + return lowered.split(".", 1)[1] + return lowered def _default_provider_label(provider_id): diff --git a/docs/architecture.md b/docs/architecture.md index d16f63a9..8de7e262 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -26,7 +26,7 @@ Session-based via `django-allauth`. CSRF tokens from `/auth/csrf/`, passed as `X - `worldtravel` — countries, regions, cities, visited tracking - `integrations` — external service integrations - `achievements` — gamification -- `chat` — LLM chat/agent +- `chat` — LLM chat/agent (AI travel chat in Collections → Recommendations; dynamic provider catalog via LiteLLM; `GET /api/chat/providers/`) ## Frontend Structure - `src/routes/` — SvelteKit file-based routing diff --git a/docs/docs/guides/travel_agent.md b/docs/docs/guides/travel_agent.md index fc10bdbd..54067892 100644 --- a/docs/docs/guides/travel_agent.md +++ b/docs/docs/guides/travel_agent.md @@ -2,6 +2,8 @@ Voyage includes a **Travel Agent** interface exposed through an **MCP-compatible HTTP endpoint**. This lets external MCP clients read and manage trip itineraries programmatically for authenticated users. +> **Note**: This is the *external* programmatic interface. For the *in-app* AI chat assistant (conversational trip planning), see the AI Travel Chat section in [How to use Voyage](../usage/usage.md#ai-travel-chat). + ## Endpoint - Default path: `api/mcp` diff --git a/docs/docs/intro/voyage_overview.md b/docs/docs/intro/voyage_overview.md index 8a7e9d65..98f064fa 100644 --- a/docs/docs/intro/voyage_overview.md +++ b/docs/docs/intro/voyage_overview.md @@ -23,7 +23,8 @@ Voyage is a full-fledged travel companion. With Voyage, you can log your adventu - Locations and itineraries can be shared via a public link or directly with other Voyage users. - Collaborators can view and edit shared itineraries (collections), making planning a breeze. - **Customizable Themes** 🎨: Choose from 10 built-in themes including Light, Dark, Dim, Night, Forest, Aqua, Catppuccin Mocha, Aesthetic Light, Aesthetic Dark, and Northern Lights. Theme selection persists across sessions. -- **Travel Agent (MCP) access** 🤖: Voyage exposes an authenticated MCP endpoint so external clients can list collections, inspect itineraries, create itinerary items, and reorder trip timelines. See the [Travel Agent (MCP) guide](../guides/travel_agent.md). +- **AI Travel Chat** 🤖: An AI-powered chat assistant lives inside Collections → Recommendations, letting you brainstorm destinations and plan trips conversationally. The provider list is dynamic — backed by LiteLLM's full provider catalog — and includes OpenAI, Anthropic, Gemini, Ollama, Groq, Mistral, GitHub Models, OpenRouter, and OpenCode Zen. Configure your preferred provider's API key in Settings. +- **Travel Agent (MCP) access** 🔧: Voyage exposes an authenticated MCP endpoint so external clients can list collections, inspect itineraries, create itinerary items, and reorder trip timelines. See the [Travel Agent (MCP) guide](../guides/travel_agent.md). ## Why Voyage? diff --git a/docs/docs/usage/usage.md b/docs/docs/usage/usage.md index becc73c1..fc0dc26c 100644 --- a/docs/docs/usage/usage.md +++ b/docs/docs/usage/usage.md @@ -22,6 +22,10 @@ The term "Location" is now used instead of "Adventure" - the usage remains the s - **Trail**: a trail is a path or route that is associated with a location. Trails can be used to document hiking paths, biking routes, or any other type of journey that has a specific path. Trails are linked to locations either by link to an external service (e.g., AllTrails) or from the [Wanderer](/docs/configuration/wanderer_integration) integration. When linked via the Wanderer integration, trails can provide additional context and information about the journey such as distance and elevation gain. - **Activity**: an activity is what you actually do at a location. This can include things like hiking, biking, skiing, kayaking, or any other outdoor activity. Activities are associated with a visit and include fields such as the type of activity, time, distance, and trail taken. They can be manually entered or imported from the [Strava](/docs/configuration/strava_integration) integration. Once an activity is added, it will appear on the location map based on the data from the GPX file. +#### AI Travel Chat + +The AI travel chat is embedded in the **Collections → Recommendations** view. Select a collection, switch to the Recommendations tab, and use the chat to brainstorm destinations, ask for travel advice, or get location suggestions. The chat supports multiple LLM providers — configure your API key in **Settings → API Keys** and pick a provider from the dropdown in the chat interface. The provider list is loaded dynamically from the backend, so any provider supported by LiteLLM (plus OpenCode Zen) is available. + #### Collections - **Collection**: a collection is a way to group locations together. Collections are flexible and can be used in many ways. When no start or end date is added to a collection, it acts like a folder to group locations together. When a start and end date is added to a collection, it acts like a trip to group locations together that were visited during that time period. With start and end dates, the collection is transformed into a full itinerary with a timeline-style day view — each day displays numbered stops as compact cards (without image banners), connector rows between consecutive locations showing distance and travel time via OSRM routing (walking if ≤ 20 min, driving otherwise) with automatic haversine fallback when OSRM is unavailable, and a single `+ Add` control for inserting new places. Lodging placement follows directional rules: on check-in day it appears after the last stop, on check-out day it appears before the first stop, and on days with no locations a single lodging card is shown (or two cards when a checkout and checkin are different lodgings). Connector rows link lodging to adjacent locations. Day-level quick actions include Auto-fill (populates an empty itinerary from dated records) and Optimize (nearest-neighbor route ordering for coordinate-backed stops). The day date pill displays a weather temperature summary when available, with graceful fallback if weather data is unavailable. The itinerary also includes a map showing the route taken between locations. Your most recently updated collections also appear on the dashboard. For example, you could have a collection for a trip to Europe with dates so you can plan where you want to visit, a collection of local hiking trails, or a collection for a list of restaurants you want to try. diff --git a/frontend/src/routes/chat/+page.svelte b/frontend/src/lib/components/AITravelChat.svelte similarity index 55% rename from frontend/src/routes/chat/+page.svelte rename to frontend/src/lib/components/AITravelChat.svelte index 487bbc6c..a1b8a0d3 100644 --- a/frontend/src/routes/chat/+page.svelte +++ b/frontend/src/lib/components/AITravelChat.svelte @@ -16,6 +16,8 @@ name?: string; }; + export let embedded = false; + let conversations: Conversation[] = []; let activeConversation: Conversation | null = null; let messages: ChatMessage[] = []; @@ -208,147 +210,164 @@ } - - {$t('chat.title')} | Voyage - - -
-
-
-

{$t('chat.conversations')}

- -
-
- {#each conversations as conv} -
- +
+
+
+
+
+

{$t('chat.conversations')}

- {/each} - {#if conversations.length === 0} -

{$t('chat.no_conversations')}

- {/if} -
-
- -
-
- - -

{$t('chat.title')}

-
- + {#if conversations.length === 0} +

{$t('chat.no_conversations')}

+ {/if} +
-
-
- {#if messages.length === 0 && !activeConversation} -
+
+
+ -

{$t('chat.welcome_title')}

-

{$t('chat.welcome_message')}

-
- {:else} - {#each messages as msg} -
- {#if msg.role === 'tool'} -
-
-
🔧 {msg.name}
-
{msg.content}
-
-
- {:else} -
-
-
{msg.content}
- {#if msg.role === 'assistant' && - isStreaming && - msg.id === messages[messages.length - 1]?.id && - !msg.content} - - {/if} -
-
- {/if} +

{$t('chat.title')}

+
+
- {/each} - {/if} -
+
-
-
- - +
+ +
+
+ + +
+
diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 31838839..8c16e6b7 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -3,7 +3,6 @@ import { goto } from '$app/navigation'; export let data: any; import type { SubmitFunction } from '@sveltejs/kit'; - import { mdiRobotOutline } from '@mdi/js'; import DotsHorizontal from '~icons/mdi/dots-horizontal'; import Calendar from '~icons/mdi/calendar'; @@ -122,7 +121,6 @@ const navigationItems: NavigationItem[] = [ { path: '/locations', icon: MapMarker, label: 'locations.locations' }, { path: '/collections', icon: FormatListBulletedSquare, label: 'navbar.collections' }, - { path: '/chat', iconPath: mdiRobotOutline, label: 'navbar.chat' }, { path: '/invites', icon: AccountMultiple, label: 'invites.title' }, { path: '/worldtravel', icon: Earth, label: 'navbar.worldtravel' }, { path: '/map', icon: MapIcon, label: 'navbar.map' }, diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 4c9d7563..38425cf1 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -19,6 +19,7 @@ import CollectionAllItems from '$lib/components/collections/CollectionAllItems.svelte'; import CollectionItineraryPlanner from '$lib/components/collections/CollectionItineraryPlanner.svelte'; import CollectionRecommendationView from '$lib/components/CollectionRecommendationView.svelte'; + import AITravelChat from '$lib/components/AITravelChat.svelte'; import CollectionMap from '$lib/components/collections/CollectionMap.svelte'; import CollectionStats from '$lib/components/collections/CollectionStats.svelte'; import LocationLink from '$lib/components/LocationLink.svelte'; @@ -1259,7 +1260,10 @@ {#if currentView === 'recommendations'} - +
+ + +
{/if}
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index a5cf2793..b71d7275 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -497,7 +497,9 @@ updated[existingIndex] = payload; userApiKeys = updated.sort((a, b) => a.provider.localeCompare(b.provider)); } else { - userApiKeys = [...userApiKeys, payload].sort((a, b) => a.provider.localeCompare(b.provider)); + userApiKeys = [...userApiKeys, payload].sort((a, b) => + a.provider.localeCompare(b.provider) + ); } newApiKeyValue = ''; apiKeysConfigError = null; @@ -1276,14 +1278,14 @@

📖 {$t('immich.need_help')} - {$t('navbar.documentation')} -

-
+ {$t('navbar.documentation')} +

+
@@ -1307,14 +1309,14 @@ {#if user.is_staff}

📖 {$t('immich.need_help')} - {$t('navbar.documentation')} -

- {:else if !googleMapsEnabled} + {$t('navbar.documentation')} +

+ {:else if !googleMapsEnabled}

ℹ️ {$t('google_maps.google_maps_integration_desc_no_staff')}

@@ -1371,14 +1373,14 @@ {#if user.is_staff}

📖 {$t('immich.need_help')} - {$t('navbar.documentation')} -

- {:else if !stravaGlobalEnabled} + {$t('navbar.documentation')} +

+ {:else if !stravaGlobalEnabled}

ℹ️ {$t('google_maps.google_maps_integration_desc_no_staff')}

@@ -1486,14 +1488,14 @@

📖 {$t('immich.need_help')} - {$t('navbar.documentation')} -

-
+ {$t('navbar.documentation')} +

+
{/if}
@@ -1560,10 +1562,9 @@ > {$t('settings.travel_agent_help_setup_guide')}{$t('settings.travel_agent_help_setup_guide')}

@@ -1572,8 +1573,8 @@

MCP Access Token

- Create or fetch your personal token for MCP clients. The same token is reused if one - already exists. + Create or fetch your personal token for MCP clients. The same token is reused if + one already exists.

@@ -1587,11 +1588,7 @@ {/if} {mcpToken ? 'Refresh token' : 'Get MCP token'} -
@@ -1616,7 +1613,9 @@ {:else}
{#each userApiKeys as apiKey} -
+
{getApiKeyProviderLabel(apiKey.provider)}
@@ -1643,20 +1642,20 @@

{$t('settings.add_api_key')}

- - -
+ + +
+ {$t('settings.documentation_link')} +
+
@@ -2059,21 +2058,21 @@ Sean Morley. {$t('settings.all_rights_reserved')}