From b60a4924171ebadf561935bd7bd431f0accee3ae Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Tue, 11 Aug 2026 10:21:18 +0200 Subject: [PATCH] feat: offer a demo instance link from the live deployment --- backend/app/config.py | 3 +++ backend/app/main.py | 5 ++++- frontend/src/components/AppHeader.vue | 16 ++++++++++++++++ frontend/src/components/AssistantMessage.vue | 4 +++- frontend/src/components/ChatView.vue | 2 ++ frontend/src/components/MessageList.vue | 2 ++ frontend/src/components/StreamError.vue | 17 ++++++++++++++++- frontend/src/composables/useApi.ts | 14 ++++++++++++-- frontend/src/lib/messages.ts | 2 ++ frontend/src/lib/models.ts | 6 +++--- 10 files changed, 63 insertions(+), 8 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index fcd8d3e..df42064 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -18,6 +18,9 @@ class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") app_mode: AppMode = AppMode.DEMO + # Optional public URL of a fixture-replay twin of this deployment; the UI + # offers it as an escape hatch when the live Spotify quota runs out. + demo_instance_url: str = "" # Spotify application and OAuth. The redirect URI must exactly match a # URI registered in the Spotify developer dashboard, port included. diff --git a/backend/app/main.py b/backend/app/main.py index 4bd51cf..82d9982 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -124,7 +124,10 @@ def create_app( @app.get("/api/health") def health() -> dict[str, str]: - return {"status": "ok", "mode": active_settings.app_mode} + payload = {"status": "ok", "mode": active_settings.app_mode} + if active_settings.demo_instance_url: + payload["demo_url"] = active_settings.demo_instance_url + return payload app.include_router(router) app.include_router(recommendations_router) diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 786f1ff..09dfd7e 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -8,6 +8,7 @@ const props = defineProps<{ auth: AuthState mode: AppMode | null healthFailed: boolean + demoUrl: string | null }>() const emit = defineEmits<{ logout: []; reset: []; toggleDev: [opener: HTMLElement] }>() @@ -47,6 +48,9 @@ const modeLabel = computed(() => { {{ modeLabel }} + + {{ messages.appHeaderDemoInstance }} + + + {{ messages.streamErrorDemoInstance }} + @@ -36,4 +39,16 @@ defineEmits<{ retry: [] }>() border-radius: var(--r-md); cursor: pointer; } + +.demo-link { + display: inline-block; + margin-top: var(--s-3); + margin-left: var(--s-3); + padding: 8px var(--s-4); + color: var(--c-text); + font-family: var(--f-mono); + font-size: var(--t-micro); + border: 1px solid var(--c-line-strong); + border-radius: var(--r-md); +} diff --git a/frontend/src/composables/useApi.ts b/frontend/src/composables/useApi.ts index e4d1133..578b1b5 100644 --- a/frontend/src/composables/useApi.ts +++ b/frontend/src/composables/useApi.ts @@ -35,7 +35,12 @@ function loginFailed(): boolean { /** Manage authentication, service mode, and playlist API operations. */ export function useApi() { const auth = ref({ status: 'checking', user: null, message: null }) - const health = ref({ status: 'checking', mode: null, message: null }) + const health = ref({ + status: 'checking', + mode: null, + message: null, + demoUrl: null, + }) async function loadAuth(hadLoginError: boolean): Promise { try { @@ -81,12 +86,17 @@ export function useApi() { ) { throw new Error(messages.useApiInvalidHealth) } - health.value = { status: 'ready', mode: body.mode, message: null } + const demoUrl = + typeof body.demo_url === 'string' && body.demo_url.startsWith('https://') + ? body.demo_url + : null + health.value = { status: 'ready', mode: body.mode, message: null, demoUrl } } catch { health.value = { status: 'failed', mode: null, message: messages.useApiModeUnavailable, + demoUrl: null, } } } diff --git a/frontend/src/lib/messages.ts b/frontend/src/lib/messages.ts index 4add1fe..c34e523 100644 --- a/frontend/src/lib/messages.ts +++ b/frontend/src/lib/messages.ts @@ -3,6 +3,7 @@ export const messages = { appHeaderBrandPrefix: 'discovery', appHeaderBrandResetLabel: 'discovery-by-llm: start a new chat', + appHeaderDemoInstance: 'demo', appHeaderBrandAccent: '-by-', appHeaderBrandSuffix: 'llm', appHeaderKicker: 'proof of concept', @@ -94,6 +95,7 @@ export const messages = { resultSetRecommendedTrackLabel: '{count} recommended track', resultSetRecommendedTracksLabel: '{count} recommended tracks', + streamErrorDemoInstance: 'Open the demo instance', streamErrorRetry: 'Edit and retry', suggestionChipsRegionLabel: 'Listening suggestions', diff --git a/frontend/src/lib/models.ts b/frontend/src/lib/models.ts index b839da4..97e38c7 100644 --- a/frontend/src/lib/models.ts +++ b/frontend/src/lib/models.ts @@ -49,9 +49,9 @@ export type AuthState = | { status: 'failed'; user: null; message: string } export type HealthState = - | { status: 'checking'; mode: null; message: null } - | { status: 'ready'; mode: AppMode; message: null } - | { status: 'failed'; mode: null; message: string } + | { status: 'checking'; mode: null; message: null; demoUrl: null } + | { status: 'ready'; mode: AppMode; message: null; demoUrl: string | null } + | { status: 'failed'; mode: null; message: string; demoUrl: null } export interface EventLogEntry { timestamp: string