chore: restore wrapped chips, trim unused scopes, and complete env docs

This commit is contained in:
Justin Visser 2026-08-11 09:31:39 +02:00
parent 401a0ddea7
commit 5080e9a609
6 changed files with 74 additions and 57 deletions

View file

@ -1,5 +1,17 @@
# Copy to .env and adjust. Without a .env the app starts in demo mode.
APP_MODE=demo
# Runtime mode; set to demo for no-key fixtures or live for Spotify and Anthropic.
APP_MODE=
# The redirect URI must exactly match the URI registered in the Spotify dashboard.
# Spotify app client ID; required in live mode and unused in demo mode.
SPOTIFY_CLIENT_ID=
# Spotify OAuth callback URI; set in live mode if the local default is not registered.
SPOTIFY_REDIRECT_URI=
# Anthropic API key; required in live mode and unused in demo mode.
ANTHROPIC_API_KEY=
# Pre-authorized Spotify token; only for hosted instances without interactive login.
SPOTIFY_SEED_REFRESH_TOKEN=
# Secure session cookie flag; set to true on every HTTPS deployment.
SESSION_COOKIE_SECURE=

View file

@ -26,10 +26,9 @@ api -> pipeline -> ports <- adapters domain imports nothing app-level
- Spotify JSON never escapes `adapters/spotify/mapping.py`.
- Pipeline tunables (counts, thresholds, budgets, model id, effort, TTLs) live
in `app/config.py` (pydantic-settings). No magic numbers in code.
- LLM prompts are versioned template files under `app/prompts/`, not inline
strings.
- `eval/scenarios.yaml` is one source of truth for three consumers: golden
eval queries, demo fixture keys, and UI suggestion chips.
- LLM prompts live as constants in `app/prompts.py`, not inline in adapters.
- `eval/scenarios.yaml` defines golden eval queries and fixture-recording keys.
UI suggestion chips live in `frontend/src/lib/suggestions.ts`.
## Naming

View file

@ -13,11 +13,7 @@ from app.adapters.spotify.errors import SpotifyAuthenticationError
AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
TOKEN_URL = "https://accounts.spotify.com/api/token"
SCOPES = (
"user-top-read user-library-read user-read-recently-played "
"playlist-modify-public playlist-modify-private "
"user-read-playback-state user-modify-playback-state"
)
SCOPES = "user-top-read user-library-read playlist-modify-public playlist-modify-private"
TOKEN_EXPIRY_SKEW_SECONDS = 60.0

View file

@ -396,3 +396,45 @@ Wat ik heb laten vallen of uitgesteld:
- Byte-snapshots per pipeline-stap; de checks hierboven en de cassettes
zijn nu het bewijs.
### Demo mode
Wat ik deed:
- Demo mode af: zonder keys speelt de app de opgenomen cassettes af door
dezelfde pipeline en hetzelfde streamingpad als live. Onbekende vragen
krijgen het dichtstbijzijnde scenario, met een banner die dat eerlijk
benoemt; playlist-acties zijn gesimuleerd en zo gelabeld.
- Een eerste versie deelde replay-state tussen gelijktijdige requests;
dat kon elkaars refinement verstoren. Nu krijgt elke request zijn
eigen replay-pool, met een test die de botsing naspeelt.
- Het docker image bestaat nu in twee smaken: live zonder fixtures, demo
met. docker compose bouwt standaard de demo-smaak, dus een verse clone
zonder keys werkt direct. Een .dockerignore bracht de build context
van ~350 MB terug naar ~7 MB.
Waarom:
- De reviewer moet de app kunnen starten met alleen docker compose,
zonder accounts of keys; demo mode is ook het bewijs dat de
ports-and-adapters opzet echt is (dezelfde poorten, andere adapter).
Wat ik heb laten vallen of uitgesteld:
- Meer demo-scenario's; de 8 opgenomen zijn de suggestion chips en dat
is genoeg voor het verhaal.
### Opruimen
Wat ik deed:
- De suggestion chips terug naar de wrapped look; de scrollbare box uit de
vorige batch stond lelijker dan wat er eerst was.
- Ongebruikte OAuth scopes weggehaald (player en recently-played; nergens
in de code gebruikt), .env.example compleet gemaakt en AGENTS.md
gelijkgetrokken met hoe de repo er echt uitziet.
Waarom:
- Minder scopes vragen dan je gebruikt is netter richting de reviewer en
richting Spotify.

View file

@ -6,8 +6,7 @@ defineEmits<{ pick: [suggestion: string] }>()
</script>
<template>
<div class="frame">
<div class="chips" role="region" :aria-label="messages.suggestionChipsRegionLabel" tabindex="0">
<div class="chips" role="region" :aria-label="messages.suggestionChipsRegionLabel">
<button
v-for="suggestion in suggestions"
:key="suggestion"
@ -18,47 +17,16 @@ defineEmits<{ pick: [suggestion: string] }>()
{{ suggestion }}
</button>
</div>
</div>
</template>
<style scoped>
.frame {
position: relative;
min-width: 0;
overflow: hidden;
background: var(--c-surface-raised);
border: 1px solid var(--c-line);
border-radius: var(--r-md);
}
.frame::after {
position: absolute;
top: 1px;
right: 1px;
bottom: var(--s-3);
width: var(--s-10);
pointer-events: none;
content: '';
background: linear-gradient(to right, transparent, var(--c-surface-raised));
}
.chips {
display: flex;
flex-wrap: nowrap;
flex-wrap: wrap;
gap: var(--s-3);
padding: var(--s-3) var(--s-10) var(--s-3) var(--s-3);
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-color: var(--c-line-strong) transparent;
scrollbar-width: thin;
}
.chips:focus-visible {
outline-offset: -3px;
}
.chip {
flex: none;
padding: var(--s-3) 15px;
color: var(--c-text-chip);
font-size: var(--t-small);

View file

@ -150,14 +150,14 @@ describe('result presentation', () => {
expect(scrollIntoView).toHaveBeenCalledWith({ block: 'start' })
})
it('keeps suggestions in a labeled focusable row', () => {
it('keeps suggestions in a labeled region', () => {
const onPick = vi.fn()
const suggestions = ['Focus', 'Energy', 'Surprise']
const root = mountComponent(SuggestionChips, { suggestions, onPick })
const region = root.querySelector<HTMLElement>('[aria-label="Listening suggestions"]')
const buttons = root.querySelectorAll<HTMLButtonElement>('button')
expect(region?.tabIndex).toBe(0)
expect(region).not.toBeNull()
expect(buttons).toHaveLength(suggestions.length)
expect([...buttons].map((button) => button.textContent?.trim())).toEqual(suggestions)