feat: offer a demo instance link from the live deployment
This commit is contained in:
parent
cdace7d170
commit
b60a492417
10 changed files with 63 additions and 8 deletions
|
|
@ -18,6 +18,9 @@ class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||||
|
|
||||||
app_mode: AppMode = AppMode.DEMO
|
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
|
# Spotify application and OAuth. The redirect URI must exactly match a
|
||||||
# URI registered in the Spotify developer dashboard, port included.
|
# URI registered in the Spotify developer dashboard, port included.
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,10 @@ def create_app(
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
def health() -> dict[str, str]:
|
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(router)
|
||||||
app.include_router(recommendations_router)
|
app.include_router(recommendations_router)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const props = defineProps<{
|
||||||
auth: AuthState
|
auth: AuthState
|
||||||
mode: AppMode | null
|
mode: AppMode | null
|
||||||
healthFailed: boolean
|
healthFailed: boolean
|
||||||
|
demoUrl: string | null
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{ logout: []; reset: []; toggleDev: [opener: HTMLElement] }>()
|
const emit = defineEmits<{ logout: []; reset: []; toggleDev: [opener: HTMLElement] }>()
|
||||||
|
|
||||||
|
|
@ -47,6 +48,9 @@ const modeLabel = computed(() => {
|
||||||
<span class="mode" :class="{ failed: healthFailed }">
|
<span class="mode" :class="{ failed: healthFailed }">
|
||||||
{{ modeLabel }}
|
{{ modeLabel }}
|
||||||
</span>
|
</span>
|
||||||
|
<a v-if="demoUrl" class="demo-link" :href="demoUrl">
|
||||||
|
{{ messages.appHeaderDemoInstance }}
|
||||||
|
</a>
|
||||||
<AuthStatus :auth="auth" @logout="$emit('logout')" />
|
<AuthStatus :auth="auth" @logout="$emit('logout')" />
|
||||||
<button class="button" type="button" data-dev-panel-opener @click="openDevPanel">
|
<button class="button" type="button" data-dev-panel-opener @click="openDevPanel">
|
||||||
<span class="full-label">
|
<span class="full-label">
|
||||||
|
|
@ -141,6 +145,18 @@ const modeLabel = computed(() => {
|
||||||
color: var(--c-error);
|
color: var(--c-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo-link {
|
||||||
|
color: var(--c-text-chip);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--c-line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-link:hover {
|
||||||
|
color: var(--c-text);
|
||||||
|
border-color: var(--c-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
min-height: 31px;
|
min-height: 31px;
|
||||||
padding: 6px 11px;
|
padding: 6px 11px;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import StreamError from './StreamError.vue'
|
||||||
import StreamWarning from './StreamWarning.vue'
|
import StreamWarning from './StreamWarning.vue'
|
||||||
import ThinkingIndicator from './ThinkingIndicator.vue'
|
import ThinkingIndicator from './ThinkingIndicator.vue'
|
||||||
|
|
||||||
const props = defineProps<{ turn: AssistantTurn; canSave: boolean }>()
|
const props = defineProps<{ turn: AssistantTurn; canSave: boolean; demoUrl: string | null }>()
|
||||||
defineEmits<{ save: []; retry: [query: string] }>()
|
defineEmits<{ save: []; retry: [query: string] }>()
|
||||||
|
|
||||||
const thinkingLabel = computed(() => {
|
const thinkingLabel = computed(() => {
|
||||||
|
|
@ -54,12 +54,14 @@ const isEmptyResult = computed(
|
||||||
v-if="turn.error"
|
v-if="turn.error"
|
||||||
:code="turn.error.code"
|
:code="turn.error.code"
|
||||||
:message="turn.error.message"
|
:message="turn.error.message"
|
||||||
|
:demo-url="demoUrl"
|
||||||
@retry="$emit('retry', turn.query)"
|
@retry="$emit('retry', turn.query)"
|
||||||
/>
|
/>
|
||||||
<StreamError
|
<StreamError
|
||||||
v-else-if="turn.transportFailure"
|
v-else-if="turn.transportFailure"
|
||||||
:code="turn.transportFailure.kind"
|
:code="turn.transportFailure.kind"
|
||||||
:message="turn.transportFailure.message"
|
:message="turn.transportFailure.message"
|
||||||
|
:demo-url="demoUrl"
|
||||||
@retry="$emit('retry', turn.query)"
|
@retry="$emit('retry', turn.query)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ onUnmounted(() => window.removeEventListener('keydown', onShortcut))
|
||||||
:auth="auth"
|
:auth="auth"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:health-failed="health.status === 'failed'"
|
:health-failed="health.status === 'failed'"
|
||||||
|
:demo-url="health.demoUrl"
|
||||||
@logout="logout"
|
@logout="logout"
|
||||||
@reset="reset"
|
@reset="reset"
|
||||||
@toggle-dev="openDevPanel"
|
@toggle-dev="openDevPanel"
|
||||||
|
|
@ -122,6 +123,7 @@ onUnmounted(() => window.removeEventListener('keydown', onShortcut))
|
||||||
:turns="turns"
|
:turns="turns"
|
||||||
:suggestions="DISCOVERY_SUGGESTIONS"
|
:suggestions="DISCOVERY_SUGGESTIONS"
|
||||||
:can-save="canSave"
|
:can-save="canSave"
|
||||||
|
:demo-url="health.demoUrl"
|
||||||
@pick="pickSuggestion"
|
@pick="pickSuggestion"
|
||||||
@save="savePlaylist"
|
@save="savePlaylist"
|
||||||
@retry="retryQuery"
|
@retry="retryQuery"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const props = defineProps<{
|
||||||
turns: ChatTurn[]
|
turns: ChatTurn[]
|
||||||
suggestions: readonly string[]
|
suggestions: readonly string[]
|
||||||
canSave: boolean
|
canSave: boolean
|
||||||
|
demoUrl: string | null
|
||||||
}>()
|
}>()
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
pick: [suggestion: string]
|
pick: [suggestion: string]
|
||||||
|
|
@ -107,6 +108,7 @@ watch(streaming, (value) => {
|
||||||
v-else
|
v-else
|
||||||
:turn="turn"
|
:turn="turn"
|
||||||
:can-save="canSave"
|
:can-save="canSave"
|
||||||
|
:demo-url="demoUrl"
|
||||||
@save="$emit('save', turn.id)"
|
@save="$emit('save', turn.id)"
|
||||||
@retry="$emit('retry', $event)"
|
@retry="$emit('retry', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { messages } from '../lib/messages'
|
import { messages } from '../lib/messages'
|
||||||
|
|
||||||
defineProps<{ code: string; message: string }>()
|
defineProps<{ code: string; message: string; demoUrl?: string | null }>()
|
||||||
defineEmits<{ retry: [] }>()
|
defineEmits<{ retry: [] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -9,6 +9,9 @@ defineEmits<{ retry: [] }>()
|
||||||
<div class="error" role="alert" :data-error-code="code">
|
<div class="error" role="alert" :data-error-code="code">
|
||||||
<p>{{ message }}</p>
|
<p>{{ message }}</p>
|
||||||
<button type="button" @click="$emit('retry')">{{ messages.streamErrorRetry }}</button>
|
<button type="button" @click="$emit('retry')">{{ messages.streamErrorRetry }}</button>
|
||||||
|
<a v-if="demoUrl" class="demo-link" :href="demoUrl">
|
||||||
|
{{ messages.streamErrorDemoInstance }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -36,4 +39,16 @@ defineEmits<{ retry: [] }>()
|
||||||
border-radius: var(--r-md);
|
border-radius: var(--r-md);
|
||||||
cursor: pointer;
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,12 @@ function loginFailed(): boolean {
|
||||||
/** Manage authentication, service mode, and playlist API operations. */
|
/** Manage authentication, service mode, and playlist API operations. */
|
||||||
export function useApi() {
|
export function useApi() {
|
||||||
const auth = ref<AuthState>({ status: 'checking', user: null, message: null })
|
const auth = ref<AuthState>({ status: 'checking', user: null, message: null })
|
||||||
const health = ref<HealthState>({ status: 'checking', mode: null, message: null })
|
const health = ref<HealthState>({
|
||||||
|
status: 'checking',
|
||||||
|
mode: null,
|
||||||
|
message: null,
|
||||||
|
demoUrl: null,
|
||||||
|
})
|
||||||
|
|
||||||
async function loadAuth(hadLoginError: boolean): Promise<void> {
|
async function loadAuth(hadLoginError: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -81,12 +86,17 @@ export function useApi() {
|
||||||
) {
|
) {
|
||||||
throw new Error(messages.useApiInvalidHealth)
|
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 {
|
} catch {
|
||||||
health.value = {
|
health.value = {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
mode: null,
|
mode: null,
|
||||||
message: messages.useApiModeUnavailable,
|
message: messages.useApiModeUnavailable,
|
||||||
|
demoUrl: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
export const messages = {
|
export const messages = {
|
||||||
appHeaderBrandPrefix: 'discovery',
|
appHeaderBrandPrefix: 'discovery',
|
||||||
appHeaderBrandResetLabel: 'discovery-by-llm: start a new chat',
|
appHeaderBrandResetLabel: 'discovery-by-llm: start a new chat',
|
||||||
|
appHeaderDemoInstance: 'demo',
|
||||||
appHeaderBrandAccent: '-by-',
|
appHeaderBrandAccent: '-by-',
|
||||||
appHeaderBrandSuffix: 'llm',
|
appHeaderBrandSuffix: 'llm',
|
||||||
appHeaderKicker: 'proof of concept',
|
appHeaderKicker: 'proof of concept',
|
||||||
|
|
@ -94,6 +95,7 @@ export const messages = {
|
||||||
resultSetRecommendedTrackLabel: '{count} recommended track',
|
resultSetRecommendedTrackLabel: '{count} recommended track',
|
||||||
resultSetRecommendedTracksLabel: '{count} recommended tracks',
|
resultSetRecommendedTracksLabel: '{count} recommended tracks',
|
||||||
|
|
||||||
|
streamErrorDemoInstance: 'Open the demo instance',
|
||||||
streamErrorRetry: 'Edit and retry',
|
streamErrorRetry: 'Edit and retry',
|
||||||
|
|
||||||
suggestionChipsRegionLabel: 'Listening suggestions',
|
suggestionChipsRegionLabel: 'Listening suggestions',
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,9 @@ export type AuthState =
|
||||||
| { status: 'failed'; user: null; message: string }
|
| { status: 'failed'; user: null; message: string }
|
||||||
|
|
||||||
export type HealthState =
|
export type HealthState =
|
||||||
| { status: 'checking'; mode: null; message: null }
|
| { status: 'checking'; mode: null; message: null; demoUrl: null }
|
||||||
| { status: 'ready'; mode: AppMode; message: null }
|
| { status: 'ready'; mode: AppMode; message: null; demoUrl: string | null }
|
||||||
| { status: 'failed'; mode: null; message: string }
|
| { status: 'failed'; mode: null; message: string; demoUrl: null }
|
||||||
|
|
||||||
export interface EventLogEntry {
|
export interface EventLogEntry {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue