Grabby-Voice / README.md
moonlantern1's picture
Deploy Grabby Voice mobile app
a733514 verified
metadata
title: Grabby Voice
colorFrom: green
colorTo: yellow
sdk: docker
app_port: 7860
fullWidth: true
pinned: false

Matcha Moments β€” frontend

Cafe-aesthetic, mobile-first PWA that walks a customer through a 5-clip guided video review and, on submit, hands them a matcha redemption code.

This repo is a standalone Next.js app. It calls Humeo's deployed public review APIs (https://humeo.app/api/public/reviews/*) β€” no backend changes needed in the Humeo monorepo for v1.

The original Humeo codebase lives at reference/ for type / pattern lookups. It's gitignored, so it never ships with this repo.


Tech stack

  • Next.js 14 (App Router) + TypeScript + Tailwind CSS
  • @ffmpeg/ffmpeg (ffmpeg.wasm) β€” client-side concatenation of the 5 recorded clips into one video before upload
  • getUserMedia + MediaRecorder β€” standard browser camera APIs (no native install required)
  • zod β€” shared validation schemas, mirrors the ones in Humeo's src/lib/reviews/types.ts

Why a PWA over Expo: the customer-facing flow has to start in a tabletop QR scan in a cafe. Asking the customer to install an app kills conversion. Browser-based flow opens in 2 seconds, no install, works on iOS Safari and Android Chrome.


What it does (5 screens)

  1. / β€” QR landing context screen (dev-only; in prod, the cafe's QR deep-links straight to /c/[slug])
  2. /c/[slug] β€” Cafe landing: brand, big "Free matcha, on the house" headline, consent copy, primary CTA
  3. /c/[slug]/record β€” Guided 5-clip recorder (video preview β†’ prompt card β†’ record button β†’ auto-advance)
  4. /preview β€” ffmpeg.wasm stitches the clips, uploads to Humeo, polls submission status, shows the rendered preview
  5. /reward β€” Confetti, reward code in a dark card, "show this screen to your server"

The [slug] route is a real Next.js dynamic segment that fetches its campaign from ${NEXT_PUBLIC_HUMEO_API_URL}/api/public/reviews/campaign/[slug], the same endpoint Humeo's existing public review flow already uses (reference/src/app/api/public/reviews/campaign/[slug]/route.ts).


Getting started

cp .env.example .env.local      # then edit NEXT_PUBLIC_HUMEO_API_URL if needed
npm install
npm run dev                     # http://localhost:3000

Test on your phone (recommended)

getUserMedia only works on https:// (or http://localhost). Easiest path:

npx ngrok http 3000

Then open the https://*.ngrok-free.app URL on your phone, scan the QR or load /c/sageandstone directly. iOS Safari and Android Chrome will prompt for camera and mic. Allow both.


How it talks to Humeo

matcha-moments PWA                   Humeo backend (deployed)
-----------------                    ------------------------
GET  /c/[slug]            ──────►    GET  /api/public/reviews/campaign/[slug]
                          ◄──────    { id, slug, restaurantName, rulesConfig, ... }

stitch clips locally (ffmpeg.wasm)

POST /preview submit      ──────►    POST /api/public/reviews/submit
                                       FormData: video, slug, consentAccepted,
                                                 deviceKey, durationSeconds, tableId
                          ◄──────    { submissionId, status, decision, reward }

poll every 6s             ──────►    GET  /api/public/reviews/submission/[id]?slug=...
                          ◄──────    { status, decision, feedback, reward }

src/lib/humeoApi.ts is the only place that fetches from humeo.app. If Humeo's review_campaigns row doesn't yet have mode / prompts / theme columns, getCampaign() augments the response with a hardcoded fallback prompts list β€” flagged with TODO so we drop it once the BE migration ships.

Fields Humeo's BE will eventually need

To remove the fallback, Humeo's review_campaigns schema would add:

  • mode text β€” 'single_take' | 'guided_clips'
  • prompts jsonb β€” array of { step, title, tip, camera, maxSeconds }
  • theme text β€” 'default' | 'cafe-cream'

Until then the matcha-moments app silently injects the cafe defaults.


Why client-side ffmpeg.wasm?

Humeo's /api/public/reviews/submit accepts a single video file. We want a multi-clip guided UX without forking Humeo's submit flow. Stitching the 5 recordings in the browser solves that with zero backend changes.

Trade-offs:

  • 8MB WASM download, lazy-loaded only after the customer finishes recording
  • 3-6 seconds of stitch time on a modern phone for ~50s of total video
  • next.config.js sets COOP/COEP headers (required for SharedArrayBuffer)

If cafe staff start hearing complaints about phone heat, swap to a multi-clip upload + server-side ffmpeg endpoint. Humeo's worker (reference/src/lib/server/processInterview.ts) already uses ffmpeg, so the migration is mostly a new submit endpoint.


Project layout

src/
  app/
    layout.tsx                    Fonts (Fraunces / DM Sans / DM Mono), global CSS
    page.tsx                      QR landing (dev-only)
    c/[slug]/
      page.tsx                    Server component β€” fetches campaign
      LandingClient.tsx           Cafe landing screen
      record/
        page.tsx                  Server component β€” fetches campaign
        GuidedRecordingClient.tsx 5-clip guided recorder
    preview/page.tsx              Stitch + upload + preview
    reward/page.tsx               Reward code reveal
    globals.css
  components/                     Button, MatchaCircle, ProgressPips, PromptCard,
                                  RecordButton, RecordingBadge, RenderShimmer,
                                  Confetti, RewardCard
  hooks/
    useGuidedRecording.ts         getUserMedia + MediaRecorder + hard cap
    useSubmissionPolling.ts       Mirrors Humeo's PublicReviewRecordingClient polling
  lib/
    humeoApi.ts                   Typed fetch wrapper for /api/public/reviews/*
    ffmpeg.ts                     ffmpeg.wasm concatClips() helper
    recordingStore.ts             In-tab clip store, useRecordingStore() hook
    utils.ts                      cn(), ensureDeviceKey()
    reviews/
      types.ts                    Zod schemas + types (mirrors Humeo's)
      public.ts                   Display helpers (verbatim from Humeo)
reference/                        Humeo's repo (gitignored, read-only library)
matcha-moments-prototype_2.html   Original wireframe (visual spec)

Out of scope (v1)

  • Per-clip re-record (current "re-record" wipes all 5 β€” flagged as a known product call in matcha-moments-prototype_2.html dev notes)
  • Email-me-a-copy of the final video
  • Staff-side redemption screen
  • Reward expiry / redemption tracking
  • Native iOS/Android wrapping (Humeo can ship this as a Capacitor or PWA-installed shortcut later)

Deploying

Vercel β€” connect this repo, set NEXT_PUBLIC_HUMEO_API_URL=https://humeo.app, done. The COOP/COEP headers in next.config.js carry over on Vercel.

CORS: confirm humeo.app allow-lists this app's deploy domain (e.g. matcha.humeo.app) on the /api/public/reviews/* routes.