update ai commerce section

This commit is contained in:
2026-07-23 10:26:49 +05:30
parent 4e0facc9af
commit a4328a791a
135 changed files with 4081 additions and 391 deletions

View File

@@ -0,0 +1,214 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useReducedMotion } from "framer-motion";
import { getVoiceRequests } from "@/lib/experience/voice";
import type { VoiceRequest } from "@/types/experience";
// The story the hero plays out, end to end:
// open Nearle → speak → AI understands → AI finds a nearby merchant →
// order created → merchant accepts → packed → rider → delivered.
//
// Note there is no store-selection step. The merchant is *discovered by the AI*
// after it understands the request — that is the whole point of voice commerce.
export type VoiceStage =
| "home" // iOS home screen, Nearle icon pulsing
| "tap" // touch indicator presses the icon
| "launch" // icon expands into the app
| "listening" // mic open, waveform idling
| "speaking" // transcript building word by word
| "thinking" // AI reasoning steps
| "merchant" // nearby merchant resolved
| "order" // draft order + confirm
| "confirmed" // order placed
| "fulfilment" // merchant → packing → rider
| "delivered"; // handed over
export const AI_STEPS = [
"Understanding request…",
"Searching nearby merchants…",
"Checking availability…",
"Comparing options…",
"Creating order…",
] as const;
export const FULFILMENT_STEPS = [
{ label: "Order confirmed", icon: "receipt_long" },
{ label: "Merchant accepts", icon: "storefront" },
{ label: "Packing your order", icon: "inventory_2" },
{ label: "Nearle rider assigned", icon: "two_wheeler" },
{ label: "Delivered", icon: "check_circle" },
] as const;
// A booked service is fulfilled by a person turning up, not a rider dropping a
// bag — so it gets its own track, same five beats and the same pacing.
export const SERVICE_FULFILMENT_STEPS = [
{ label: "Booking confirmed", icon: "receipt_long" },
{ label: "Professional accepts", icon: "engineering" },
{ label: "Slot locked in", icon: "event_available" },
{ label: "On the way to you", icon: "two_wheeler" },
{ label: "Job completed", icon: "check_circle" },
] as const;
// Pacing. Tuned so a visitor reads each beat without waiting on it — the whole
// story lands in one ~24s pass, and the first 8s already says "speak, AI
// understands, a nearby shop fulfils it".
const TIMING = {
home: 1500, // let the home screen register, icon pulses
tap: 450, // press down on the Nearle icon
launch: 700, // iOS-style expand into the app
listening: 1200, // mic opens, waveform idles
wordMs: 190, // per-word cadence of the transcript
afterSpeak: 620, // beat between the last word and the AI starting
thinkStepMs: 900, // gap between reasoning steps
thinkFinalHold: 700, // dwell on the last step
merchant: 1900, // read the merchant card
order: 2600, // read the items + total, then confirm auto-taps
confirmed: 1100, // success bloom
fulfilStepMs: 1050, // each fulfilment beat stays readable
delivered: 1900, // hold on delivered before the next request
} as const;
const REQUESTS = getVoiceRequests();
export type VoiceJourney = {
stage: VoiceStage;
request: VoiceRequest;
requests: VoiceRequest[];
/** Words of the transcript revealed so far. */
spokenWords: number;
thinkingStep: number;
fulfilStep: number;
reduced: boolean;
start: () => void;
stop: () => void;
};
// One shared state machine drives the phone screen. Timers only run while the
// caller says it is visible (start/stop from an in-view observer), so the loop
// costs nothing when the hero is scrolled past.
export function useVoiceJourney(): VoiceJourney {
const reduce = useReducedMotion() ?? false;
const [running, setRunning] = useState(false);
const [stage, setStage] = useState<VoiceStage>("home");
const [index, setIndex] = useState(0);
// Tagged with the request it belongs to, so a new request starts from zero
// words without needing to reset anything.
const [spoken, setSpoken] = useState({ at: -1, n: 0 });
const [think, setThink] = useState({ at: -1, n: 0 });
const [fulfil, setFulfil] = useState({ at: -1, n: 0 });
const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
const intervals = useRef<ReturnType<typeof setInterval>[]>([]);
const clearAll = useCallback(() => {
timers.current.forEach(clearTimeout);
intervals.current.forEach(clearInterval);
timers.current = [];
intervals.current = [];
}, []);
useEffect(() => clearAll, [clearAll]);
const request = useMemo(() => REQUESTS[index % REQUESTS.length], [index]);
const wordCount = useMemo(() => request.transcript.split(" ").length, [request]);
const spokenWords = spoken.at === index ? spoken.n : 0;
const thinkingStep = think.at === index ? think.n : 0;
const fulfilStep = fulfil.at === index ? fulfil.n : 0;
// ── Auto-play orchestration (skipped under reduced motion) ──
useEffect(() => {
if (!running || reduce) return;
clearAll();
const push = (t: ReturnType<typeof setTimeout>) => timers.current.push(t);
const next = (s: VoiceStage, delay: number) => push(setTimeout(() => setStage(s), delay));
switch (stage) {
case "home":
next("tap", TIMING.home);
break;
case "tap":
next("launch", TIMING.tap);
break;
case "launch":
next("listening", TIMING.launch);
break;
case "listening":
next("speaking", TIMING.listening);
break;
case "speaking": {
// Reveal the request one word at a time, the way live dictation lands.
let w = 0;
const iv = setInterval(() => {
w += 1;
setSpoken({ at: index, n: w });
if (w >= wordCount) {
clearInterval(iv);
next("thinking", TIMING.afterSpeak);
}
}, TIMING.wordMs);
intervals.current.push(iv);
break;
}
case "thinking":
for (let s = 1; s < AI_STEPS.length; s += 1) {
push(setTimeout(() => setThink({ at: index, n: s }), TIMING.thinkStepMs * s));
}
next(
"merchant",
TIMING.thinkStepMs * (AI_STEPS.length - 1) + TIMING.thinkFinalHold
);
break;
case "merchant":
next("order", TIMING.merchant);
break;
case "order":
next("confirmed", TIMING.order);
break;
case "confirmed":
next("fulfilment", TIMING.confirmed);
break;
case "fulfilment":
for (let s = 1; s < FULFILMENT_STEPS.length; s += 1) {
push(setTimeout(() => setFulfil({ at: index, n: s }), TIMING.fulfilStepMs * s));
}
next("delivered", TIMING.fulfilStepMs * FULFILMENT_STEPS.length);
break;
case "delivered":
// Loop back to the mic with the *next* request — never the same one twice.
push(
setTimeout(() => {
setIndex((i) => (i + 1) % REQUESTS.length);
setStage("listening");
}, TIMING.delivered)
);
break;
default:
break;
}
return clearAll;
}, [stage, running, reduce, wordCount, index, clearAll]);
// ── Reduced motion: report the completed end state directly ──
// Derived rather than pushed through setState, so nothing ever animates or
// re-renders its way there — the demo is simply already finished.
const settled = reduce && running;
const start = useCallback(() => setRunning(true), []);
const stop = useCallback(() => {
setRunning(false);
clearAll();
}, [clearAll]);
return {
stage: settled ? "delivered" : stage,
request,
requests: REQUESTS,
spokenWords: settled ? wordCount : spokenWords,
thinkingStep: settled ? AI_STEPS.length - 1 : thinkingStep,
fulfilStep: settled ? FULFILMENT_STEPS.length - 1 : fulfilStep,
reduced: reduce,
start,
stop,
};
}