/* Hand Hub — Live Simulation
Recreates the Veo3 / Google Flow dark UI and animates an AI cursor
driving every manual click, type, upload, submit. The point is to
make the viewer FEEL: "the robot is doing my hands' work." */
const { useState, useEffect, useRef, useMemo } = React;
// ── Step script (one platform loop) ─────────────────────────────────
// Each step describes: (a) what the cursor does, (b) what mutates on screen,
// (c) the human-readable caption shown below the browser.
const PROMPT_TEXT = "Cinematic wide shot, vịnh Hạ Long sương sớm, drone bay chậm trên mặt nước, ánh nắng vàng xuyên qua các đảo đá vôi, máy quay tiến chậm";
const PLATFORMS = {
veo3: { url: "labs.google/fx/vi/tools/flow", profile: "veo3", label: "Veo 3 · Google Flow" },
grok: { url: "grok.com/imagine", profile: "grok", label: "Grok Imagine" },
kling: { url: "klingai.com/text-to-video", profile: "kling", label: "Kling AI" },
};
const STEPS = [
{ id: 1, cap: "Mở Chrome profile", detail: "Veo 3 · profile=veo3", target: "url", wait: 900 },
{ id: 2, cap: "Click vào ô prompt", detail: "contenteditable[role=textbox]", target: "promptBox", click: true, wait: 700 },
{ id: 3, cap: "Bấm nút '+' để mở cài đặt", detail: "click button[aria-label=add]", target: "plus", click: true, wait: 700, mutate: { menu: true } },
{ id: 4, cap: "Chọn loại 'Video'", detail: "menuitem · type=video", target: "menuVideo", click: true, wait: 600, mutate: { type: "video" } },
{ id: 5, cap: "Chọn 'Khung hình'", detail: "menuitem · mode=frame", target: "menuFrame", click: true, wait: 600, mutate: { mode: "frame" } },
{ id: 6, cap: "Chọn tỷ lệ 16:9", detail: "aspect=16:9", target: "menu169", click: true, wait: 600, mutate: { aspect: "16:9" } },
{ id: 7, cap: "Chọn model 'Veo 3.1 - Lite'", detail: "model=Veo3.1-Lite", target: "menuModel", click: true, wait: 600 },
{ id: 8, cap: "Chọn thời lượng 8s", detail: "duration=8s", target: "menu8s", click: true, wait: 700, mutate: { dur: "8s" } },
{ id: 9, cap: "Đóng menu, quay về prompt", detail: "esc · focus textarea", target: "promptBox", click: true, wait: 700, mutate: { menu: false, focus: true } },
{ id: 10, cap: "Gõ prompt 142 ký tự", detail: "type · 38 wpm jitter", target: "promptBox", wait: 2400, mutate: { typing: true } },
{ id: 11, cap: "Bấm nút gửi", detail: "click button[arrow]", target: "submit", click: true, wait: 800, mutate: { typing: false, armed: true } },
{ id: 12, cap: "Chờ render — 28%", detail: "polling DOM · video src", target: "thumb", wait: 1100, mutate: { armed: false, thumb: 28 } },
{ id: 13, cap: "Render xong, tải về máy", detail: "~/Videos/Veo3/job_1.mp4", target: "thumb", wait: 900, mutate: { thumb: 100 } },
{ id: 14, cap: "Sang job kế trong hàng đợi", detail: "next job · giữ profile veo3", target: "url", wait: 900, mutate: { reset: true, platform: "veo3" } },
];
// ── Hit-test coordinates inside the 760×460 browser canvas ──────────
// Adjusted so the cursor lands on real-looking UI element centers.
const COORDS = {
url: { x: 320, y: 26 },
promptBox: { x: 360, y: 388 },
plus: { x: 100, y: 422 },
menuVideo: { x: 605, y: 226 },
menuFrame: { x: 480, y: 268 },
menu169: { x: 605, y: 312 },
menuModel: { x: 540, y: 354 },
menu8s: { x: 615, y: 396 },
submit: { x: 632, y: 422 },
thumb: { x: 130, y: 110 },
};
// ── Pixel flower — recreated from screenshot ───────────────────────
// 8×8 grid; 1 = pixel on, 0 = off
const FLOWER = [
"00111100",
"01111110",
"11000011",
"11000011",
"01111110",
"00011000",
"00111100",
"00000000",
];
function PixelFlower() {
return (
{FLOWER.flatMap((row, r) =>
row.split("").map((c, i) => (
))
)}
);
}
// ── Icons ──────────────────────────────────────────────────────────
const Ico = {
back: ,
search:,
plus: ,
film: ,
gear: ,
help: ,
more: ,
sliders:,
arrowR: ,
grid: ,
download:,
image: ,
videoIc:,
frame: ,
compose:,
phone: ,
};
// ── AI cursor with robot tag ────────────────────────────────────────
function AICursor({ x, y, label, ripple }) {
return (
<>
{ripple !== null && (
)}
>
);
}
// ── The fake Flow dark UI ──────────────────────────────────────────
function FlowPage({ s, typed }) {
const promptText = typed > 0 ? PROMPT_TEXT.slice(0, typed) : "";
const showPlaceholder = !s.typing && promptText.length === 0;
return (
{/* top toolbar */}
May 09, 09:51 PM
{Ico.search}
ULTRA
d
{/* left rail */}
{Ico.grid}
{Ico.image}
{Ico.compose}
{/* stage */}
{/* loading thumbnail */}
0 ? "show" : "")}>
{Ico.image}
{s.thumb}%
{s.thumb > 0 && s.thumb < 100 &&
}
{!s.thumb && (
Bắt đầu tạo hoặc thả nội dung nghe nhìn
)}
{/* settings popup */}
{Ico.image} Hình ảnh
{Ico.videoIc} Video
{Ico.frame} Khung hình
{Ico.compose} Thành phần
Veo 3.1 - Lite
▾
Quá trình tạo sẽ tốn 5 tín dụng
{/* prompt bar */}
{showPlaceholder ? "Bạn muốn tạo gì?" : promptText}
{s.typing && }
Video ▯ 1x
Flow có thể mắc sai sót nên bạn hãy xác minh nội dung do công cụ này tạo
);
}
// ── Queue model ────────────────────────────────────────────────────
const QUEUE = [
{ id: 1, title: "Hạ Long bay sương sớm", platform: "veo3" },
{ id: 2, title: "Phố cổ Hội An đèn lồng đêm", platform: "veo3" },
{ id: 3, title: "Cận cảnh hoa sen nở chậm", platform: "veo3" },
{ id: 4, title: "Drone biển Phú Quốc hoàng hôn", platform: "veo3" },
{ id: 5, title: "Trẻ thả diều ruộng bậc thang", platform: "veo3" },
{ id: 6, title: "Cà phê sữa đá nhỏ giọt", platform: "veo3" },
];
// ── Main component ─────────────────────────────────────────────────
function Simulation() {
const [stepIdx, setStepIdx] = useState(0);
const [paused, setPaused] = useState(false);
const [rippleKey, setRippleKey] = useState(null);
const [typed, setTyped] = useState(0);
const [activeJob, setActiveJob] = useState(0);
const [cursorTarget, setCursorTarget] = useState("url");
const [armedKey, setArmedKey] = useState(null);
// Aggregated state passed to FlowPage
const [state, setState] = useState({
menu: false, type: "video", mode: "frame", aspect: "16:9",
dur: "8s", focus: false, typing: false, armed: false,
thumb: 0, platform: "veo3"
});
// Step driver
useEffect(() => {
if (paused) return;
const step = STEPS[stepIdx];
const target = step.target;
setCursorTarget(target);
setArmedKey(target);
let typingTimer;
const advance = setTimeout(() => {
if (step.click) {
setRippleKey(Date.now());
}
// Apply mutations
if (step.mutate) {
if (step.mutate.reset) {
// job done — bump activeJob and reset visuals after a beat
setTimeout(() => {
setActiveJob(j => Math.min(j + 1, QUEUE.length - 1));
setState({
menu: false, type: "video", mode: "frame", aspect: "16:9",
dur: "8s", focus: false, typing: false, armed: false,
thumb: 0, platform: step.mutate.platform || "veo3"
});
setTyped(0);
}, 300);
} else {
setState(s => ({ ...s, ...step.mutate }));
}
}
// After step.wait, advance
const cont = setTimeout(() => {
setArmedKey(null);
setStepIdx(i => (i + 1) % STEPS.length);
}, step.wait);
return () => clearTimeout(cont);
}, 240);
// Type animation during step 10
if (step.mutate && step.mutate.typing) {
const total = PROMPT_TEXT.length;
let i = 0;
typingTimer = setInterval(() => {
i += Math.random() < 0.5 ? 2 : 3;
setTyped(Math.min(i, total));
if (i >= total) clearInterval(typingTimer);
}, step.wait / (total / 2.5));
}
if (!step.mutate || !step.mutate.typing) {
// Reset typed when starting a new platform loop
if (step.mutate && step.mutate.reset) setTyped(0);
}
return () => {
clearTimeout(advance);
if (typingTimer) clearInterval(typingTimer);
};
}, [stepIdx, paused]);
// Pass armedKey through state for FlowPage
const flowState = { ...state, armedKey };
// Cursor coordinates
const coords = COORDS[cursorTarget] || COORDS.url;
// Active platform / URL
const platform = PLATFORMS[state.platform] || PLATFORMS.veo3;
return (
{/* LEFT side */}
{/* RIGHT — fake browser */}
🔒
{platform.url}
Profile: {platform.profile}
STEP {String(stepIdx+1).padStart(2,"0")}/{String(STEPS.length).padStart(2,"0")}
{STEPS[stepIdx].cap}
{STEPS[stepIdx].detail}
);
}
window.Simulation = Simulation;