@vanillaskyai/video 0.10.18 → 0.10.19

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.10.19
8
+
9
+ - Show a subtle preparation status while the first response scene is loading.
10
+ - Use the same compact suggestion-card layout before and after answers, and link the logo to the homepage.
11
+ - Plan relevant illustrative stock subjects per beat and prefer, rather than require, matching footage orientation in the starter.
12
+
7
13
  ## 0.10.18
8
14
 
9
15
  - Preserve later valid shots after a malformed body record containing an unescaped newline, while keeping malformed initial containers rejected. Bound unfinished JSON records independently of provider chunk sizes.
package/dist/react.js CHANGED
@@ -3047,35 +3047,46 @@ function Welcome({ data, onAsk, title }) {
3047
3047
 
3048
3048
  // src/video-chat/opening-chapter.tsx
3049
3049
  import { useLayoutEffect, useRef as useRef5, useState as useState4 } from "react";
3050
- import { jsx as jsx6 } from "react/jsx-runtime";
3051
- function OpeningChapter({ title }) {
3050
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3051
+ function OpeningChapter({ title, preparing = false }) {
3052
3052
  const root = useRef5(null);
3053
3053
  const [size, setSize] = useState4({ width: 1080, height: 1080 });
3054
+ const [titleBottom, setTitleBottom] = useState4(0);
3054
3055
  useLayoutEffect(() => {
3055
3056
  const element = root.current;
3056
3057
  if (!element) return;
3057
3058
  const measure = () => {
3058
3059
  const { width, height } = element.getBoundingClientRect();
3059
3060
  if (width > 0 && height > 0) setSize({ width, height });
3061
+ const title3 = element.querySelector('[data-title-composition="centered"]');
3062
+ if (title3) setTitleBottom(title3.getBoundingClientRect().bottom - element.getBoundingClientRect().top);
3060
3063
  };
3061
3064
  measure();
3062
3065
  if (typeof ResizeObserver === "undefined") return;
3063
3066
  const observer = new ResizeObserver(measure);
3064
3067
  observer.observe(element);
3068
+ const title2 = element.querySelector('[data-title-composition="centered"]');
3069
+ if (title2) observer.observe(title2);
3065
3070
  return () => observer.disconnect();
3066
- }, []);
3067
- return /* @__PURE__ */ jsx6("div", { ref: root, "data-opening-chapter": true, className: "opening-chapter", children: /* @__PURE__ */ jsx6(
3068
- TitleSceneTemplate,
3069
- {
3070
- variables: { title },
3071
- style: {},
3072
- width: size.width,
3073
- height: size.height,
3074
- progress: 0.3,
3075
- beatIntensity: 0,
3076
- safeZone: { top: 0, right: 0, bottom: 0, left: 0 }
3077
- }
3078
- ) });
3071
+ }, [title]);
3072
+ return /* @__PURE__ */ jsxs6("div", { ref: root, "data-opening-chapter": true, className: "opening-chapter", children: [
3073
+ /* @__PURE__ */ jsx6(
3074
+ TitleSceneTemplate,
3075
+ {
3076
+ variables: { title },
3077
+ style: {},
3078
+ width: size.width,
3079
+ height: size.height,
3080
+ progress: 0.3,
3081
+ beatIntensity: 0,
3082
+ safeZone: { top: 0, right: 0, bottom: 0, left: 0 }
3083
+ }
3084
+ ),
3085
+ preparing && /* @__PURE__ */ jsxs6("div", { className: "video-preparation", style: { top: titleBottom + 24 }, role: "status", "aria-label": "Video preparation", children: [
3086
+ /* @__PURE__ */ jsx6("span", { className: "video-preparation-spinner", "aria-hidden": "true" }),
3087
+ /* @__PURE__ */ jsx6("span", { className: "video-preparation-text", children: "Preparing your video\u2026" })
3088
+ ] })
3089
+ ] });
3079
3090
  }
3080
3091
 
3081
3092
  // src/video-chat/use-voice-input.ts
@@ -3353,7 +3364,7 @@ var visualModes = [
3353
3364
  var defaultMode = visualModes[0];
3354
3365
 
3355
3366
  // src/video-chat/video-chat.tsx
3356
- import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
3367
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3357
3368
  var DESKTOP_WIDTH = 900;
3358
3369
  function useViewportOrientation() {
3359
3370
  const [portrait, setPortrait] = useState7(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`).matches : false);
@@ -3457,6 +3468,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3457
3468
  const handoffStopped = chat.status === "error" || chat.status === "cancelled";
3458
3469
  const waitingForBody = showing && presentedBody !== handoffKey;
3459
3470
  const openingChapter = Boolean(shown?.prompt) && (!showing || waitingForBody) && chat.status !== "error" && chat.status !== "cancelled" && chat.status !== "ended";
3471
+ const [preparingKey, setPreparingKey] = useState7();
3472
+ useEffect9(() => {
3473
+ setPreparingKey(void 0);
3474
+ if (!openingChapter || !shown?.opening || chat.speaking) return;
3475
+ const timer = setTimeout(() => setPreparingKey(shown.id), 1e3);
3476
+ return () => clearTimeout(timer);
3477
+ }, [openingChapter, shown?.opening, shown?.id, chat.speaking]);
3460
3478
  useLayoutEffect2(() => {
3461
3479
  const current = { key: handoffKey, live: showing && !handoffStopped, active: openingChapter && showing, frame: 0 };
3462
3480
  handoff.current = current;
@@ -3533,7 +3551,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3533
3551
  observer.observe(composer);
3534
3552
  return () => observer.disconnect();
3535
3553
  }, []);
3536
- return /* @__PURE__ */ jsxs6(
3554
+ return /* @__PURE__ */ jsxs7(
3537
3555
  "div",
3538
3556
  {
3539
3557
  className: `vanillasky-video-chat${className ? ` ${className}` : ""}`,
@@ -3545,13 +3563,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3545
3563
  onPointerDown: controls.reveal,
3546
3564
  onKeyDownCapture: controls.reveal,
3547
3565
  children: [
3548
- /* @__PURE__ */ jsxs6("header", { className: "chrome", ...controlEvents, children: [
3549
- /* @__PURE__ */ jsxs6("div", { className: "session-brand", children: [
3550
- /* @__PURE__ */ jsx8(Logo, {}),
3551
- (shown?.mode ?? selectedMode ?? options.mode) === "pexels" && /* @__PURE__ */ jsx8("a", { className: "media-credit", href: "https://www.pexels.com", target: "_blank", rel: "noopener noreferrer", children: "Videos by Pexels" })
3566
+ /* @__PURE__ */ jsxs7("header", { className: "chrome", ...controlEvents, children: [
3567
+ /* @__PURE__ */ jsxs7("div", { className: "session-brand", children: [
3568
+ /* @__PURE__ */ jsx8("a", { className: "home-link", href: "/", "aria-label": "Home", children: /* @__PURE__ */ jsx8(Logo, {}) }),
3569
+ (shown?.mode ?? selectedMode ?? options.mode) === "pexels" && /* @__PURE__ */ jsx8("a", { className: "media-credit", href: "https://www.pexels.com", target: "_blank", rel: "noopener noreferrer", children: "Pexels" })
3552
3570
  ] }),
3553
- /* @__PURE__ */ jsxs6("div", { className: "group", children: [
3554
- /* @__PURE__ */ jsxs6(
3571
+ /* @__PURE__ */ jsxs7("div", { className: "group", children: [
3572
+ /* @__PURE__ */ jsxs7(
3555
3573
  "button",
3556
3574
  {
3557
3575
  ref: historyButtonRef,
@@ -3601,9 +3619,9 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3601
3619
  )
3602
3620
  ] })
3603
3621
  ] }),
3604
- /* @__PURE__ */ jsxs6("div", { className: "stage-area", children: [
3605
- /* @__PURE__ */ jsxs6("div", { className: "stage", style: { background: "#000" }, children: [
3606
- openingChapter && /* @__PURE__ */ jsx8(OpeningChapter, { title: openingTitle.length > 120 ? `${openingTitle.slice(0, 117).trimEnd()}\u2026` : openingTitle }, shown.id),
3622
+ /* @__PURE__ */ jsxs7("div", { className: "stage-area", children: [
3623
+ /* @__PURE__ */ jsxs7("div", { className: "stage", style: { background: "#000" }, children: [
3624
+ openingChapter && /* @__PURE__ */ jsx8(OpeningChapter, { preparing: preparingKey === shown.id && !chat.speaking, title: openingTitle.length > 120 ? `${openingTitle.slice(0, 117).trimEnd()}\u2026` : openingTitle }, shown.id),
3607
3625
  !showing && chat.turns.length === 0 && /* @__PURE__ */ jsx8(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
3608
3626
  chat.playerProps && /* @__PURE__ */ jsx8("div", { className: "player-fit", style: { width: stageOrientation === "portrait" ? "min(100cqw, 56.25cqh)" : "min(100cqw, 177.7778cqh)" }, children: /* @__PURE__ */ jsx8(
3609
3627
  VideoPlayer,
@@ -3617,29 +3635,29 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3617
3635
  },
3618
3636
  chat.playerKey
3619
3637
  ) }),
3620
- showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "ending", children: [
3638
+ showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "ending", children: [
3621
3639
  /* @__PURE__ */ jsx8("div", { className: "ending-wash", "aria-hidden": "true" }),
3622
- /* @__PURE__ */ jsxs6("div", { className: "ending-body", children: [
3640
+ /* @__PURE__ */ jsxs7("div", { className: "ending-body", children: [
3623
3641
  /* @__PURE__ */ jsx8("p", { className: "ending-label", children: "Ask next" }),
3624
3642
  /* @__PURE__ */ jsx8(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
3625
3643
  ] })
3626
3644
  ] })
3627
3645
  ] }),
3628
- historyOpen && /* @__PURE__ */ jsxs6("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "Sessions", children: [
3629
- /* @__PURE__ */ jsxs6("div", { className: "popover-heading", children: [
3646
+ historyOpen && /* @__PURE__ */ jsxs7("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "Sessions", children: [
3647
+ /* @__PURE__ */ jsxs7("div", { className: "popover-heading", children: [
3630
3648
  /* @__PURE__ */ jsx8("h2", { children: "Sessions" }),
3631
3649
  /* @__PURE__ */ jsx8("button", { type: "button", className: "round", "aria-label": "Close sessions", onClick: closeHistory, children: /* @__PURE__ */ jsx8(Close, {}) })
3632
3650
  ] }),
3633
- /* @__PURE__ */ jsxs6("button", { type: "button", className: "history-row session-new", "aria-label": "New session", onClick: newSession, children: [
3651
+ /* @__PURE__ */ jsxs7("button", { type: "button", className: "history-row session-new", "aria-label": "New session", onClick: newSession, children: [
3634
3652
  /* @__PURE__ */ jsx8(Plus, {}),
3635
- /* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
3653
+ /* @__PURE__ */ jsxs7("span", { className: "prompt", children: [
3636
3654
  "New session",
3637
3655
  /* @__PURE__ */ jsx8("small", { children: "Start a fresh conversation" })
3638
3656
  ] })
3639
3657
  ] }),
3640
3658
  /* @__PURE__ */ jsx8("h3", { className: "section-label", children: "Current session" }),
3641
3659
  chat.turns.length === 0 && /* @__PURE__ */ jsx8("p", { className: "history-empty", children: "Your questions will appear here." }),
3642
- chat.turns.map((turn, index) => /* @__PURE__ */ jsxs6(
3660
+ chat.turns.map((turn, index) => /* @__PURE__ */ jsxs7(
3643
3661
  "button",
3644
3662
  {
3645
3663
  type: "button",
@@ -3656,7 +3674,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3656
3674
  },
3657
3675
  children: [
3658
3676
  /* @__PURE__ */ jsx8("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
3659
- /* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
3677
+ /* @__PURE__ */ jsxs7("span", { className: "prompt", children: [
3660
3678
  turn.prompt,
3661
3679
  /* @__PURE__ */ jsx8("small", { children: turn.id === shown?.id ? "Now showing" : turn.completed ? "Play answer" : "Unfinished answer" })
3662
3680
  ] })
@@ -3664,9 +3682,9 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3664
3682
  },
3665
3683
  turn.id
3666
3684
  )),
3667
- savedSessions.length > 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3685
+ savedSessions.length > 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
3668
3686
  /* @__PURE__ */ jsx8("h3", { className: "section-label", children: "Earlier sessions" }),
3669
- savedSessions.map((session) => /* @__PURE__ */ jsxs6("button", { type: "button", className: "history-row", onClick: () => {
3687
+ savedSessions.map((session) => /* @__PURE__ */ jsxs7("button", { type: "button", className: "history-row", onClick: () => {
3670
3688
  listen.stop();
3671
3689
  setDraft("");
3672
3690
  setEditing(false);
@@ -3678,9 +3696,9 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3678
3696
  setCaptionsExpanded(false);
3679
3697
  }, children: [
3680
3698
  /* @__PURE__ */ jsx8(Replay, {}),
3681
- /* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
3699
+ /* @__PURE__ */ jsxs7("span", { className: "prompt", children: [
3682
3700
  session.turns[0]?.prompt,
3683
- /* @__PURE__ */ jsxs6("small", { children: [
3701
+ /* @__PURE__ */ jsxs7("small", { children: [
3684
3702
  session.turns.length,
3685
3703
  " ",
3686
3704
  session.turns.length === 1 ? "answer" : "answers"
@@ -3689,7 +3707,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3689
3707
  ] }, session.id))
3690
3708
  ] })
3691
3709
  ] }),
3692
- settingsOpen && /* @__PURE__ */ jsxs6(
3710
+ settingsOpen && /* @__PURE__ */ jsxs7(
3693
3711
  "div",
3694
3712
  {
3695
3713
  ref: settingsRef,
@@ -3699,14 +3717,14 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3699
3717
  "aria-label": "Settings",
3700
3718
  "aria-modal": "true",
3701
3719
  children: [
3702
- /* @__PURE__ */ jsxs6("div", { className: "popover-heading", children: [
3720
+ /* @__PURE__ */ jsxs7("div", { className: "popover-heading", children: [
3703
3721
  /* @__PURE__ */ jsx8("h2", { children: "Settings" }),
3704
3722
  /* @__PURE__ */ jsx8("button", { type: "button", className: "round", "aria-label": "Close settings", onClick: closeSettings, children: /* @__PURE__ */ jsx8(Close, {}) })
3705
3723
  ] }),
3706
- /* @__PURE__ */ jsxs6("fieldset", { className: "playback-options", children: [
3724
+ /* @__PURE__ */ jsxs7("fieldset", { className: "playback-options", children: [
3707
3725
  /* @__PURE__ */ jsx8("legend", { children: "Video source" }),
3708
- visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
3709
- /* @__PURE__ */ jsxs6("span", { children: [
3726
+ visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */ jsxs7("label", { className: "switch-row", children: [
3727
+ /* @__PURE__ */ jsxs7("span", { children: [
3710
3728
  /* @__PURE__ */ jsx8("strong", { children: mode.label }),
3711
3729
  /* @__PURE__ */ jsx8("small", { children: mode.note })
3712
3730
  ] }),
@@ -3722,10 +3740,10 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3722
3740
  )
3723
3741
  ] }, mode.id))
3724
3742
  ] }),
3725
- /* @__PURE__ */ jsxs6("fieldset", { className: "playback-options", children: [
3743
+ /* @__PURE__ */ jsxs7("fieldset", { className: "playback-options", children: [
3726
3744
  /* @__PURE__ */ jsx8("legend", { children: "Watching" }),
3727
- /* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
3728
- /* @__PURE__ */ jsxs6("span", { children: [
3745
+ /* @__PURE__ */ jsxs7("label", { className: "switch-row", children: [
3746
+ /* @__PURE__ */ jsxs7("span", { children: [
3729
3747
  /* @__PURE__ */ jsx8("strong", { children: "Subtitles" }),
3730
3748
  /* @__PURE__ */ jsx8("small", { children: "Read along with the answer" })
3731
3749
  ] }),
@@ -3734,26 +3752,26 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3734
3752
  setCaptionsExpanded(false);
3735
3753
  } })
3736
3754
  ] }),
3737
- /* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
3738
- /* @__PURE__ */ jsxs6("span", { children: [
3755
+ /* @__PURE__ */ jsxs7("label", { className: "switch-row", children: [
3756
+ /* @__PURE__ */ jsxs7("span", { children: [
3739
3757
  /* @__PURE__ */ jsx8("strong", { children: "Keep controls visible" }),
3740
3758
  /* @__PURE__ */ jsx8("small", { children: "Keep the input bar on screen" })
3741
3759
  ] }),
3742
3760
  /* @__PURE__ */ jsx8("input", { type: "checkbox", role: "switch", checked: alwaysShowControls, onChange: (event) => setAlwaysShowControls(event.target.checked) })
3743
3761
  ] })
3744
3762
  ] }),
3745
- /* @__PURE__ */ jsxs6("nav", { className: "developer-links", "aria-label": "Build with VanillaSky", children: [
3763
+ /* @__PURE__ */ jsxs7("nav", { className: "developer-links", "aria-label": "Build with VanillaSky", children: [
3746
3764
  /* @__PURE__ */ jsx8("p", { className: "section-label", children: "Build with VanillaSky" }),
3747
- /* @__PURE__ */ jsxs6("a", { href: "https://github.com/VanillaSkyAi/video/blob/main/docs/getting-started.md", target: "_blank", rel: "noopener noreferrer", children: [
3765
+ /* @__PURE__ */ jsxs7("a", { href: "https://github.com/VanillaSkyAi/video/blob/main/docs/getting-started.md", target: "_blank", rel: "noopener noreferrer", children: [
3748
3766
  "Docs",
3749
3767
  /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\u2197" })
3750
3768
  ] }),
3751
- /* @__PURE__ */ jsxs6("button", { type: "button", "aria-expanded": aboutOpen, "aria-controls": `${instanceId}-about`, onClick: () => setAboutOpen((open) => !open), children: [
3769
+ /* @__PURE__ */ jsxs7("button", { type: "button", "aria-expanded": aboutOpen, "aria-controls": `${instanceId}-about`, onClick: () => setAboutOpen((open) => !open), children: [
3752
3770
  "About",
3753
3771
  /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: aboutOpen ? "\u2212" : "+" })
3754
3772
  ] }),
3755
3773
  /* @__PURE__ */ jsx8("div", { id: `${instanceId}-about`, className: "developer-about", role: "region", "aria-label": "About VanillaSky", hidden: !aboutOpen, children: /* @__PURE__ */ jsx8("p", { children: "VanillaSky is an open-source SDK for conversations that answer in video. Developers connect their own AI providers through their application server." }) }),
3756
- /* @__PURE__ */ jsxs6("a", { href: "https://github.com/VanillaSkyAi/video", target: "_blank", rel: "noopener noreferrer", children: [
3774
+ /* @__PURE__ */ jsxs7("a", { href: "https://github.com/VanillaSkyAi/video", target: "_blank", rel: "noopener noreferrer", children: [
3757
3775
  "GitHub",
3758
3776
  /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\u2197" })
3759
3777
  ] })
@@ -3762,8 +3780,8 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3762
3780
  }
3763
3781
  )
3764
3782
  ] }),
3765
- /* @__PURE__ */ jsx8("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs6("div", { className: "panel-inner", children: [
3766
- /* @__PURE__ */ jsx8("div", { className: "caption-slot", "data-captions": captionsOn && Boolean(line), "aria-hidden": !captionsOn || !line, children: /* @__PURE__ */ jsx8("div", { className: "caption-clip", children: /* @__PURE__ */ jsxs6(
3783
+ /* @__PURE__ */ jsx8("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs7("div", { className: "panel-inner", children: [
3784
+ /* @__PURE__ */ jsx8("div", { className: "caption-slot", "data-captions": captionsOn && Boolean(line), "aria-hidden": !captionsOn || !line, children: /* @__PURE__ */ jsx8("div", { className: "caption-clip", children: /* @__PURE__ */ jsxs7(
3767
3785
  "div",
3768
3786
  {
3769
3787
  className: "line-row",
@@ -3776,8 +3794,8 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3776
3794
  onFocusCapture: captionControls.onFocusCapture,
3777
3795
  onBlurCapture: captionControls.onBlurCapture,
3778
3796
  children: [
3779
- captionsOn && line && /* @__PURE__ */ jsxs6("div", { className: "caption-actions", children: [
3780
- /* @__PURE__ */ jsxs6("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
3797
+ captionsOn && line && /* @__PURE__ */ jsxs7("div", { className: "caption-actions", children: [
3798
+ /* @__PURE__ */ jsxs7("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
3781
3799
  captionsExpanded ? "Collapse" : "Expand",
3782
3800
  /* @__PURE__ */ jsx8(ChevronUp, {})
3783
3801
  ] }),
@@ -3790,17 +3808,17 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
3790
3808
  ]
3791
3809
  }
3792
3810
  ) }) }),
3793
- showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */ jsxs6("div", { className: "recovery-notice", children: [
3811
+ showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */ jsxs7("div", { className: "recovery-notice", children: [
3794
3812
  /* @__PURE__ */ jsx8("p", { role: "status", children: MEDIA_RECOVERY_NOTICE }),
3795
3813
  /* @__PURE__ */ jsx8("button", { type: "button", className: "round", "aria-label": "Dismiss notice", onClick: () => setDismissedNoticeTurn(chat.shownTurn?.id), children: /* @__PURE__ */ jsx8(Close, {}) })
3796
3814
  ] }),
3797
- (chat.error || listen.error) && /* @__PURE__ */ jsxs6("p", { className: "error", role: "status", children: [
3815
+ (chat.error || listen.error) && /* @__PURE__ */ jsxs7("p", { className: "error", role: "status", children: [
3798
3816
  /* @__PURE__ */ jsx8(Warning, {}),
3799
3817
  /* @__PURE__ */ jsx8("span", { children: chat.error?.message ?? listen.error })
3800
3818
  ] }),
3801
- /* @__PURE__ */ jsxs6("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
3819
+ /* @__PURE__ */ jsxs7("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
3802
3820
  (listen.listening || listen.thinking) && /* @__PURE__ */ jsx8("div", { className: "composer-meta", children: /* @__PURE__ */ jsx8("span", { role: "status", children: listen.listening ? "Listening\u2026 Tap the mic to finish, then review and send." : "Turning your words into a draft\u2026" }) }),
3803
- /* @__PURE__ */ jsxs6("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
3821
+ /* @__PURE__ */ jsxs7("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
3804
3822
  event.preventDefault();
3805
3823
  ask(draft);
3806
3824
  }, children: [
package/dist/server.js CHANGED
@@ -1235,6 +1235,7 @@ function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlr
1235
1235
  `The selected footage mode is ${mode === "pexels" ? "Pexels stock search: use literal filmable subjects; never imply stock proves a mechanism or depicts fictional events exactly" : `AI video, with at most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generation attempts`}. Missing footage becomes the authored chapter title, with complete narration. Never truncate already-authored narration when footage fails. The host selects providers; do not make source choices.`,
1236
1236
  ...mode === "cinematic" ? [generatedVideoAvailable && maxGeneratedVideos > 0 ? `Plan at most ${maxGeneratedVideos} generated-video beats in total, including the saved ending. Use at most ${maxGeneratedVideos - 1} developing shot records; the ending uses the remaining beat. The opening chapter does not consume a generated clip. Before writing, choose a concise, complete treatment that fits this budget: combine related ideas, preserve essential facts and qualifiers, and finish the requested answer. Do not plan an extra chapter tail simply because generation attempts will run out.${maxGeneratedVideos === 1 ? " Put the complete answer in the saved ending, set development to an empty string, and emit no developing shot records." : ""}` : "No generated-video attempts are available; plan a complete chapter-led answer with a useful ending. Do not omit the answer to satisfy a zero clip budget."] : [],
1237
1237
  ...mode === "pexels" ? ["Stock queries must retain the essential subject, activity and distinguishing equipment in the shot's subject field, within its word limit. That field alone is the search query; action and visualDirection do not refine it. Prefer common observable actions with usable framing. Do not replace the required actor or activity with scenery, a different sport or a loosely related setting. Preserve fictional or comic narration, but do not depend on stock showing an exact invented expression or sequence; choose an illustrative action that supports the beat."] : [],
1238
+ ...mode === "pexels" ? ["Choose a separate stock subject for each beat, describing footage that can realistically exist in a stock library. For historical, abstract or unseen events, use relevant present-day evidence, objects, environments or analogous visible processes as clearly illustrative support. Do not require literal footage of events or subjects that cannot realistically be filmed. Keep the causal explanation in narration; do not claim illustrative footage records the historical event or proves the mechanism. Make stockSelection describe the chosen visible subject, not the overall topic. Do not use illustrative freedom to replace a required practical action, person, sport or distinguishing equipment with unrelated scenery."] : [],
1238
1239
  ...mode === "pexels" ? ['Include stockSelection on every shot and the saved ending when the essential subject is known: "stockSelection":{"subject":"essential actor or object category","activity":"optional literal activity","equipment":"optional distinguishing equipment","exclude":["optional contradictory subject or activity"]}. Each phrase must be 1\u20134 words and at most 48 characters; exclude has at most 3 phrases. The essential subject is separate from the setting: do not use scenery, mood, camera framing or incidental appearance as the actor. Keep the search query broad enough to find footage; the optional hint helps select results without substituting a different actor or task. Use exclusions only for actual contradictions, not every detail absent from the story. Omit unknown fields or the whole hint rather than inventing an anchor. This is selection guidance, not verification that footage depicts the exact narration.'] : [],
1239
1240
  "Keep development to one concise sentence and visualDirection to the few details needed for consistency. Emit the complete brief, then the first developing shot immediately when developing shots are needed and allowed by the budget; otherwise end after the brief. Continue the same stream without an outline, recap or second planning pass. The saved ending must still contain the complete payoff before the brief is emitted.",
1240
1241
  "For a very short answer whose ending alone fulfills the request, development may be empty and no developing shots are needed. Otherwise, develop the essential content before the ending.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillaskyai/video",
3
- "version": "0.10.18",
3
+ "version": "0.10.19",
4
4
  "description": "Open-source voice-and-video chat SDK for AI applications.",
5
5
  "keywords": [
6
6
  "video-chat",
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@vanillaskyai/video": "0.10.18",
12
+ "@vanillaskyai/video": "0.10.19",
13
13
  "react": "^19.2.8",
14
14
  "react-dom": "^19.2.8",
15
15
  "@ai-sdk/anthropic": "^3.0.0",
@@ -53,19 +53,21 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
53
53
  const normalized = query.trim().toLowerCase().replace(/\s+/g, " ");
54
54
  const tokens = words(normalized);
55
55
  const selection = selectionHint(rawSelection);
56
- const key = JSON.stringify({version: 3, orientation, query: normalized, selection});
56
+ const key = JSON.stringify({version: 4, orientation, query: normalized, selection});
57
57
  const apiKey = process.env.PEXELS_API_KEY;
58
58
  if (!apiKey || !tokens.length || normalized.length > 80 || tokens.length > 8) return null;
59
59
  const existing = cache.get(key);
60
60
  if (existing && existing.expires > Date.now()) return existing.media;
61
61
  const url = new URL("https://api.pexels.com/v1/videos/search");
62
- url.search = new URLSearchParams({ query: normalized, orientation, per_page: "12", size: "medium" }).toString();
62
+ url.search = new URLSearchParams({ query: normalized, per_page: "12", size: "medium" }).toString();
63
63
  const response = await fetch(url, { headers: { Authorization: apiKey }, signal });
64
64
  signal.throwIfAborted();
65
65
  if (!response.ok) return null;
66
66
  const result = await response.json() as { videos?: PexelsVideo[] };
67
67
  signal.throwIfAborted();
68
- let selected: StockVideo | null = null, bestScore = -1;
68
+ let selected: StockVideo | null = null, bestScore = -1, bestOrientation = -1;
69
+ const matchesOrientation = (file: {width?: number; height?: number}) => orientation === "portrait"
70
+ ? file.height! > file.width! : file.width! >= file.height!;
69
71
  for (const video of (Array.isArray(result.videos) ? result.videos : []).slice(0, 12)) {
70
72
  if (!pexelsUrl(video.url)) continue;
71
73
  const slug = new URL(video.url).pathname.replace(/^\/video\//, "");
@@ -88,12 +90,15 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
88
90
  const files = (Array.isArray(video.video_files) ? video.video_files : []).filter(file =>
89
91
  file.file_type === "video/mp4" && pexelsUrl(file.link)
90
92
  && Number.isFinite(file.width) && Number.isFinite(file.height)
91
- && Math.min(file.width!, file.height!) >= 360
92
- && (orientation === "portrait" ? file.height! > file.width! : file.width! >= file.height!),
93
- ).sort((a, b) => Math.abs(Math.max(a.width!, a.height!) - 1280) - Math.abs(Math.max(b.width!, b.height!) - 1280));
93
+ && Math.min(file.width!, file.height!) >= 360,
94
+ ).sort((a, b) => Number(matchesOrientation(b)) - Number(matchesOrientation(a)) || Math.abs(Math.max(a.width!, a.height!) - 1280) - Math.abs(Math.max(b.width!, b.height!) - 1280));
94
95
  const file = files[0];
95
- if (!file || matches <= bestScore) continue;
96
+ if (!file) continue;
97
+ const orientationScore = Number(matchesOrientation(file));
98
+ // Prefer composition fit only when subject relevance is equal.
99
+ if (matches < bestScore || (matches === bestScore && orientationScore <= bestOrientation)) continue;
96
100
  bestScore = matches;
101
+ bestOrientation = orientationScore;
97
102
  selected = { url: file.link!, type: "video", ...(pexelsUrl(video.image) ? { posterUrl: video.image } : {}) };
98
103
  }
99
104
  // Bounded process-local cache; no request signal or credentials are retained.
@@ -370,16 +370,6 @@
370
370
  text-shadow: 0 1px 3px oklch(0.14 0.05 265 / 70%);
371
371
  }
372
372
 
373
- .vanillasky-video-chat .ending .cards {
374
- margin-top: 0;
375
- display: grid;
376
- grid-template-columns: repeat(2, minmax(0, 1fr));
377
- flex-shrink: 0;
378
- overflow: visible;
379
- }
380
-
381
- .vanillasky-video-chat .ending .card-dots { align-self: flex-start; margin-left: -0.35rem; }
382
-
383
373
  .vanillasky-video-chat .panel { display: grid; align-items: center; min-height: 0; background: var(--vs-bg); }
384
374
 
385
375
  .vanillasky-video-chat .panel-inner {
@@ -620,7 +610,7 @@
620
610
 
621
611
  .vanillasky-video-chat .cards { gap: 14px; margin: auto -12px 0; width: calc(100% + 24px); max-width: calc(100% + 24px); padding: 12px; }
622
612
 
623
- .vanillasky-video-chat .cards button { width: clamp(160px, 18vw, 260px); aspect-ratio: 1.65; border-radius: 14px; box-shadow: 0 8px 30px rgb(0 0 0 / 24%); background: #202438; }
613
+ .vanillasky-video-chat .cards button { --vs-card-width: clamp(160px, 18vw, 260px); --vs-card-ratio: 1.65; width: var(--vs-card-width); aspect-ratio: 1.65; border-radius: 14px; box-shadow: 0 8px 30px rgb(0 0 0 / 24%); background: #202438; }
624
614
 
625
615
  .vanillasky-video-chat .card-prompt { font-size: clamp(13px, 1.1vw, 16px); padding: 14px 16px; letter-spacing: -.015em; font-weight: 550; }
626
616
 
@@ -642,7 +632,7 @@
642
632
  .vanillasky-video-chat .welcome-body { padding: max(135px, 17dvh) 20px 118px; gap: 24px; }
643
633
  .vanillasky-video-chat .welcome-title { font-size: clamp(36px, 9.6vw, 58px); line-height: 1.12; letter-spacing: -.055em; }
644
634
  .vanillasky-video-chat .cards { gap: 12px; width: calc(100% + 20px); max-width: calc(100% + 20px); padding: 12px 20px 12px 12px; }
645
- .vanillasky-video-chat .cards button { width: 172px; aspect-ratio: .95; }
635
+ .vanillasky-video-chat .cards button { --vs-card-width: 172px; --vs-card-ratio: .95; width: var(--vs-card-width); aspect-ratio: .95; }
646
636
  .vanillasky-video-chat .card-prompt { font-size: 14px; padding: 14px; }
647
637
 
648
638
  }
@@ -650,13 +640,13 @@
650
640
  @media (max-height: 650px) and (min-width: 701px) {
651
641
  .vanillasky-video-chat .welcome-body { padding-top: 110px; padding-bottom: 118px; }
652
642
  .vanillasky-video-chat .welcome-title { font-size: clamp(32px, 4vw, 54px); }
653
- .vanillasky-video-chat .cards button { width: 160px; }
643
+ .vanillasky-video-chat .cards button { --vs-card-width: 160px; width: var(--vs-card-width); }
654
644
  }
655
645
 
656
646
  @media (max-height: 650px) and (max-width: 700px) {
657
647
  .vanillasky-video-chat .welcome-body { padding-top: 98px; }
658
648
  .vanillasky-video-chat .welcome-title { font-size: 34px; }
659
- .vanillasky-video-chat .cards button { width: 145px; aspect-ratio: 1.5; }
649
+ .vanillasky-video-chat .cards button { --vs-card-width: 145px; --vs-card-ratio: 1.5; width: var(--vs-card-width); aspect-ratio: 1.5; }
660
650
  }
661
651
 
662
652
  @media (prefers-reduced-motion: reduce) {
@@ -1079,31 +1069,58 @@
1079
1069
  @keyframes vanillasky-chapter-enter { from { opacity: 0; } to { opacity: 1; } }
1080
1070
  @media (prefers-reduced-motion: reduce) { .vanillasky-video-chat .opening-chapter { animation: none; } }
1081
1071
 
1082
- .vanillasky-video-chat .ending .cards li { min-width: 0; }
1083
-
1084
- .vanillasky-video-chat .ending .cards button {
1085
- display: flex;
1086
- flex-direction: column;
1087
- justify-content: flex-end;
1088
- width: 100%;
1089
- height: 100%;
1090
- min-height: 180px;
1091
- aspect-ratio: auto;
1072
+ @media (max-height: 650px) {
1073
+ .vanillasky-video-chat .ending-body { inset-block: 80px 180px; }
1092
1074
  }
1093
1075
 
1094
- .vanillasky-video-chat .ending .card-prompt {
1095
- position: relative;
1096
- inset: auto;
1097
- overflow-wrap: anywhere;
1098
- font-size: clamp(14px, 1.2vw, 18px);
1076
+ .vanillasky-video-chat .video-preparation {
1077
+ position: absolute;
1078
+ inset-inline: 0;
1079
+ z-index: 3;
1080
+ display: flex;
1081
+ align-items: center;
1082
+ justify-content: center;
1083
+ gap: 10px;
1084
+ pointer-events: none;
1085
+ color: rgb(255 255 255 / 62%);
1086
+ font-size: 17px;
1099
1087
  line-height: 1.4;
1100
1088
  }
1101
-
1102
- @container (min-width: 1200px) {
1103
- .vanillasky-video-chat .ending .cards { grid-template-columns: repeat(4, minmax(0, 1fr)); }
1104
- .vanillasky-video-chat .ending .cards button { min-height: 210px; }
1089
+ .vanillasky-video-chat .video-preparation-spinner {
1090
+ width: 16px;
1091
+ height: 16px;
1092
+ border: 1.5px solid rgb(255 255 255 / 18%);
1093
+ border-top-color: rgb(255 255 255 / 75%);
1094
+ border-radius: 50%;
1095
+ animation: vanillasky-video-preparation 1s linear infinite;
1096
+ }
1097
+ @keyframes vanillasky-video-preparation { to { transform: rotate(360deg); } }
1098
+ @media (prefers-reduced-motion: reduce) {
1099
+ .vanillasky-video-chat .video-preparation-spinner { animation: none; }
1105
1100
  }
1106
1101
 
1107
- @media (max-height: 650px) {
1108
- .vanillasky-video-chat .ending-body { inset-block: 80px 180px; }
1102
+ .vanillasky-video-chat .video-preparation-text {
1103
+ color: transparent;
1104
+ background: linear-gradient(105deg, rgb(255 255 255 / 55%) 35%, #fff 50%, rgb(255 255 255 / 55%) 65%);
1105
+ background-size: 250% 100%;
1106
+ background-clip: text;
1107
+ -webkit-background-clip: text;
1108
+ animation: vanillasky-preparation-shimmer 2.4s ease-in-out infinite;
1109
+ }
1110
+ @keyframes vanillasky-preparation-shimmer {
1111
+ from { background-position: 100% 0; }
1112
+ to { background-position: 0% 0; }
1113
+ }
1114
+ @media (prefers-reduced-motion: reduce) {
1115
+ .vanillasky-video-chat .video-preparation-text { animation: none; background: none; color: inherit; }
1109
1116
  }
1117
+ @media (forced-colors: active) {
1118
+ .vanillasky-video-chat .video-preparation-text { animation: none; background: none; color: CanvasText; }
1119
+ }
1120
+
1121
+ .vanillasky-video-chat .home-link { display: inline-flex; border-radius: 4px; }
1122
+ .vanillasky-video-chat .home-link:focus-visible { outline: 2px solid currentColor; outline-offset: 6px; }
1123
+ .vanillasky-video-chat .cards button { display: flex; flex-direction: column; justify-content: flex-end; aspect-ratio: auto; min-height: calc(var(--vs-card-width) / var(--vs-card-ratio)); }
1124
+ .vanillasky-video-chat .card-prompt { position: relative; inset: auto; overflow-wrap: anywhere; flex-shrink: 0; }
1125
+
1126
+ .vanillasky-video-chat .cards { flex-shrink: 0; }