// Shared composition root for practice and embed modes: builds the UI // shell, injects the Blockly workspace, constructs the robot adapter, // and wires the interpreter Runner to the Run/Stop button. import * as Blockly from "blockly"; import type { RobotAdapter } from "../robot/adapter"; import { CommandLog } from "../util/log"; import { createWorkspace, loadProgram, setWorkspaceTheme } from "../blocks/workspace"; import type { EmotionSource, SoundSource } from "../interpreter/commands"; import { Runner } from "../interpreter/runner"; import { STARTER_PROGRAM } from "./starter"; import { buildAppShell, type AppShell } from "./ui"; export interface BootOptions { /** Create the robot once the puppet's host element exists. */ attachRobot(puppetHost: HTMLElement, log: CommandLog): RobotAdapter; theme: "light" | "dark"; emotions: EmotionSource; sounds: SoundSource; /** Skip localStorage restore (e2e hermetic runs). */ fresh?: boolean; } export interface App { shell: AppShell; workspace: Blockly.WorkspaceSvg; robot: RobotAdapter; runner: Runner; log: CommandLog; setTheme(theme: "light" | "dark"): void; loadProgram(state: object): void; /** Override the coin-flip RNG (deterministic tests). */ setRandom(fn: () => number): void; } export function bootApp(root: HTMLElement, opts: BootOptions): App { const shell = buildAppShell(root); const log = new CommandLog(); const robot = opts.attachRobot(shell.puppetHost, log); const workspace = createWorkspace(shell.workspaceHost, { theme: opts.theme, fresh: opts.fresh, starter: STARTER_PROGRAM, }); window.addEventListener("resize", () => Blockly.svgResize(workspace)); let random: () => number = Math.random; const runner = new Runner(workspace, robot, { emotions: opts.emotions, sounds: opts.sounds, random: () => random(), hooks: { onHighlight: (id) => workspace.highlightBlock(id), onRunStateChange: (running) => shell.setRunning(running), }, }); shell.onRunClick(() => { if (runner.running) void runner.stop(); else void runner.run(); }); // Warm the emotion cache in the background so the first "act happy" // doesn't stall on the network. (opts.emotions as { prefetchAll?: () => void }).prefetchAll?.(); return { shell, workspace, robot, runner, log, setTheme(theme: "light" | "dark"): void { document.documentElement.setAttribute("data-theme", theme); setWorkspaceTheme(workspace, theme); }, loadProgram(state: object): void { loadProgram(workspace, state); }, setRandom(fn: () => number): void { random = fn; }, }; }