diff --git a/CHANGELOG.md b/CHANGELOG.md
index 103f6b6..ed6120c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Round 17 (simplify the core loop) — capture → suggest → ask on chat
+
+Product direction: *"the app should be not complex — just capture photos and then suggest users; if any question, just ask on chat further."* This round removes the upfront friction on the scan flow and adds the chat hand-off that was missing.
+
+| Change | Detail |
+|---|---|
+| **Lead with the photo, not a mode choice** | The scan page no longer opens with the 3-button Meal/Menu/Drink picker above the upload area. Meal is pre-selected and the upload area leads, so a first-time user can just snap a photo. Menu/Drink move behind an optional **"Scanning a menu or a drink instead?"** toggle (`scan.more_options`). No capability removed — only the *required choice* before capture. |
+| **"Ask Shinny about this" on every result** | Each suggestion now carries a primary `💬 Ask Shinny about this` CTA (beside the existing "Scan Another", which becomes secondary) on both the desktop meal sidebar and the mobile result block (covers meal/menu/drink). |
+| **Scan → chat context hand-off** | New `lib/chat-seed.ts` stashes a pre-written question (`scan.ask_seed` with the identified dish name, or `scan.ask_seed_generic`) in `localStorage`; the chat page reads it **read-once** on mount and pre-fills the composer (never auto-sends, never clobbers an in-progress draft). localStorage (not a query param) so the seed survives the login-gate redirect for logged-out users. |
+
+**Decisions taken** (the in-product picker for these didn't resolve, so safe/reversible defaults were chosen and are easy to revisit): kept the existing tested meal pipeline rather than building AI mode auto-detection; kept the chat **login gate** (opening chat to anonymous users has real AI-cost/abuse exposure — a deliberate, separate decision). The "Ask Shinny" CTA only renders on an actual suggestion (`hasResult`), not on the not-food/error cards — you ask about a result, not a rejection.
+
+4 new i18n keys × 4 locales (`ask_shinny`, `ask_seed`, `ask_seed_generic`, `more_options`); locale key count 216 → **233**. Type-check + i18n + **193/193** unit green; `next build` clean. The live scan→chat round-trip against the real AI is covered by the post-deploy `user-journey` e2e gate (the noise-PNG fixture routes to the not-food branch, so the CTA itself is a manual/post-deploy check with a real food photo).
+
### Round 16 (discoverability) — SEO + social + package metadata rework (v2.1.14)
The product was indexed under brand-led copy ("Smart Food Sequencing") while real users search by **problem and tool** ("AI food scanner", "blood sugar", "what to eat first"). This round rewrote the discovery surface — search metadata, social cards, repo/package topic tags, and the README hero — to match search intent. Pure metadata/docs; no runtime behaviour, no test-posture change.
diff --git a/README.md b/README.md
index 9cef8ba..1e5a5b5 100644
--- a/README.md
+++ b/README.md
@@ -165,7 +165,7 @@ Individually:
| Script | Purpose |
|--------|---------|
| `npm run type-check` | Strict TypeScript (`tsc --noEmit`) across the whole frontend |
-| `npm run check:i18n` | Ensures every `t('…')` key used in code exists in **all 4 locale JSONs** (th/en/de/da) — 216 keys each |
+| `npm run check:i18n` | Ensures every `t('…')` key used in code exists in **all 4 locale JSONs** (th/en/de/da) — 233 keys each |
| `npm test` | Vitest unit tests — crypto (PBKDF2 + constant-time compare), ai-prompt validators, zod schemas, auth-store `authChecked` lifecycle |
| `npm run test:e2e` | Playwright e2e against the live deploy — **99 cases** across `smoke`, `ui-ux`, `deep-probes`, `a11y`, `responsive-perf`, `user-journey` (opt-in, needs Chromium + network) |
| `npm run build` | Full Next.js production build, same thing Cloudflare Pages runs |
diff --git a/frontend/src/app/[locale]/chat/page.tsx b/frontend/src/app/[locale]/chat/page.tsx
index dfc7fb7..10e80e0 100644
--- a/frontend/src/app/[locale]/chat/page.tsx
+++ b/frontend/src/app/[locale]/chat/page.tsx
@@ -29,6 +29,7 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Send, Sparkles, AlertCircle, RotateCcw } from 'lucide-react';
import { useAuthStore } from '@/lib/auth-store';
+import { takeChatSeed } from '@/lib/chat-seed';
const STORAGE_KEY = 'shinny-chat-history-v1';
const MAX_HISTORY_TURNS = 50;
@@ -107,6 +108,16 @@ export default function ChatPage() {
setTurns(loadHistory());
}, []);
+ // Pick up a pending "Ask Shinny about this" question handed over from the
+ // scan result. Read-once (takeChatSeed clears it), and only pre-fill an
+ // empty composer so it never clobbers something the user is mid-typing.
+ // The user still taps Send — we don't auto-fire, so they can tweak the
+ // question first and the login gate stays in control of delivery.
+ useEffect(() => {
+ const seed = takeChatSeed();
+ if (seed) setDraft((current) => (current ? current : seed));
+ }, []);
+
// Auto-scroll to the latest message whenever turns or sending changes.
useEffect(() => {
if (scrollRef.current) {
diff --git a/frontend/src/app/[locale]/scan/page.tsx b/frontend/src/app/[locale]/scan/page.tsx
index f3fefa4..c2504fc 100644
--- a/frontend/src/app/[locale]/scan/page.tsx
+++ b/frontend/src/app/[locale]/scan/page.tsx
@@ -3,7 +3,10 @@
import { useRef, useEffect, useState } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import Link from 'next/link';
-import { Scan, Cpu } from 'lucide-react';
+import { useRouter } from 'next/navigation';
+import { Scan, Cpu, MessageCircle } from 'lucide-react';
+
+import { setChatSeed } from '@/lib/chat-seed';
import { useAuthStore } from '@/lib/auth-store';
import { TIER_LIMITS } from '@/lib/tier-config';
@@ -50,6 +53,7 @@ export default function ScanPage() {
const tCommon = useTranslations('common');
const tMascot = useTranslations('mascot');
const locale = useLocale();
+ const router = useRouter();
const { user, isAuthenticated } = useAuthStore();
const tier = (isAuthenticated && user?.subscriptionTier) ? user.subscriptionTier : 'free';
@@ -58,7 +62,12 @@ export default function ScanPage() {
const fileInputRef = useRef(null);
const cameraInputRef = useRef(null);
+ // Default flow is "just snap a meal": meal mode is pre-selected and the
+ // upload area leads. The Menu / Drink modes live behind an optional
+ // toggle so a first-time user never has to choose a mode before they
+ // can take a photo.
const [scanMode, setScanMode] = useState('meal');
+ const [showModeOptions, setShowModeOptions] = useState(false);
// Debug Hook
const debug = useScanDebug();
@@ -110,6 +119,19 @@ export default function ScanPage() {
analysis.resetAnalysis();
};
+ // "Ask Shinny about this" — hand the just-scanned result to the chat
+ // page as a pre-written question so a follow-up is one tap away. Use
+ // the first identified dish name when we have it; otherwise a generic
+ // "about my meal" seed. The chat page reads + clears this on mount.
+ const askShinnyAboutScan = () => {
+ const dishName = analysis.mealResult?.dishes?.[0]?.name?.trim();
+ const seed = dishName
+ ? t('ask_seed', { dish: dishName })
+ : t('ask_seed_generic');
+ setChatSeed(seed);
+ router.push(`/${locale}/chat`);
+ };
+
const isUploading = upload.uploadedImages.length > 0 && !analysis.isAnalyzing && !analysis.hasResult && !analysis.nonFoodReason && !upload.uploadError && !analysis.analysisError;
const isReadyForUpload = upload.uploadedImages.length === 0 && !analysis.isAnalyzing && !analysis.hasResult && !analysis.nonFoodReason;
@@ -151,7 +173,9 @@ export default function ScanPage() {
{isReadyForUpload && (
<>
-
+ {/* Upload area leads — the default path is "just snap a
+ meal". Meal mode is pre-selected; no choice required
+ before taking a photo. */}
{t('privacy_note')}
+
+ {/* Menu / Drink modes are optional — tucked behind a
+ toggle so they don't make the default "snap a meal"
+ path feel like a decision. Picking a non-meal mode
+ keeps the picker open. */}
+