@vanillaskyai/video 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -2255,6 +2255,17 @@ function reducer(state, action) {
2255
2255
  error: void 0
2256
2256
  };
2257
2257
  }
2258
+ case "restore": {
2259
+ const restored = {
2260
+ ...initialState(state.muted),
2261
+ capabilities: state.capabilities,
2262
+ welcome: state.welcome,
2263
+ turns: action.turns,
2264
+ playerKey: state.playerKey
2265
+ };
2266
+ const latest = action.turns.at(-1);
2267
+ return latest ? reducer(restored, { type: "select", id: latest.id }) : restored;
2268
+ }
2258
2269
  case "reset":
2259
2270
  return { ...initialState(state.muted), capabilities: state.capabilities, welcome: state.welcome };
2260
2271
  }
@@ -2338,6 +2349,9 @@ function prepareVisualScene(scene, mode) {
2338
2349
  return overMedia ? { ...shown, textArchetype: "subtle" } : shown;
2339
2350
  }
2340
2351
  function useVideoChat(options = {}) {
2352
+ return useVideoChatSession(options).chat;
2353
+ }
2354
+ function useVideoChatSession(options = {}) {
2341
2355
  const optionsRef = useRef3(options);
2342
2356
  optionsRef.current = options;
2343
2357
  const voiceWarningRef = useRef3(() => void 0);
@@ -2861,6 +2875,12 @@ function useVideoChat(options = {}) {
2861
2875
  voiceRef.current.resume();
2862
2876
  dispatch({ type: "reset" });
2863
2877
  }, [cancel]);
2878
+ const restoreSession = useCallback2((turns) => {
2879
+ cancel("Restoring session");
2880
+ heldRef.current = false;
2881
+ voiceRef.current.resume();
2882
+ dispatch({ type: "restore", turns: turns.filter((turn) => turn.completed && turn.video).slice(-100) });
2883
+ }, [cancel]);
2864
2884
  const setMuted = useCallback2((muted) => dispatch({ type: "mute", value: muted }), []);
2865
2885
  const currentTurn = state.turns.at(-1);
2866
2886
  const shownTurn = state.turns.find((turn) => turn.id === state.shownTurnId) ?? currentTurn;
@@ -2907,7 +2927,7 @@ function useVideoChat(options = {}) {
2907
2927
  if (id) dispatch({ type: "error", id, error: errorFrom(cause) });
2908
2928
  }
2909
2929
  } : void 0;
2910
- return {
2930
+ const chat = {
2911
2931
  ask,
2912
2932
  cancel,
2913
2933
  pause,
@@ -2934,15 +2954,16 @@ function useVideoChat(options = {}) {
2934
2954
  playerKey: state.playerKey,
2935
2955
  playerProps
2936
2956
  };
2957
+ return { chat, restoreSession };
2937
2958
  }
2938
2959
 
2939
2960
  // src/video-chat/video-chat.tsx
2940
- import { useCallback as useCallback5, useEffect as useEffect8, useId, useMemo as useMemo2, useRef as useRef6, useState as useState5 } from "react";
2961
+ import { useCallback as useCallback6, useEffect as useEffect9, useId, useLayoutEffect, useMemo as useMemo2, useRef as useRef7, useState as useState6 } from "react";
2941
2962
 
2942
2963
  // src/video-chat/modes.ts
2943
2964
  var visualModes = [
2944
- { id: "templates", label: "Templates only", note: "Rendered scenes \xB7 free and instant" },
2945
- { id: "full", label: "Full AI video", note: "Every beat filmed \xB7 billed per clip" }
2965
+ { id: "templates", label: "Templates only", note: "Animated text, diagrams and charts" },
2966
+ { id: "full", label: "Full AI video", note: "AI-generated footage \xB7 billed per clip" }
2946
2967
  ];
2947
2968
  var defaultMode = visualModes[0];
2948
2969
  var modeById = (id) => visualModes.find((mode) => mode.id === id) ?? defaultMode;
@@ -3406,14 +3427,77 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
3406
3427
  return { supported, listening, thinking, error, toggle, stop };
3407
3428
  }
3408
3429
 
3430
+ // src/video-chat/use-immersive-controls.ts
3431
+ import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef6, useState as useState5 } from "react";
3432
+ function useImmersiveControls(playing, pinned, hasCaptions = false) {
3433
+ const [visible, setVisible] = useState5(true);
3434
+ const hovered = useRef6(false);
3435
+ const focused = useRef6(false);
3436
+ const previousCaptions = useRef6(false);
3437
+ const timer = useRef6(null);
3438
+ const clearTimer = useCallback5(() => {
3439
+ if (timer.current !== null) clearTimeout(timer.current);
3440
+ timer.current = null;
3441
+ }, []);
3442
+ const reveal = useCallback5(() => {
3443
+ clearTimer();
3444
+ setVisible(true);
3445
+ if (playing && !pinned && !hovered.current && !focused.current) {
3446
+ timer.current = setTimeout(() => {
3447
+ timer.current = null;
3448
+ setVisible(false);
3449
+ }, 2e3);
3450
+ }
3451
+ }, [clearTimer, playing, pinned]);
3452
+ useEffect8(() => {
3453
+ const firstCaption = hasCaptions && !previousCaptions.current;
3454
+ previousCaptions.current = hasCaptions;
3455
+ if (firstCaption && playing && !pinned && !focused.current) {
3456
+ hovered.current = false;
3457
+ clearTimer();
3458
+ setVisible(false);
3459
+ } else {
3460
+ reveal();
3461
+ }
3462
+ return clearTimer;
3463
+ }, [hasCaptions, playing, pinned, reveal, clearTimer]);
3464
+ const onPointerEnter = useCallback5((event) => {
3465
+ if (event?.pointerType === "touch") return;
3466
+ hovered.current = true;
3467
+ reveal();
3468
+ }, [reveal]);
3469
+ const onPointerLeave = useCallback5(() => {
3470
+ if (!hovered.current) return;
3471
+ hovered.current = false;
3472
+ reveal();
3473
+ }, [reveal]);
3474
+ const onFocusCapture = useCallback5((event) => {
3475
+ focused.current = event.target.matches(":focus-visible");
3476
+ reveal();
3477
+ }, [reveal]);
3478
+ const onBlurCapture = useCallback5((event) => {
3479
+ if (focused.current && !event.currentTarget.contains(event.relatedTarget)) {
3480
+ focused.current = false;
3481
+ reveal();
3482
+ }
3483
+ }, [reveal]);
3484
+ return { visible, reveal, onPointerEnter, onPointerLeave, onFocusCapture, onBlurCapture };
3485
+ }
3486
+
3487
+ // src/video-chat/logo.tsx
3488
+ import { jsx as jsx6 } from "react/jsx-runtime";
3489
+ function Logo() {
3490
+ return /* @__PURE__ */ jsx6("img", { className: "brand-logo", src: new URL("./assets/vanillasky-logo.svg", import.meta.url).href, alt: "VanillaSky", width: 144, height: 36 });
3491
+ }
3492
+
3409
3493
  // src/video-chat/video-chat.tsx
3410
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3494
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3411
3495
  var DESKTOP_WIDTH = 900;
3412
3496
  function useViewportOrientation() {
3413
- const [portrait, setPortrait] = useState5(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`).matches : false);
3414
- useEffect8(() => {
3497
+ const [portrait, setPortrait] = useState6(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`).matches : false);
3498
+ useEffect9(() => {
3415
3499
  if (typeof window.matchMedia !== "function") return;
3416
- const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`);
3500
+ const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`);
3417
3501
  const update = () => setPortrait(query.matches);
3418
3502
  update();
3419
3503
  query.addEventListener("change", update);
@@ -3428,8 +3512,8 @@ var STEPS = [
3428
3512
  "Still filming\u2026"
3429
3513
  ];
3430
3514
  function useFilmingStep(active, key) {
3431
- const [step, setStep] = useState5(0);
3432
- useEffect8(() => {
3515
+ const [step, setStep] = useState6(0);
3516
+ useEffect9(() => {
3433
3517
  if (!active) return;
3434
3518
  setStep(0);
3435
3519
  const stepper = window.setInterval(() => setStep((current) => current + 1), 3e3);
@@ -3438,22 +3522,28 @@ function useFilmingStep(active, key) {
3438
3522
  return STEPS[Math.min(step, STEPS.length - 1)];
3439
3523
  }
3440
3524
  function Waveform({ active, listening }) {
3441
- return /* @__PURE__ */ jsx6("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx6("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
3525
+ return /* @__PURE__ */ jsx7("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx7("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
3442
3526
  }
3443
3527
  function VideoChat({ options = {}, className, welcomeTitle }) {
3444
- const [draft, setDraft] = useState5("");
3445
- const [themeId, setThemeId] = useState5(defaultTheme.id);
3446
- const [modeId, setModeId] = useState5(options.mode ?? defaultMode.id);
3447
- const [historyOpen, setHistoryOpen] = useState5(false);
3448
- const [settingsOpen, setSettingsOpen] = useState5(false);
3449
- const [sheetOpen, setSheetOpen] = useState5(false);
3528
+ const [draft, setDraft] = useState6("");
3529
+ const [savedSessions, setSavedSessions] = useState6([]);
3530
+ const [themeId, setThemeId] = useState6(defaultTheme.id);
3531
+ const [modeId, setModeId] = useState6(options.mode ?? defaultMode.id);
3532
+ const [historyOpen, setHistoryOpen] = useState6(false);
3533
+ const [settingsOpen, setSettingsOpen] = useState6(false);
3534
+ const [captionsOn, setCaptionsOn] = useState6(true);
3535
+ const [captionsExpanded, setCaptionsExpanded] = useState6(false);
3536
+ const [alwaysShowControls, setAlwaysShowControls] = useState6(false);
3537
+ const [editing, setEditing] = useState6(false);
3538
+ const resumeAfterInput = useRef7(false);
3539
+ const panelRef = useRef7(null);
3540
+ const composerRef = useRef7(null);
3450
3541
  const viewportOrientation = useViewportOrientation();
3451
- const [themeChoice, setThemeChoice] = useState5("system");
3452
3542
  const theme = themeById(themeId);
3453
3543
  const sessionOrientation = options.orientation ?? viewportOrientation;
3454
3544
  const { brand: configuredBrand, style: configuredStyle, ...sessionOptions } = options;
3455
3545
  const presentationBrand = configuredBrand ?? theme.brand;
3456
- const chat = useVideoChat({
3546
+ const { chat, restoreSession } = useVideoChatSession({
3457
3547
  ...sessionOptions,
3458
3548
  mode: modeId,
3459
3549
  orientation: sessionOrientation,
@@ -3464,16 +3554,13 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
3464
3554
  const historyId = `${instanceId}-history`;
3465
3555
  const settingsId = `${instanceId}-settings`;
3466
3556
  const promptId = `${instanceId}-prompt`;
3467
- const responseTitleId = `${instanceId}-response-title`;
3468
- const appearanceName = `${instanceId}-appearance`;
3469
3557
  const visualsName = `${instanceId}-visuals`;
3470
3558
  const styleName = `${instanceId}-style`;
3471
- const inputRef = useRef6(null);
3472
- const historyRef = useRef6(null);
3473
- const historyButtonRef = useRef6(null);
3474
- const settingsRef = useRef6(null);
3475
- const settingsButtonRef = useRef6(null);
3476
- const sheetRef = useRef6(null);
3559
+ const inputRef = useRef7(null);
3560
+ const historyRef = useRef7(null);
3561
+ const historyButtonRef = useRef7(null);
3562
+ const settingsRef = useRef7(null);
3563
+ const settingsButtonRef = useRef7(null);
3477
3564
  const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
3478
3565
  endpoint: options.endpoint,
3479
3566
  headers: options.headers,
@@ -3482,40 +3569,49 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
3482
3569
  });
3483
3570
  const historySurfaces = useMemo2(() => [historyRef, historyButtonRef], []);
3484
3571
  const settingsSurfaces = useMemo2(() => [settingsRef, settingsButtonRef], []);
3485
- const noSurfaces = useMemo2(() => [], []);
3486
- const closeHistory = useCallback5(() => setHistoryOpen(false), []);
3487
- const closeSettings = useCallback5(() => setSettingsOpen(false), []);
3488
- const closeSheet = useCallback5(() => setSheetOpen(false), []);
3572
+ const closeHistory = useCallback6(() => setHistoryOpen(false), []);
3573
+ const closeSettings = useCallback6(() => setSettingsOpen(false), []);
3489
3574
  useDismiss(historyOpen, closeHistory, historySurfaces);
3490
3575
  useDismiss(settingsOpen, closeSettings, settingsSurfaces);
3491
- useDismiss(sheetOpen, closeSheet, noSurfaces);
3492
- useFocusTrap(sheetOpen, sheetRef);
3493
- const ask = useCallback5((value) => {
3576
+ useFocusTrap(settingsOpen, settingsRef);
3577
+ useFocusTrap(historyOpen, historyRef);
3578
+ const ask = useCallback6((value) => {
3494
3579
  const prompt = (typeof value === "string" ? value : value.prompt).trim();
3495
3580
  if (!prompt) return;
3496
3581
  setDraft("");
3497
3582
  listen.stop();
3498
3583
  setHistoryOpen(false);
3499
3584
  setSettingsOpen(false);
3500
- setSheetOpen(false);
3585
+ setCaptionsExpanded(false);
3586
+ setEditing(false);
3587
+ resumeAfterInput.current = false;
3588
+ inputRef.current?.blur();
3501
3589
  void chat.ask(prompt, typeof value === "string" ? void 0 : {
3502
3590
  openingMedia: value.media,
3503
3591
  opening: value.opening
3504
3592
  });
3505
3593
  }, [chat, listen]);
3506
- const newSession = useCallback5(() => {
3594
+ const newSession = useCallback6(() => {
3507
3595
  if (chat.turns.length === 0) {
3508
3596
  inputRef.current?.focus();
3509
3597
  return;
3510
3598
  }
3599
+ listen.stop();
3600
+ setDraft("");
3601
+ const completed = chat.turns.filter((turn) => turn.completed && turn.video);
3602
+ if (completed.length) setSavedSessions((sessions) => [{ id: completed[0].id, turns: completed }, ...sessions].slice(0, 10));
3511
3603
  chat.reset();
3512
3604
  setHistoryOpen(false);
3513
3605
  setSettingsOpen(false);
3514
- setSheetOpen(false);
3515
- }, [chat]);
3606
+ setCaptionsExpanded(false);
3607
+ setEditing(false);
3608
+ resumeAfterInput.current = false;
3609
+ inputRef.current?.blur();
3610
+ }, [chat, listen]);
3516
3611
  const current = chat.currentTurn;
3517
3612
  const shown = chat.shownTurn;
3518
3613
  const showing = chat.playerProps != null;
3614
+ const ambientPoster = shown?.openingMedia?.type === "image" ? shown.openingMedia.url : shown?.openingMedia?.posterUrl;
3519
3615
  const waitingForPicture = chat.turns.length > 0 && !showing && (chat.status === "composing" || chat.status === "playing" || chat.status === "paused");
3520
3616
  const filmingStep = useFilmingStep(waitingForPicture, current?.id);
3521
3617
  const status = chat.turns.length === 0 ? "idle" : chat.status === "composing" ? "drawing" : chat.status === "playing" ? "narrating" : chat.status === "error" || chat.status === "cancelled" ? "ended" : chat.status;
@@ -3524,47 +3620,99 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
3524
3620
  const availableModes = visualModes.filter((option) => chat.availableModes.includes(option.id));
3525
3621
  const selectedMode = modeById(modeId);
3526
3622
  const line = chat.caption ?? "";
3527
- const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx6(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx6(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx6(Replay, {}) } : void 0;
3623
+ const fullTranscript = shown?.video ? [shown.opening, ...shown.video.scenes.map((scene) => scene.narration)].filter((entry) => Boolean(entry)) : chat.transcript;
3624
+ const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx7(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx7(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx7(Replay, {}) } : void 0;
3625
+ const cancelInput = useCallback6(() => {
3626
+ listen.stop();
3627
+ setDraft("");
3628
+ setEditing(false);
3629
+ inputRef.current?.blur();
3630
+ if (resumeAfterInput.current) chat.resume();
3631
+ resumeAfterInput.current = false;
3632
+ }, [listen, chat]);
3633
+ const beginInput = (speak = false) => {
3634
+ if (!editing) {
3635
+ resumeAfterInput.current = chat.status === "playing" || chat.status === "composing";
3636
+ if (resumeAfterInput.current) chat.pause();
3637
+ }
3638
+ setEditing(true);
3639
+ setSettingsOpen(false);
3640
+ setHistoryOpen(false);
3641
+ if (speak) listen.toggle();
3642
+ };
3643
+ const controls = useImmersiveControls(
3644
+ status === "narrating" || Boolean(line) && status !== "paused",
3645
+ alwaysShowControls || captionsExpanded || editing || historyOpen || settingsOpen || listen.listening || listen.thinking || Boolean(chat.error || listen.error),
3646
+ captionsOn && Boolean(line)
3647
+ );
3648
+ const captionControls = useImmersiveControls(Boolean(line), captionsExpanded, false);
3649
+ const controlEvents = {
3650
+ onPointerEnter: controls.onPointerEnter,
3651
+ onPointerLeave: controls.onPointerLeave,
3652
+ onFocusCapture: controls.onFocusCapture,
3653
+ onBlurCapture: controls.onBlurCapture
3654
+ };
3655
+ useLayoutEffect(() => {
3656
+ const composer = composerRef.current;
3657
+ if (!composer) return;
3658
+ const measure = () => panelRef.current?.style.setProperty("--composer-height", `${composer.getBoundingClientRect().height}px`);
3659
+ measure();
3660
+ if (typeof ResizeObserver === "undefined") return;
3661
+ const observer = new ResizeObserver(measure);
3662
+ observer.observe(composer);
3663
+ return () => observer.disconnect();
3664
+ }, []);
3528
3665
  return /* @__PURE__ */ jsxs6(
3529
3666
  "div",
3530
3667
  {
3531
3668
  className: `vanillasky-video-chat${className ? ` ${className}` : ""}`,
3532
3669
  "data-orientation": stageOrientation,
3533
- "data-theme": themeChoice,
3670
+ "data-controls-visible": controls.visible,
3671
+ tabIndex: 0,
3672
+ "aria-label": "Video conversation",
3673
+ onPointerMove: controls.reveal,
3674
+ onPointerDown: controls.reveal,
3675
+ onKeyDownCapture: controls.reveal,
3534
3676
  children: [
3535
- /* @__PURE__ */ jsxs6("header", { className: "chrome", children: [
3677
+ /* @__PURE__ */ jsxs6("header", { className: "chrome", ...controlEvents, children: [
3678
+ /* @__PURE__ */ jsx7("div", { className: "session-brand", children: /* @__PURE__ */ jsx7(Logo, {}) }),
3536
3679
  /* @__PURE__ */ jsxs6("div", { className: "group", children: [
3537
- /* @__PURE__ */ jsx6(
3680
+ /* @__PURE__ */ jsxs6(
3538
3681
  "button",
3539
3682
  {
3540
3683
  type: "button",
3541
3684
  className: "round",
3542
- "aria-label": chat.turns.length === 0 ? "Ask a prompt" : "New session",
3685
+ "aria-label": "New session",
3543
3686
  onClick: newSession,
3544
- children: /* @__PURE__ */ jsx6(Plus, {})
3687
+ children: [
3688
+ /* @__PURE__ */ jsx7(Plus, {}),
3689
+ /* @__PURE__ */ jsx7("span", { className: "nav-label", children: "New session" })
3690
+ ]
3545
3691
  }
3546
3692
  ),
3547
- chat.turns.length > 0 && /* @__PURE__ */ jsxs6(
3693
+ /* @__PURE__ */ jsxs6(
3548
3694
  "button",
3549
3695
  {
3550
3696
  ref: historyButtonRef,
3551
3697
  type: "button",
3552
3698
  className: "pill",
3553
3699
  "aria-expanded": historyOpen,
3700
+ "aria-label": "History",
3701
+ "aria-haspopup": "dialog",
3554
3702
  "aria-controls": historyId,
3555
3703
  onClick: () => {
3556
3704
  setHistoryOpen((open) => !open);
3557
3705
  setSettingsOpen(false);
3558
3706
  },
3559
3707
  children: [
3560
- chat.turns.length,
3561
- " asked"
3708
+ /* @__PURE__ */ jsx7(Replay, {}),
3709
+ /* @__PURE__ */ jsx7("span", { className: "nav-label", children: "History" })
3562
3710
  ]
3563
3711
  }
3564
3712
  )
3565
3713
  ] }),
3566
3714
  /* @__PURE__ */ jsxs6("div", { className: "group", children: [
3567
- /* @__PURE__ */ jsx6(
3715
+ /* @__PURE__ */ jsx7(
3568
3716
  "button",
3569
3717
  {
3570
3718
  ref: settingsButtonRef,
@@ -3578,10 +3726,10 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
3578
3726
  setSettingsOpen((open) => !open);
3579
3727
  setHistoryOpen(false);
3580
3728
  },
3581
- children: /* @__PURE__ */ jsx6(Gear, {})
3729
+ children: /* @__PURE__ */ jsx7(Gear, {})
3582
3730
  }
3583
3731
  ),
3584
- /* @__PURE__ */ jsx6(
3732
+ /* @__PURE__ */ jsx7(
3585
3733
  "button",
3586
3734
  {
3587
3735
  type: "button",
@@ -3589,60 +3737,102 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
3589
3737
  "aria-label": chat.muted ? "Turn the voice on" : "Turn the voice off",
3590
3738
  "aria-pressed": chat.muted,
3591
3739
  onClick: () => chat.setMuted(!chat.muted),
3592
- children: chat.muted ? /* @__PURE__ */ jsx6(Muted, {}) : /* @__PURE__ */ jsx6(Sound, {})
3740
+ children: chat.muted ? /* @__PURE__ */ jsx7(Muted, {}) : /* @__PURE__ */ jsx7(Sound, {})
3593
3741
  }
3594
3742
  )
3595
3743
  ] })
3596
3744
  ] }),
3597
3745
  /* @__PURE__ */ jsxs6("div", { className: "stage-area", children: [
3598
3746
  /* @__PURE__ */ jsxs6("div", { className: "stage", style: { background: themeBackground(presentationBrand) }, children: [
3747
+ showing && /* @__PURE__ */ jsx7("div", { className: "ambient-media", "aria-hidden": "true", children: ambientPoster && /* @__PURE__ */ jsx7("img", { className: "frame-media", src: ambientPoster, alt: "" }) }),
3599
3748
  !showing && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3600
- /* @__PURE__ */ jsx6("div", { className: "ground", "aria-hidden": "true" }),
3749
+ /* @__PURE__ */ jsx7("div", { className: "ground", "aria-hidden": "true" }),
3601
3750
  shown?.openingMedia && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3602
- /* @__PURE__ */ jsx6(Frame, { media: shown.openingMedia, poster: true }),
3603
- /* @__PURE__ */ jsx6("div", { className: "opening-wash", "aria-hidden": "true" })
3751
+ /* @__PURE__ */ jsx7(Frame, { media: shown.openingMedia, poster: true }),
3752
+ /* @__PURE__ */ jsx7("div", { className: "opening-wash", "aria-hidden": "true" })
3604
3753
  ] }),
3605
- chat.turns.length === 0 && /* @__PURE__ */ jsx6(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
3754
+ chat.turns.length === 0 && /* @__PURE__ */ jsx7(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
3606
3755
  shown?.prompt && /* @__PURE__ */ jsxs6("div", { className: "asked", children: [
3607
- /* @__PURE__ */ jsx6("p", { className: "asked-prompt", children: shown.prompt }),
3608
- waitingForPicture && /* @__PURE__ */ jsx6("p", { className: "asked-step", "aria-live": "polite", children: filmingStep })
3756
+ /* @__PURE__ */ jsx7("p", { className: "asked-prompt", children: shown.prompt }),
3757
+ waitingForPicture && /* @__PURE__ */ jsx7("p", { className: "asked-step", "aria-live": "polite", children: filmingStep })
3609
3758
  ] })
3610
3759
  ] }),
3611
- chat.playerProps && /* @__PURE__ */ jsx6(
3760
+ chat.playerProps && /* @__PURE__ */ jsx7("div", { className: "player-fit", style: { width: stageOrientation === "portrait" ? "min(100cqw, 56.25cqh)" : "min(100cqw, 177.7778cqh)" }, children: /* @__PURE__ */ jsx7(
3612
3761
  VideoPlayer,
3613
3762
  {
3614
3763
  ...chat.playerProps,
3615
3764
  templates: options.templates,
3765
+ orientation: stageOrientation,
3616
3766
  responsiveBreakpoint: DESKTOP_WIDTH,
3617
3767
  ariaLabel: chat.playerProps.video ? "Replay" : "The response"
3618
3768
  },
3619
3769
  chat.playerKey
3620
- ),
3770
+ ) }),
3621
3771
  showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "ending", children: [
3622
- /* @__PURE__ */ jsx6("div", { className: "ending-wash", "aria-hidden": "true" }),
3772
+ /* @__PURE__ */ jsx7("div", { className: "ending-wash", "aria-hidden": "true" }),
3623
3773
  /* @__PURE__ */ jsxs6("div", { className: "ending-body", children: [
3624
- /* @__PURE__ */ jsx6("p", { className: "ending-label", children: "Ask next" }),
3625
- /* @__PURE__ */ jsx6(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
3774
+ /* @__PURE__ */ jsx7("p", { className: "ending-label", children: "Ask next" }),
3775
+ /* @__PURE__ */ jsx7(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
3626
3776
  ] })
3627
3777
  ] })
3628
3778
  ] }),
3629
- historyOpen && /* @__PURE__ */ jsx6("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", "aria-label": "Prompts this session", children: chat.turns.map((turn, index) => /* @__PURE__ */ jsxs6(
3630
- "button",
3631
- {
3632
- type: "button",
3633
- className: turn.id === shown?.id ? "current" : void 0,
3634
- disabled: !turn.completed || !turn.video,
3635
- onClick: () => {
3636
- chat.selectTurn(turn.id);
3637
- setHistoryOpen(false);
3779
+ historyOpen && /* @__PURE__ */ jsxs6("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "History", children: [
3780
+ /* @__PURE__ */ jsxs6("div", { className: "popover-heading", children: [
3781
+ /* @__PURE__ */ jsx7("h2", { children: "History" }),
3782
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "round", "aria-label": "Close history", onClick: closeHistory, children: /* @__PURE__ */ jsx7(Close, {}) })
3783
+ ] }),
3784
+ /* @__PURE__ */ jsx7("h3", { className: "section-label", children: "Current session" }),
3785
+ chat.turns.length === 0 && /* @__PURE__ */ jsx7("p", { className: "history-empty", children: "Your questions will appear here." }),
3786
+ chat.turns.map((turn, index) => /* @__PURE__ */ jsxs6(
3787
+ "button",
3788
+ {
3789
+ type: "button",
3790
+ className: `history-row${turn.id === shown?.id ? " current" : ""}`,
3791
+ "aria-current": turn.id === shown?.id ? true : void 0,
3792
+ disabled: !turn.completed || !turn.video,
3793
+ onClick: () => {
3794
+ listen.stop();
3795
+ setDraft("");
3796
+ setEditing(false);
3797
+ resumeAfterInput.current = false;
3798
+ chat.selectTurn(turn.id);
3799
+ setHistoryOpen(false);
3800
+ },
3801
+ children: [
3802
+ /* @__PURE__ */ jsx7("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
3803
+ /* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
3804
+ turn.prompt,
3805
+ /* @__PURE__ */ jsx7("small", { children: turn.id === shown?.id ? "Now showing" : turn.completed ? "Play answer" : "Unfinished answer" })
3806
+ ] })
3807
+ ]
3638
3808
  },
3639
- children: [
3640
- /* @__PURE__ */ jsx6("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
3641
- /* @__PURE__ */ jsx6("span", { className: "prompt", children: turn.prompt })
3642
- ]
3643
- },
3644
- turn.id
3645
- )) }),
3809
+ turn.id
3810
+ )),
3811
+ savedSessions.length > 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3812
+ /* @__PURE__ */ jsx7("h3", { className: "section-label", children: "Earlier sessions" }),
3813
+ savedSessions.map((session) => /* @__PURE__ */ jsxs6("button", { type: "button", className: "history-row", onClick: () => {
3814
+ listen.stop();
3815
+ setDraft("");
3816
+ setEditing(false);
3817
+ resumeAfterInput.current = false;
3818
+ const completed = chat.turns.filter((turn) => turn.completed && turn.video);
3819
+ setSavedSessions((sessions) => [...completed.length ? [{ id: completed[0].id, turns: completed }] : [], ...sessions.filter((entry) => entry.id !== session.id)].slice(0, 10));
3820
+ restoreSession(session.turns);
3821
+ setHistoryOpen(false);
3822
+ setCaptionsExpanded(false);
3823
+ }, children: [
3824
+ /* @__PURE__ */ jsx7(Replay, {}),
3825
+ /* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
3826
+ session.turns[0]?.prompt,
3827
+ /* @__PURE__ */ jsxs6("small", { children: [
3828
+ session.turns.length,
3829
+ " ",
3830
+ session.turns.length === 1 ? "answer" : "answers"
3831
+ ] })
3832
+ ] })
3833
+ ] }, session.id))
3834
+ ] })
3835
+ ] }),
3646
3836
  settingsOpen && /* @__PURE__ */ jsxs6(
3647
3837
  "div",
3648
3838
  {
@@ -3651,97 +3841,135 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
3651
3841
  className: "sheet-popover settings",
3652
3842
  role: "dialog",
3653
3843
  "aria-label": "Settings",
3844
+ "aria-modal": "true",
3654
3845
  children: [
3655
- /* @__PURE__ */ jsxs6("fieldset", { children: [
3656
- /* @__PURE__ */ jsx6("legend", { children: "Appearance" }),
3657
- [["system", "Match system"], ["light", "Light"], ["dark", "Dark"]].map(([value, label]) => /* @__PURE__ */ jsxs6("label", { children: [
3658
- /* @__PURE__ */ jsx6("input", { type: "radio", name: appearanceName, checked: value === themeChoice, onChange: () => setThemeChoice(value) }),
3659
- /* @__PURE__ */ jsx6("span", { children: /* @__PURE__ */ jsx6("strong", { children: label }) })
3660
- ] }, value))
3846
+ /* @__PURE__ */ jsxs6("div", { className: "popover-heading", children: [
3847
+ /* @__PURE__ */ jsx7("h2", { children: "Settings" }),
3848
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "round", "aria-label": "Close settings", onClick: closeSettings, children: /* @__PURE__ */ jsx7(Close, {}) })
3849
+ ] }),
3850
+ /* @__PURE__ */ jsxs6("fieldset", { className: "playback-options", children: [
3851
+ /* @__PURE__ */ jsx7("legend", { children: "Watching" }),
3852
+ /* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
3853
+ /* @__PURE__ */ jsxs6("span", { children: [
3854
+ /* @__PURE__ */ jsx7("strong", { children: "Subtitles" }),
3855
+ /* @__PURE__ */ jsx7("small", { children: "Read along with the answer" })
3856
+ ] }),
3857
+ /* @__PURE__ */ jsx7("input", { type: "checkbox", role: "switch", checked: captionsOn, onChange: (event) => {
3858
+ setCaptionsOn(event.target.checked);
3859
+ setCaptionsExpanded(false);
3860
+ } })
3861
+ ] }),
3862
+ /* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
3863
+ /* @__PURE__ */ jsxs6("span", { children: [
3864
+ /* @__PURE__ */ jsx7("strong", { children: "Keep controls visible" }),
3865
+ /* @__PURE__ */ jsx7("small", { children: "Keep the input bar on screen" })
3866
+ ] }),
3867
+ /* @__PURE__ */ jsx7("input", { type: "checkbox", role: "switch", checked: alwaysShowControls, onChange: (event) => setAlwaysShowControls(event.target.checked) })
3868
+ ] })
3661
3869
  ] }),
3662
- /* @__PURE__ */ jsx6("p", { className: "settings-note", children: "Visuals and style apply to your next prompt." }),
3663
- /* @__PURE__ */ jsxs6("fieldset", { children: [
3664
- /* @__PURE__ */ jsx6("legend", { children: "Visuals" }),
3665
- availableModes.map((option) => /* @__PURE__ */ jsxs6("label", { children: [
3666
- /* @__PURE__ */ jsx6("input", { type: "radio", name: visualsName, checked: option.id === selectedMode.id, onChange: () => setModeId(option.id) }),
3870
+ /* @__PURE__ */ jsxs6("fieldset", { className: "visual-options", children: [
3871
+ /* @__PURE__ */ jsx7("legend", { children: "Video creation" }),
3872
+ availableModes.map((option) => /* @__PURE__ */ jsxs6("label", { className: "choice-row", children: [
3873
+ /* @__PURE__ */ jsx7("input", { type: "radio", name: visualsName, checked: option.id === selectedMode.id, onChange: () => setModeId(option.id) }),
3667
3874
  /* @__PURE__ */ jsxs6("span", { children: [
3668
- /* @__PURE__ */ jsx6("strong", { children: option.label }),
3669
- option.note
3875
+ /* @__PURE__ */ jsx7("strong", { children: option.label }),
3876
+ /* @__PURE__ */ jsx7("small", { children: option.note })
3670
3877
  ] })
3671
3878
  ] }, option.id))
3672
3879
  ] }),
3673
- /* @__PURE__ */ jsxs6("fieldset", { children: [
3674
- /* @__PURE__ */ jsx6("legend", { children: "Style" }),
3880
+ /* @__PURE__ */ jsxs6("fieldset", { className: "style-options", children: [
3881
+ /* @__PURE__ */ jsx7("legend", { children: "Video style" }),
3675
3882
  themes.map((option) => /* @__PURE__ */ jsxs6("label", { children: [
3676
- /* @__PURE__ */ jsx6("input", { type: "radio", name: styleName, checked: option.id === themeId, onChange: () => setThemeId(option.id) }),
3677
- /* @__PURE__ */ jsx6("span", { children: /* @__PURE__ */ jsx6("strong", { children: option.label }) })
3883
+ /* @__PURE__ */ jsx7("input", { type: "radio", name: styleName, checked: option.id === themeId, onChange: () => setThemeId(option.id) }),
3884
+ /* @__PURE__ */ jsx7("span", { children: /* @__PURE__ */ jsx7("strong", { children: option.label }) })
3678
3885
  ] }, option.id))
3679
- ] })
3886
+ ] }),
3887
+ /* @__PURE__ */ jsx7("p", { className: "settings-note", children: "Creation and style changes apply to your next question." })
3680
3888
  ]
3681
3889
  }
3682
3890
  )
3683
3891
  ] }),
3684
- /* @__PURE__ */ jsx6("div", { className: "panel", children: /* @__PURE__ */ jsxs6("div", { className: "panel-inner", children: [
3685
- /* @__PURE__ */ jsxs6("div", { className: "line-row", children: [
3686
- /* @__PURE__ */ jsx6("p", { className: "line", "aria-live": "polite", children: line }),
3687
- shown?.video && /* @__PURE__ */ jsxs6("button", { type: "button", className: "full-response", onClick: () => setSheetOpen(true), children: [
3688
- "Full response ",
3689
- /* @__PURE__ */ jsx6(ChevronUp, {})
3690
- ] })
3691
- ] }),
3892
+ /* @__PURE__ */ jsx7("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs6("div", { className: "panel-inner", children: [
3893
+ /* @__PURE__ */ jsx7("div", { className: "caption-slot", "data-captions": captionsOn && Boolean(line), "aria-hidden": !captionsOn || !line, children: /* @__PURE__ */ jsx7("div", { className: "caption-clip", children: /* @__PURE__ */ jsxs6(
3894
+ "div",
3895
+ {
3896
+ className: "line-row",
3897
+ "data-expanded": captionsExpanded,
3898
+ "data-actions-visible": captionControls.visible,
3899
+ onPointerMove: captionControls.onPointerEnter,
3900
+ onPointerLeave: captionControls.onPointerLeave,
3901
+ onPointerDown: captionControls.reveal,
3902
+ onFocusCapture: captionControls.onFocusCapture,
3903
+ onBlurCapture: captionControls.onBlurCapture,
3904
+ children: [
3905
+ captionsOn && line && /* @__PURE__ */ jsxs6("div", { className: "caption-actions", children: [
3906
+ /* @__PURE__ */ jsxs6("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
3907
+ captionsExpanded ? "Collapse" : "Expand",
3908
+ /* @__PURE__ */ jsx7(ChevronUp, {})
3909
+ ] }),
3910
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "caption-action", "aria-label": "Hide subtitles", onClick: () => {
3911
+ setCaptionsOn(false);
3912
+ setCaptionsExpanded(false);
3913
+ }, children: /* @__PURE__ */ jsx7(Close, {}) })
3914
+ ] }),
3915
+ captionsExpanded ? /* @__PURE__ */ jsx7("div", { className: "expanded-captions", role: "region", tabIndex: 0, "aria-label": "Expanded subtitles", children: fullTranscript.map((entry, index) => /* @__PURE__ */ jsx7("p", { children: entry }, index)) }) : /* @__PURE__ */ jsx7("p", { className: "line", "aria-live": "polite", children: line })
3916
+ ]
3917
+ }
3918
+ ) }) }),
3692
3919
  (chat.error || listen.error) && /* @__PURE__ */ jsxs6("p", { className: "error", role: "status", children: [
3693
- /* @__PURE__ */ jsx6(Warning, {}),
3694
- /* @__PURE__ */ jsx6("span", { children: chat.error?.message ?? listen.error })
3920
+ /* @__PURE__ */ jsx7(Warning, {}),
3921
+ /* @__PURE__ */ jsx7("span", { children: chat.error?.message ?? listen.error })
3695
3922
  ] }),
3696
- /* @__PURE__ */ jsxs6("form", { className: "composer", onSubmit: (event) => {
3697
- event.preventDefault();
3698
- ask(draft);
3699
- }, children: [
3700
- /* @__PURE__ */ jsx6(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
3701
- /* @__PURE__ */ jsx6("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
3702
- /* @__PURE__ */ jsx6(
3703
- "textarea",
3704
- {
3705
- id: promptId,
3706
- ref: inputRef,
3707
- rows: 1,
3708
- value: draft,
3709
- placeholder: listen.listening ? "Listening\u2026" : listen.thinking ? "Working out what you said\u2026" : status === "narrating" ? "Talking \u2014 type to jump in\u2026" : chat.turns.length > 0 ? "Ask a follow-up\u2026" : "Ask anything\u2026",
3710
- onChange: (event) => setDraft(event.target.value),
3711
- onKeyDown: (event) => {
3712
- if (event.key === "Enter" && !event.shiftKey) {
3713
- event.preventDefault();
3714
- ask(draft);
3923
+ /* @__PURE__ */ jsxs6("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
3924
+ (listen.listening || listen.thinking) && /* @__PURE__ */ jsx7("div", { className: "composer-meta", children: /* @__PURE__ */ jsx7("span", { role: "status", children: listen.listening ? "Listening\u2026 Tap the mic to finish, then review and send." : "Turning your words into a draft\u2026" }) }),
3925
+ /* @__PURE__ */ jsxs6("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
3926
+ event.preventDefault();
3927
+ ask(draft);
3928
+ }, children: [
3929
+ transport && /* @__PURE__ */ jsx7("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: () => {
3930
+ listen.stop();
3931
+ setEditing(false);
3932
+ resumeAfterInput.current = false;
3933
+ inputRef.current?.blur();
3934
+ transport.action();
3935
+ }, children: transport.icon }),
3936
+ /* @__PURE__ */ jsx7(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
3937
+ /* @__PURE__ */ jsx7("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
3938
+ /* @__PURE__ */ jsx7(
3939
+ "textarea",
3940
+ {
3941
+ id: promptId,
3942
+ ref: inputRef,
3943
+ rows: 1,
3944
+ value: draft,
3945
+ placeholder: listen.listening ? "Listening\u2026" : chat.turns.length ? "Ask a follow-up\u2026" : "Ask anything\u2026",
3946
+ onFocus: () => {
3947
+ if (!editing) beginInput();
3948
+ },
3949
+ onChange: (event) => {
3950
+ setDraft(event.target.value);
3951
+ event.target.style.height = "auto";
3952
+ event.target.style.height = `${Math.min(event.target.scrollHeight, 120)}px`;
3953
+ },
3954
+ onKeyDown: (event) => {
3955
+ if (event.key === "Escape") {
3956
+ event.stopPropagation();
3957
+ cancelInput();
3958
+ }
3959
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
3960
+ event.preventDefault();
3961
+ ask(draft);
3962
+ }
3715
3963
  }
3716
3964
  }
3717
- }
3718
- ),
3719
- transport && /* @__PURE__ */ jsx6("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: transport.action, children: transport.icon }),
3720
- listen.supported && /* @__PURE__ */ jsx6(
3721
- "button",
3722
- {
3723
- type: "button",
3724
- className: `ghost${listen.listening ? " listening" : ""}`,
3725
- "aria-label": listen.listening ? "Stop listening" : "Ask by voice",
3726
- "aria-pressed": listen.listening,
3727
- disabled: listen.thinking,
3728
- onClick: listen.toggle,
3729
- children: /* @__PURE__ */ jsx6(Mic, {})
3730
- }
3731
- ),
3732
- /* @__PURE__ */ jsx6("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || status === "drawing", children: /* @__PURE__ */ jsx6(Send, {}) })
3733
- ] })
3734
- ] }) }),
3735
- sheetOpen && shown && /* @__PURE__ */ jsxs6("div", { className: "sheet-layer", children: [
3736
- /* @__PURE__ */ jsx6("button", { type: "button", className: "scrim", tabIndex: -1, "aria-hidden": "true", onClick: () => setSheetOpen(false) }),
3737
- /* @__PURE__ */ jsxs6("article", { ref: sheetRef, className: "sheet", role: "dialog", "aria-modal": "true", "aria-labelledby": responseTitleId, children: [
3738
- /* @__PURE__ */ jsxs6("header", { children: [
3739
- /* @__PURE__ */ jsx6("h1", { id: responseTitleId, children: shown.prompt }),
3740
- /* @__PURE__ */ jsx6("button", { type: "button", className: "round", "aria-label": "Close the full response", onClick: () => setSheetOpen(false), children: /* @__PURE__ */ jsx6(Close, {}) })
3741
- ] }),
3742
- /* @__PURE__ */ jsx6("div", { className: "sheet-body", children: chat.transcript.map((entry, index) => /* @__PURE__ */ jsx6("p", { children: entry }, `${index}-${entry}`)) })
3743
- ] })
3744
- ] })
3965
+ ),
3966
+ listen.supported && /* @__PURE__ */ jsx7("button", { type: "button", className: `ghost${listen.listening ? " listening" : ""}`, "aria-label": listen.listening ? "Stop listening" : "Ask by voice", "aria-pressed": listen.listening, disabled: listen.thinking, onClick: () => beginInput(true), children: /* @__PURE__ */ jsx7(Mic, {}) }),
3967
+ /* @__PURE__ */ jsx7("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || listen.thinking, children: /* @__PURE__ */ jsx7(Send, {}) }),
3968
+ editing && /* @__PURE__ */ jsx7("button", { type: "button", className: "ghost", "aria-label": "Cancel question", onClick: cancelInput, children: /* @__PURE__ */ jsx7(Close, {}) })
3969
+ ] })
3970
+ ] }),
3971
+ !showing && chat.turns.length === 0 && /* @__PURE__ */ jsx7("p", { className: "dock-hint", children: "Speak or type. See where it takes you." })
3972
+ ] }) })
3745
3973
  ]
3746
3974
  }
3747
3975
  );