import type { Page } from 'playwright'; import { DashboardRecorder } from './dashboard.js'; import { installCursor, installZoomWrapper } from './dom.js'; import { sleep } from './motion.js'; import { dashboardUrl } from './routes.js'; import { runStoryboard, type RunnerResult } from './runner.js'; import type { ScriptCtx, Storyboard } from './script.js'; export type TimelineResult = RunnerResult; /** * Boot the dashboard, wait for the first map response, and inject the * recording chrome (cursor, zoom wrapper, caption layer). Also opens the * AI prompt textarea so the storyboard can begin typing immediately. */ export async function prepareTimeline( page: Page, storyboard: Storyboard ): Promise { const dashboard = new DashboardRecorder(page); const initialMapVersion = dashboard.getMapDataVersion(); await page.goto(dashboardUrl(storyboard), { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('load', { timeout: 15000 }).catch(() => {}); await page .locator('[data-tutorial="ai-filters"]') .waitFor({ state: 'visible', timeout: 15000 }); await page.locator('canvas').first().waitFor({ state: 'attached', timeout: 15000 }); await dashboard.waitForMapSettled(initialMapVersion, 15000); await sleep(400); await installZoomWrapper(page); await installCursor(page); const ctx: ScriptCtx = { page, dashboard, cursor: { x: 200, y: 240 } }; await page.mouse.move(ctx.cursor.x, ctx.cursor.y); await prepareAiBox(ctx); await sleep(80); return ctx; } export async function runTimeline( ctx: ScriptCtx, storyboard: Storyboard ): Promise { return runStoryboard(ctx, storyboard); } /** * Open the AI prompt before the timed scene starts. This is preparation * work, not part of the storyboard, because waiting for the textarea to * appear has indeterminate duration. */ async function prepareAiBox(ctx: ScriptCtx): Promise { const { page } = ctx; const aiRoot = page.locator('[data-tutorial="ai-filters"]').first(); await aiRoot.waitFor({ state: 'visible', timeout: 15000 }); const textarea = page.locator('[data-tutorial="ai-filters"] textarea'); if (!(await textarea.isVisible().catch(() => false))) { const aiButton = aiRoot.locator('button').first(); await aiButton.waitFor({ state: 'visible', timeout: 8000 }); const btnBox = await aiButton.boundingBox(); if (btnBox) await page.mouse.click(btnBox.x + btnBox.width / 2, btnBox.y + btnBox.height / 2); } if (!(await textarea.isVisible().catch(() => false))) { await page.evaluate(() => { document.querySelector('[data-tutorial="ai-filters"] button')?.click(); }); } await textarea.waitFor({ state: 'visible', timeout: 15000 }); await sleep(100); }