chore: restore wrapped chips, trim unused scopes, and complete env docs
This commit is contained in:
parent
401a0ddea7
commit
5080e9a609
6 changed files with 74 additions and 57 deletions
18
.env.example
18
.env.example
|
|
@ -1,5 +1,17 @@
|
||||||
# Copy to .env and adjust. Without a .env the app starts in demo mode.
|
# Runtime mode; set to demo for no-key fixtures or live for Spotify and Anthropic.
|
||||||
APP_MODE=demo
|
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_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=
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,9 @@ api -> pipeline -> ports <- adapters domain imports nothing app-level
|
||||||
- Spotify JSON never escapes `adapters/spotify/mapping.py`.
|
- Spotify JSON never escapes `adapters/spotify/mapping.py`.
|
||||||
- Pipeline tunables (counts, thresholds, budgets, model id, effort, TTLs) live
|
- Pipeline tunables (counts, thresholds, budgets, model id, effort, TTLs) live
|
||||||
in `app/config.py` (pydantic-settings). No magic numbers in code.
|
in `app/config.py` (pydantic-settings). No magic numbers in code.
|
||||||
- LLM prompts are versioned template files under `app/prompts/`, not inline
|
- LLM prompts live as constants in `app/prompts.py`, not inline in adapters.
|
||||||
strings.
|
- `eval/scenarios.yaml` defines golden eval queries and fixture-recording keys.
|
||||||
- `eval/scenarios.yaml` is one source of truth for three consumers: golden
|
UI suggestion chips live in `frontend/src/lib/suggestions.ts`.
|
||||||
eval queries, demo fixture keys, and UI suggestion chips.
|
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,7 @@ from app.adapters.spotify.errors import SpotifyAuthenticationError
|
||||||
|
|
||||||
AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
|
AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
|
||||||
TOKEN_URL = "https://accounts.spotify.com/api/token"
|
TOKEN_URL = "https://accounts.spotify.com/api/token"
|
||||||
SCOPES = (
|
SCOPES = "user-top-read user-library-read playlist-modify-public playlist-modify-private"
|
||||||
"user-top-read user-library-read user-read-recently-played "
|
|
||||||
"playlist-modify-public playlist-modify-private "
|
|
||||||
"user-read-playback-state user-modify-playback-state"
|
|
||||||
)
|
|
||||||
TOKEN_EXPIRY_SKEW_SECONDS = 60.0
|
TOKEN_EXPIRY_SKEW_SECONDS = 60.0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -396,3 +396,45 @@ Wat ik heb laten vallen of uitgesteld:
|
||||||
|
|
||||||
- Byte-snapshots per pipeline-stap; de checks hierboven en de cassettes
|
- Byte-snapshots per pipeline-stap; de checks hierboven en de cassettes
|
||||||
zijn nu het bewijs.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -6,59 +6,27 @@ defineEmits<{ pick: [suggestion: string] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="frame">
|
<div class="chips" role="region" :aria-label="messages.suggestionChipsRegionLabel">
|
||||||
<div class="chips" role="region" :aria-label="messages.suggestionChipsRegionLabel" tabindex="0">
|
<button
|
||||||
<button
|
v-for="suggestion in suggestions"
|
||||||
v-for="suggestion in suggestions"
|
:key="suggestion"
|
||||||
:key="suggestion"
|
class="chip"
|
||||||
class="chip"
|
type="button"
|
||||||
type="button"
|
@click="$emit('pick', suggestion)"
|
||||||
@click="$emit('pick', suggestion)"
|
>
|
||||||
>
|
{{ suggestion }}
|
||||||
{{ suggestion }}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--s-3);
|
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 {
|
.chip {
|
||||||
flex: none;
|
|
||||||
padding: var(--s-3) 15px;
|
padding: var(--s-3) 15px;
|
||||||
color: var(--c-text-chip);
|
color: var(--c-text-chip);
|
||||||
font-size: var(--t-small);
|
font-size: var(--t-small);
|
||||||
|
|
|
||||||
|
|
@ -150,14 +150,14 @@ describe('result presentation', () => {
|
||||||
expect(scrollIntoView).toHaveBeenCalledWith({ block: 'start' })
|
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 onPick = vi.fn()
|
||||||
const suggestions = ['Focus', 'Energy', 'Surprise']
|
const suggestions = ['Focus', 'Energy', 'Surprise']
|
||||||
const root = mountComponent(SuggestionChips, { suggestions, onPick })
|
const root = mountComponent(SuggestionChips, { suggestions, onPick })
|
||||||
const region = root.querySelector<HTMLElement>('[aria-label="Listening suggestions"]')
|
const region = root.querySelector<HTMLElement>('[aria-label="Listening suggestions"]')
|
||||||
const buttons = root.querySelectorAll<HTMLButtonElement>('button')
|
const buttons = root.querySelectorAll<HTMLButtonElement>('button')
|
||||||
|
|
||||||
expect(region?.tabIndex).toBe(0)
|
expect(region).not.toBeNull()
|
||||||
expect(buttons).toHaveLength(suggestions.length)
|
expect(buttons).toHaveLength(suggestions.length)
|
||||||
expect([...buttons].map((button) => button.textContent?.trim())).toEqual(suggestions)
|
expect([...buttons].map((button) => button.textContent?.trim())).toEqual(suggestions)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue