-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Background
The Espresso Aficionados Coffee Compass is a widely-used diagnostic tool that maps taste attributes to extraction adjustments. It helps baristas systematically dial in espresso by identifying what's wrong with a shot (e.g., sour, bitter, astringent) and recommending specific changes (grind finer, lower temperature, adjust dose).
Currently, MeticAI's shot analysis (POST /api/shots/analyze-llm) relies solely on extraction curve data — it has no taste input. Adding taste feedback gives the LLM critical context that curve data alone cannot provide (e.g., a perfectly-extracted shot can still taste sour if the coffee is underdeveloped).
Coffee Compass Dimensions
| Axis | Low End | High End | Extraction Meaning |
|---|---|---|---|
| Sour ↔ Bitter | Sour/acidic | Bitter/burnt | Under-extracted ↔ Over-extracted |
| Thin ↔ Harsh | Watery/weak body | Dry/astringent | Low strength ↔ High strength + channeling |
| Sweet Spot | — | Center of compass | Balanced extraction |
Additional taste descriptors: salty (severe under-extraction), sweet (good), clean, muddy, fruity, nutty, chocolatey, floral
Compass → Adjustments mapping:
- Sour + thin → grind much finer, increase dose
- Sour only → grind slightly finer, increase temperature
- Bitter + harsh → grind coarser, decrease temperature, check channeling
- Bitter only → grind slightly coarser, reduce contact time
- Balanced but weak → increase dose or ratio
Current State
Analysis Pipeline
POST /api/shots/analyze-llminshots.pyacceptsprofile_name,shot_date,shot_filenameviaForm(...)parameters- Sends to Gemini:
PROFILING_KNOWLEDGE(~10K chars) + profile structure + local analysis JSON - Returns 5-section structured markdown parsed by
parseStructuredAnalysis()on frontend - No taste data is sent — the LLM has zero information about how the shot tasted
- Cache key:
(profile_name, shot_date, shot_filename)— would need extending with taste hash
Frontend Analysis
ExpertAnalysisView.tsxrenders analysis sections as styled cardsShotHistoryView.tsxhosts the analysis tabs with an "Analyze" buttonFormView.tsxhas a tag-based input pattern (preset clickable tags for profile generation) that can be reused for taste input UX consistency
Existing Knowledge
PROFILING_KNOWLEDGEingemini_service.pyalready has a troubleshooting section with sour→finer, bitter→lower temp mappings- This knowledge exists but is never activated meaningfully — there's no taste signal to trigger it
Implementation Plan
Taste Input UI
-
New
TasteCompassInput.tsxcomponent:- Interactive 2D compass: sour↔bitter (x-axis), thin↔harsh (y-axis)
- User taps/drags a point on the compass to indicate where the shot falls
- Center = balanced/good, edges = increasingly problematic
- Visual zones with labels and color gradients (green center → yellow → red edges)
- Returns coordinates as
{ sour_bitter: -1..1, thin_harsh: -1..1 }
-
Strength slider: separate 1–5 slider for perceived strength (weak → intense)
-
Quick taste tags: predefined clickable tags matching
FormView.tsxpattern:- Positive:
sweet,clean,fruity,chocolatey,nutty,floral,balanced - Negative:
sour,bitter,salty,astringent,muddy,ashy,flat - Color-coded: green (positive) / red (negative) / neutral
- Multiple selection allowed
- Positive:
-
Optional free-text note — short textarea for additional observations
-
Placement: Below the shot chart in
ShotHistoryView, above the "Analyze" button. Taste input is optional — analysis works without it but is enhanced when provided.
Backend Changes
-
Extend
POST /api/shots/analyze-llmwith optionalForm(...)parameters:taste_sour_bitter: Optional[float] = Form(None) # -1 (sour) to 1 (bitter) taste_thin_harsh: Optional[float] = Form(None) # -1 (thin) to 1 (harsh) taste_strength: Optional[int] = Form(None) # 1-5 taste_tags: Optional[str] = Form(None) # comma-separated taste_notes: Optional[str] = Form(None) # free text
-
Extend cache key to include taste data hash:
(profile_name, shot_date, shot_filename, taste_hash)— same shot analyzed with different taste input returns different results -
Build
taste_contextstring for the prompt:TASTE FEEDBACK: Compass position: slightly sour, slightly thin (coordinates: -0.3, -0.2) Perceived strength: 3/5 (moderate) Taste notes: fruity, slightly sour, clean Additional: "Pleasant acidity but could be sweeter"
AI Prompt Integration
-
Add Espresso Compass knowledge section to the analysis prompt:
ESPRESSO COMPASS GUIDE: The user has provided taste feedback using the Espresso Aficionados Coffee Compass. Compass coordinates: sour_bitter={x} (-1=very sour, 0=balanced, 1=very bitter), thin_harsh={y} (-1=very thin/watery, 0=balanced, 1=very harsh/astringent) Interpretation guide: - Sour + thin (bottom-left): severely under-extracted → grind much finer, increase dose - Sour only (left): under-extracted → grind finer, increase temperature 1-2°C - Bitter + harsh (top-right): over-extracted + channeling → grind coarser, lower temp, check puck prep - Bitter only (right): over-extracted → grind coarser, decrease contact time - Thin only (bottom): low strength → increase dose or decrease yield - Harsh only (top): channeling or astringency → improve puck prep, check pressure profile Correlate the taste feedback with the extraction curve data to provide more accurate recommendations. When taste and curve data disagree, prioritize taste (the user's experience is ground truth). -
When taste data is provided, add a 6th analysis section: "Taste-Based Recommendations" that directly maps compass coordinates to specific adjustments, formatted for the profile's actual parameters.
UX Design Notes
- Compass input should be visually engaging — it's a 2D touchable area, not a form
- Show a small crosshair or dot where the user taps, with a label ("Slightly sour, good body")
- Tags use the same pill/badge design as
FormView.tsxfor consistency - On mobile: compass should be at least 200×200px for accurate tapping
- Taste input section collapsed by default with "Add Taste Feedback" expand button
- After analysis, taste data persists so re-analysis includes it
Dependencies
- None — this is additive to the existing analysis pipeline
- Consumed by Add Dial-In Guide for Beginners to Home Page #260 (Dial-In guide) for the taste feedback step
Acceptance Criteria
-
TasteCompassInputcomponent renders interactive 2D compass with draggable position indicator - Strength slider (1–5) works independently of compass
- Quick taste tags are selectable (multiple) with visual feedback
- Taste data sent as optional Form parameters to
POST /api/shots/analyze-llm - Analysis prompt includes Compass knowledge when taste data is provided
- Analysis returns a "Taste-Based Recommendations" section when taste data is present
- Analysis without taste data works exactly as before (backward compatible)
- Cache key includes taste data hash (different taste input → different analysis)
- Taste input section collapsible with "Add Taste Feedback" toggle
- All labels use i18n keys (6 locales)
- Unit tests for taste context building and prompt extension
- Frontend tests for compass interaction, tag selection, and form submission
- Mobile-friendly compass input (minimum 200×200px touch target)
Key Files
| File | Change |
|---|---|
apps/web/src/components/TasteCompassInput.tsx |
New interactive compass component |
apps/web/src/components/ShotHistoryView.tsx |
Integrate taste input above Analyze button |
apps/server/api/routes/shots.py |
Extended analyze-llm with taste parameters |
apps/server/prompt_builder.py |
Compass knowledge section + taste context builder |
apps/server/services/gemini_service.py |
Extended cache key with taste hash |
apps/web/public/locales/*/translation.json |
New i18n keys |