@vanillaskyai/video 0.10.19 → 0.10.20
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 +7 -0
- package/dist/react.js +382 -181
- package/docs/reference/protocol.md +10 -0
- package/package.json +1 -1
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/stock.ts +48 -42
- package/styles/video-chat.css +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.10.20
|
|
8
|
+
|
|
9
|
+
- Collapse completed-response subtitles to an expandable transcript and remove the Pexels header link.
|
|
10
|
+
- Present narration in compact, progressive caption pages while preserving the complete spoken text.
|
|
11
|
+
- Reflect host-resolved footage mode when a response falls back to stock.
|
|
12
|
+
- Prefer close stock matches while allowing provider-ranked illustrative results and one bounded broader search when no usable footage is found.
|
|
13
|
+
|
|
7
14
|
## 0.10.19
|
|
8
15
|
|
|
9
16
|
- Show a subtle preparation status while the first response scene is loading.
|
package/dist/react.js
CHANGED
|
@@ -1262,6 +1262,77 @@ var VideoError = class extends Error {
|
|
|
1262
1262
|
}
|
|
1263
1263
|
};
|
|
1264
1264
|
|
|
1265
|
+
// src/video-chat/caption-progress.ts
|
|
1266
|
+
function createCaptionVoice(source, now = () => performance.now()) {
|
|
1267
|
+
const durations = /* @__PURE__ */ new Map();
|
|
1268
|
+
let held = false;
|
|
1269
|
+
let current;
|
|
1270
|
+
const read = () => {
|
|
1271
|
+
const state = current;
|
|
1272
|
+
if (!state || !state.started || state.signal.aborted) return;
|
|
1273
|
+
const audioTime = !state.browser ? source.getCurrentTime?.() : void 0;
|
|
1274
|
+
const realClock = typeof audioTime === "number" && Number.isFinite(audioTime);
|
|
1275
|
+
if (!held) {
|
|
1276
|
+
state.elapsed = realClock ? Math.max(state.offset, audioTime) : state.offset + (state.since === void 0 ? 0 : (now() - state.since) / 1e3);
|
|
1277
|
+
}
|
|
1278
|
+
return { text: state.text, elapsedSeconds: Math.min(state.duration, Math.max(0, state.elapsed)), durationSeconds: state.duration, timing: realClock ? "audio" : "estimated" };
|
|
1279
|
+
};
|
|
1280
|
+
const reset = () => {
|
|
1281
|
+
current = void 0;
|
|
1282
|
+
};
|
|
1283
|
+
const voice = {
|
|
1284
|
+
get supportsOffsets() {
|
|
1285
|
+
return source.supportsOffsets;
|
|
1286
|
+
},
|
|
1287
|
+
...source.getCurrentTime ? { getCurrentTime: () => source.getCurrentTime() } : {},
|
|
1288
|
+
async prepare(text, options) {
|
|
1289
|
+
const prepared = await source.prepare(text, options);
|
|
1290
|
+
if (!options?.signal?.aborted && Number.isFinite(prepared.seconds) && prepared.seconds > 0) {
|
|
1291
|
+
if (durations.size >= 60) durations.delete(durations.keys().next().value);
|
|
1292
|
+
durations.set(text.trim(), prepared.seconds);
|
|
1293
|
+
}
|
|
1294
|
+
return prepared;
|
|
1295
|
+
},
|
|
1296
|
+
async speak(text, options) {
|
|
1297
|
+
const state = { text, duration: durations.get(text.trim()) ?? Math.max(1, text.trim().split(/\s+/u).length / 2.5), offset: options.offsetSeconds ?? 0, elapsed: options.offsetSeconds ?? 0, started: false, since: void 0, browser: false, signal: options.signal };
|
|
1298
|
+
current = state;
|
|
1299
|
+
const aborted = () => {
|
|
1300
|
+
if (current === state) reset();
|
|
1301
|
+
};
|
|
1302
|
+
options.signal.addEventListener("abort", aborted, { once: true });
|
|
1303
|
+
try {
|
|
1304
|
+
await source.speak(text, { ...options, onStart: (kind) => {
|
|
1305
|
+
if (current !== state || state.signal.aborted) return;
|
|
1306
|
+
if (!state.started) {
|
|
1307
|
+
state.started = true;
|
|
1308
|
+
state.browser = kind === "browser";
|
|
1309
|
+
state.since = now();
|
|
1310
|
+
}
|
|
1311
|
+
options.onStart?.(kind);
|
|
1312
|
+
} });
|
|
1313
|
+
} finally {
|
|
1314
|
+
options.signal.removeEventListener("abort", aborted);
|
|
1315
|
+
if (current === state) reset();
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
pause() {
|
|
1319
|
+
read();
|
|
1320
|
+
held = true;
|
|
1321
|
+
source.pause();
|
|
1322
|
+
},
|
|
1323
|
+
resume() {
|
|
1324
|
+
if (held && current?.started) {
|
|
1325
|
+
current.offset = current.elapsed;
|
|
1326
|
+
current.since = now();
|
|
1327
|
+
}
|
|
1328
|
+
held = false;
|
|
1329
|
+
source.resume();
|
|
1330
|
+
},
|
|
1331
|
+
setMuted: (muted) => source.setMuted(muted)
|
|
1332
|
+
};
|
|
1333
|
+
return { voice, getCaptionProgress: read, reset };
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1265
1336
|
// src/player/recover-scene-media.ts
|
|
1266
1337
|
function recoverSceneMedia(scene) {
|
|
1267
1338
|
if (scene.templateId === "cinemaMedia") {
|
|
@@ -1276,7 +1347,7 @@ function recoverSceneMedia(scene) {
|
|
|
1276
1347
|
}
|
|
1277
1348
|
|
|
1278
1349
|
// src/video-chat/use-video-chat.ts
|
|
1279
|
-
import { useCallback as useCallback2, useEffect as useEffect4, useReducer, useRef as useRef3 } from "react";
|
|
1350
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo2, useReducer, useRef as useRef3 } from "react";
|
|
1280
1351
|
|
|
1281
1352
|
// src/protocol/scene-timeline.ts
|
|
1282
1353
|
function createQueue() {
|
|
@@ -1905,6 +1976,9 @@ function reducer(state, action) {
|
|
|
1905
1976
|
return { ...state, capabilities: action.value };
|
|
1906
1977
|
case "welcome":
|
|
1907
1978
|
return { ...state, welcome: action.value };
|
|
1979
|
+
case "resolved-mode":
|
|
1980
|
+
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
1981
|
+
return { ...state, turns: replaceTurn(state.turns, action.id, (turn) => ({ ...turn, mode: action.mode })) };
|
|
1908
1982
|
case "start":
|
|
1909
1983
|
return {
|
|
1910
1984
|
...state,
|
|
@@ -2141,7 +2215,10 @@ function useVideoChatSession(options = {}) {
|
|
|
2141
2215
|
onFallback: () => voiceWarningRef.current()
|
|
2142
2216
|
});
|
|
2143
2217
|
}
|
|
2144
|
-
const
|
|
2218
|
+
const rawVoice = options.voice ?? ownedVoiceRef.current;
|
|
2219
|
+
const captionVoice = useMemo2(() => createCaptionVoice(rawVoice), [rawVoice]);
|
|
2220
|
+
useEffect4(() => () => captionVoice.reset(), [captionVoice]);
|
|
2221
|
+
const voice = captionVoice.voice;
|
|
2145
2222
|
const voiceRef = useRef3(voice);
|
|
2146
2223
|
voiceRef.current = voice;
|
|
2147
2224
|
const unavailableVoiceLines = useRef3(/* @__PURE__ */ new Set());
|
|
@@ -2296,7 +2373,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2296
2373
|
throw new VideoError("timeoutMs must be positive", { code: "invalid_option" });
|
|
2297
2374
|
}
|
|
2298
2375
|
const timeout = setTimeout(() => controller.abort(new DOMException("Video chat timed out", "TimeoutError")), timeoutMs);
|
|
2299
|
-
|
|
2376
|
+
let mode = currentOptions.mode ?? "cinematic";
|
|
2300
2377
|
const orientation = currentOptions.orientation ?? "landscape";
|
|
2301
2378
|
const id = (currentOptions.createTurnId ?? defaultTurnId)();
|
|
2302
2379
|
const conversation = conversationFor(stateRef.current.turns);
|
|
@@ -2441,6 +2518,12 @@ function useVideoChatSession(options = {}) {
|
|
|
2441
2518
|
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
2442
2519
|
throw new VideoError("Video chat endpoint did not return a video stream", { code: "invalid_response" });
|
|
2443
2520
|
}
|
|
2521
|
+
const resolvedMode = response.headers.get("x-vanillasky-resolved-video-mode");
|
|
2522
|
+
if (isCurrent() && (resolvedMode === "pexels" || resolvedMode === "cinematic")) {
|
|
2523
|
+
mode = resolvedMode;
|
|
2524
|
+
dispatch({ type: "resolved-mode", id, mode });
|
|
2525
|
+
if (firstFrameRef.current?.turnId === id) firstFrameRef.current.mode = mode;
|
|
2526
|
+
}
|
|
2444
2527
|
const planned = [];
|
|
2445
2528
|
const lines = spokenHook ? [spokenHook] : [];
|
|
2446
2529
|
const pending = [];
|
|
@@ -2818,14 +2901,117 @@ function useVideoChatSession(options = {}) {
|
|
|
2818
2901
|
playerKey: state.playerKey,
|
|
2819
2902
|
playerProps
|
|
2820
2903
|
};
|
|
2821
|
-
return { chat, restoreSession };
|
|
2904
|
+
return { chat, restoreSession, getCaptionProgress: captionVoice.getCaptionProgress };
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/video-chat/caption-pages.tsx
|
|
2908
|
+
import { useEffect as useEffect5, useLayoutEffect, useRef as useRef4, useState as useState3 } from "react";
|
|
2909
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
2910
|
+
function splitCaptionPages(text, fits) {
|
|
2911
|
+
const pages = [];
|
|
2912
|
+
let rest = Array.from(text);
|
|
2913
|
+
while (rest.length) {
|
|
2914
|
+
let low = 1;
|
|
2915
|
+
let high = rest.length;
|
|
2916
|
+
while (low < high) {
|
|
2917
|
+
const middle = Math.ceil((low + high) / 2);
|
|
2918
|
+
if (fits(rest.slice(0, middle).join(""))) low = middle;
|
|
2919
|
+
else high = middle - 1;
|
|
2920
|
+
}
|
|
2921
|
+
let end = low;
|
|
2922
|
+
if (end < rest.length) {
|
|
2923
|
+
const space = rest.slice(0, end + 1).lastIndexOf(" ");
|
|
2924
|
+
if (space > 0) end = space + 1;
|
|
2925
|
+
}
|
|
2926
|
+
pages.push(rest.slice(0, end).join(""));
|
|
2927
|
+
rest = rest.slice(end);
|
|
2928
|
+
}
|
|
2929
|
+
return pages;
|
|
2930
|
+
}
|
|
2931
|
+
function captionPageAt(pages, elapsed, duration) {
|
|
2932
|
+
const total = pages.reduce((sum, page) => sum + page.length, 0);
|
|
2933
|
+
const position = duration > 0 ? Math.max(0, elapsed / duration) * total : 0;
|
|
2934
|
+
let end = 0;
|
|
2935
|
+
for (let index = 0; index < pages.length - 1; index++) {
|
|
2936
|
+
end += pages[index].length;
|
|
2937
|
+
if (position < end) return index;
|
|
2938
|
+
}
|
|
2939
|
+
return Math.max(0, pages.length - 1);
|
|
2940
|
+
}
|
|
2941
|
+
function CaptionPages({ text, getProgress }) {
|
|
2942
|
+
const line = useRef4(null);
|
|
2943
|
+
const lastProgress = useRef4(void 0);
|
|
2944
|
+
const getter = useRef4(getProgress);
|
|
2945
|
+
getter.current = getProgress;
|
|
2946
|
+
const [source, setSource] = useState3(text);
|
|
2947
|
+
const [pages, setPages] = useState3([]);
|
|
2948
|
+
const [index, setIndex] = useState3(0);
|
|
2949
|
+
const [timing, setTiming] = useState3();
|
|
2950
|
+
useLayoutEffect(() => {
|
|
2951
|
+
const element = line.current;
|
|
2952
|
+
if (!element) return;
|
|
2953
|
+
const measure = () => {
|
|
2954
|
+
const width = element.getBoundingClientRect().width;
|
|
2955
|
+
if (!width) {
|
|
2956
|
+
setPages([source]);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
const style = getComputedStyle(element);
|
|
2960
|
+
const probe = element.cloneNode(false);
|
|
2961
|
+
probe.removeAttribute("aria-live");
|
|
2962
|
+
probe.setAttribute("aria-hidden", "true");
|
|
2963
|
+
Object.assign(probe.style, { position: "absolute", visibility: "hidden", pointerEvents: "none", width: `${width}px`, height: "auto", maxHeight: "none", inset: "0 auto auto 0", animation: "none" });
|
|
2964
|
+
element.parentElement.append(probe);
|
|
2965
|
+
const height = parseFloat(style.lineHeight) * 2;
|
|
2966
|
+
const next = splitCaptionPages(source, (candidate) => {
|
|
2967
|
+
probe.textContent = candidate;
|
|
2968
|
+
return probe.getBoundingClientRect().height <= height + 0.5;
|
|
2969
|
+
});
|
|
2970
|
+
probe.remove();
|
|
2971
|
+
setPages(next);
|
|
2972
|
+
};
|
|
2973
|
+
measure();
|
|
2974
|
+
let previousWidth = element.getBoundingClientRect().width;
|
|
2975
|
+
let previousFont = getComputedStyle(element).font;
|
|
2976
|
+
const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(() => {
|
|
2977
|
+
const width = element.getBoundingClientRect().width;
|
|
2978
|
+
const font = getComputedStyle(element).font;
|
|
2979
|
+
if (width !== previousWidth || font !== previousFont) {
|
|
2980
|
+
previousWidth = width;
|
|
2981
|
+
previousFont = font;
|
|
2982
|
+
measure();
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
observer?.observe(element);
|
|
2986
|
+
document.fonts?.addEventListener("loadingdone", measure);
|
|
2987
|
+
return () => {
|
|
2988
|
+
observer?.disconnect();
|
|
2989
|
+
document.fonts?.removeEventListener("loadingdone", measure);
|
|
2990
|
+
};
|
|
2991
|
+
}, [source]);
|
|
2992
|
+
useEffect5(() => {
|
|
2993
|
+
let frame = 0;
|
|
2994
|
+
const update = () => {
|
|
2995
|
+
const observed = text ? getter.current() : void 0;
|
|
2996
|
+
if (observed) lastProgress.current = { caption: text, progress: observed };
|
|
2997
|
+
const progress = observed ?? (lastProgress.current?.caption === text ? lastProgress.current.progress : void 0);
|
|
2998
|
+
const nextSource = progress?.text ?? text;
|
|
2999
|
+
setSource((current) => current === nextSource ? current : nextSource);
|
|
3000
|
+
setTiming(progress?.timing);
|
|
3001
|
+
setIndex(nextSource !== source ? 0 : captionPageAt(pages, progress?.elapsedSeconds ?? 0, progress?.durationSeconds ?? 0));
|
|
3002
|
+
frame = requestAnimationFrame(update);
|
|
3003
|
+
};
|
|
3004
|
+
update();
|
|
3005
|
+
return () => cancelAnimationFrame(frame);
|
|
3006
|
+
}, [text, source, pages]);
|
|
3007
|
+
return /* @__PURE__ */ jsx3("p", { ref: line, className: "line", "aria-live": "polite", "data-caption-timing": timing, "data-caption-page": index, children: pages[index] ?? "" });
|
|
2822
3008
|
}
|
|
2823
3009
|
|
|
2824
3010
|
// src/video-chat/video-chat.tsx
|
|
2825
|
-
import { useCallback as useCallback6, useEffect as
|
|
3011
|
+
import { useCallback as useCallback6, useEffect as useEffect10, useId, useLayoutEffect as useLayoutEffect3, useMemo as useMemo3, useRef as useRef9, useState as useState8 } from "react";
|
|
2826
3012
|
|
|
2827
3013
|
// src/video-chat/icons.tsx
|
|
2828
|
-
import { jsx as
|
|
3014
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2829
3015
|
var stroke = {
|
|
2830
3016
|
fill: "none",
|
|
2831
3017
|
stroke: "currentColor",
|
|
@@ -2834,50 +3020,50 @@ var stroke = {
|
|
|
2834
3020
|
strokeLinejoin: "round"
|
|
2835
3021
|
};
|
|
2836
3022
|
function Glyph({ children }) {
|
|
2837
|
-
return /* @__PURE__ */
|
|
3023
|
+
return /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", ...stroke, children });
|
|
2838
3024
|
}
|
|
2839
3025
|
var Sessions = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2840
|
-
/* @__PURE__ */
|
|
2841
|
-
/* @__PURE__ */
|
|
3026
|
+
/* @__PURE__ */ jsx4("path", { d: "M5 3h10a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H8l-5 3V5a2 2 0 0 1 2-2Z" }),
|
|
3027
|
+
/* @__PURE__ */ jsx4("path", { d: "M17 8h2a2 2 0 0 1 2 2v11l-4-3h-6a2 2 0 0 1-2-2v-2" })
|
|
2842
3028
|
] });
|
|
2843
|
-
var Plus = () => /* @__PURE__ */
|
|
3029
|
+
var Plus = () => /* @__PURE__ */ jsx4(Glyph, { children: /* @__PURE__ */ jsx4("path", { d: "M12 5v14M5 12h14" }) });
|
|
2844
3030
|
var Gear = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2845
|
-
/* @__PURE__ */
|
|
2846
|
-
/* @__PURE__ */
|
|
3031
|
+
/* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "3" }),
|
|
3032
|
+
/* @__PURE__ */ jsx4("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" })
|
|
2847
3033
|
] });
|
|
2848
3034
|
var Sound = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2849
|
-
/* @__PURE__ */
|
|
2850
|
-
/* @__PURE__ */
|
|
3035
|
+
/* @__PURE__ */ jsx4("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
3036
|
+
/* @__PURE__ */ jsx4("path", { d: "M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13" })
|
|
2851
3037
|
] });
|
|
2852
3038
|
var Muted = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2853
|
-
/* @__PURE__ */
|
|
2854
|
-
/* @__PURE__ */
|
|
3039
|
+
/* @__PURE__ */ jsx4("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
3040
|
+
/* @__PURE__ */ jsx4("path", { d: "m22 9-6 6M16 9l6 6" })
|
|
2855
3041
|
] });
|
|
2856
3042
|
var Mic = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2857
|
-
/* @__PURE__ */
|
|
2858
|
-
/* @__PURE__ */
|
|
3043
|
+
/* @__PURE__ */ jsx4("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3" }),
|
|
3044
|
+
/* @__PURE__ */ jsx4("path", { d: "M5 11a7 7 0 0 0 14 0M12 18v4" })
|
|
2859
3045
|
] });
|
|
2860
|
-
var Send = () => /* @__PURE__ */
|
|
3046
|
+
var Send = () => /* @__PURE__ */ jsx4(Glyph, { children: /* @__PURE__ */ jsx4("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
|
|
2861
3047
|
var Stop = () => /* @__PURE__ */ jsxs3("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: [
|
|
2862
|
-
/* @__PURE__ */
|
|
2863
|
-
/* @__PURE__ */
|
|
3048
|
+
/* @__PURE__ */ jsx4("rect", { x: "7", y: "5", width: "3.4", height: "14", rx: "1.4" }),
|
|
3049
|
+
/* @__PURE__ */ jsx4("rect", { x: "13.6", y: "5", width: "3.4", height: "14", rx: "1.4" })
|
|
2864
3050
|
] });
|
|
2865
|
-
var Play = () => /* @__PURE__ */
|
|
3051
|
+
var Play = () => /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M8 5.5v13l11-6.5z" }) });
|
|
2866
3052
|
var Replay = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2867
|
-
/* @__PURE__ */
|
|
2868
|
-
/* @__PURE__ */
|
|
3053
|
+
/* @__PURE__ */ jsx4("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
3054
|
+
/* @__PURE__ */ jsx4("path", { d: "M3 3v5h5" })
|
|
2869
3055
|
] });
|
|
2870
|
-
var ChevronUp = () => /* @__PURE__ */
|
|
2871
|
-
var Close = () => /* @__PURE__ */
|
|
3056
|
+
var ChevronUp = () => /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", width: "14", height: "14", "aria-hidden": "true", ...stroke, children: /* @__PURE__ */ jsx4("path", { d: "m6 15 6-6 6 6" }) });
|
|
3057
|
+
var Close = () => /* @__PURE__ */ jsx4(Glyph, { children: /* @__PURE__ */ jsx4("path", { d: "M6 6l12 12M18 6 6 18" }) });
|
|
2872
3058
|
var Warning = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2873
|
-
/* @__PURE__ */
|
|
2874
|
-
/* @__PURE__ */
|
|
3059
|
+
/* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "9" }),
|
|
3060
|
+
/* @__PURE__ */ jsx4("path", { d: "M12 8v5M12 16.2v.1" })
|
|
2875
3061
|
] });
|
|
2876
3062
|
|
|
2877
3063
|
// src/video-chat/use-dismiss.ts
|
|
2878
|
-
import { useEffect as
|
|
3064
|
+
import { useEffect as useEffect6 } from "react";
|
|
2879
3065
|
function useDismiss(open, close, surfaces) {
|
|
2880
|
-
|
|
3066
|
+
useEffect6(() => {
|
|
2881
3067
|
if (!open) return;
|
|
2882
3068
|
const onKeyDown = (event) => {
|
|
2883
3069
|
if (event.key !== "Escape") return;
|
|
@@ -2899,7 +3085,7 @@ function useDismiss(open, close, surfaces) {
|
|
|
2899
3085
|
}, [open, close, surfaces]);
|
|
2900
3086
|
}
|
|
2901
3087
|
function useFocusTrap(active, surface) {
|
|
2902
|
-
|
|
3088
|
+
useEffect6(() => {
|
|
2903
3089
|
if (!active) return;
|
|
2904
3090
|
const returnTo = document.activeElement;
|
|
2905
3091
|
const focusable = () => Array.from(
|
|
@@ -2932,12 +3118,12 @@ function useFocusTrap(active, surface) {
|
|
|
2932
3118
|
}
|
|
2933
3119
|
|
|
2934
3120
|
// src/video-chat/suggestion-cards.tsx
|
|
2935
|
-
import { useCallback as useCallback3, useEffect as
|
|
2936
|
-
import { Fragment as Fragment2, jsx as
|
|
3121
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef5, useState as useState4 } from "react";
|
|
3122
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2937
3123
|
function Frame({ media, poster, playing, onReady, onError, revealWhenReady = false }) {
|
|
2938
|
-
const video =
|
|
2939
|
-
const [playingUrl, setPlayingUrl] =
|
|
2940
|
-
|
|
3124
|
+
const video = useRef5(null);
|
|
3125
|
+
const [playingUrl, setPlayingUrl] = useState4(null);
|
|
3126
|
+
useEffect7(() => {
|
|
2941
3127
|
const element = video.current;
|
|
2942
3128
|
if (!element || playing === void 0) return;
|
|
2943
3129
|
if (playing) void element.play().catch(() => void 0);
|
|
@@ -2949,9 +3135,9 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
2949
3135
|
onReady?.();
|
|
2950
3136
|
};
|
|
2951
3137
|
const appearance = revealWhenReady ? { opacity: playingUrl === media.url ? 1 : 0, transition: "opacity 200ms ease" } : void 0;
|
|
2952
|
-
if (media.type === "image") return /* @__PURE__ */
|
|
3138
|
+
if (media.type === "image") return /* @__PURE__ */ jsx5("img", { className: "frame-media", src: media.url, alt: "", style: appearance, onLoad: ready, onError });
|
|
2953
3139
|
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
2954
|
-
/* @__PURE__ */
|
|
3140
|
+
/* @__PURE__ */ jsx5(
|
|
2955
3141
|
"video",
|
|
2956
3142
|
{
|
|
2957
3143
|
ref: video,
|
|
@@ -2972,24 +3158,24 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
2972
3158
|
"aria-hidden": "true"
|
|
2973
3159
|
}
|
|
2974
3160
|
),
|
|
2975
|
-
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */
|
|
3161
|
+
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */ jsx5("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady })
|
|
2976
3162
|
] });
|
|
2977
3163
|
}
|
|
2978
3164
|
function SuggestionCards({ suggestions, label, onAsk }) {
|
|
2979
|
-
const railRef =
|
|
2980
|
-
const [at, setAt] =
|
|
2981
|
-
const [taken, setTaken] =
|
|
3165
|
+
const railRef = useRef5(null);
|
|
3166
|
+
const [at, setAt] = useState4(0);
|
|
3167
|
+
const [taken, setTaken] = useState4(false);
|
|
2982
3168
|
const take = useCallback3((index) => {
|
|
2983
3169
|
setTaken(true);
|
|
2984
3170
|
setAt(index);
|
|
2985
3171
|
}, []);
|
|
2986
|
-
|
|
3172
|
+
useEffect7(() => {
|
|
2987
3173
|
if (taken || suggestions.length < 2) return;
|
|
2988
3174
|
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
2989
3175
|
const tour = window.setInterval(() => setAt((index) => (index + 1) % suggestions.length), 5e3);
|
|
2990
3176
|
return () => window.clearInterval(tour);
|
|
2991
3177
|
}, [taken, suggestions.length]);
|
|
2992
|
-
|
|
3178
|
+
useEffect7(() => {
|
|
2993
3179
|
const rail = railRef.current;
|
|
2994
3180
|
const card = rail?.children[at];
|
|
2995
3181
|
if (!rail || !card) return;
|
|
@@ -2999,7 +3185,7 @@ function SuggestionCards({ suggestions, label, onAsk }) {
|
|
|
2999
3185
|
}, [at]);
|
|
3000
3186
|
if (suggestions.length === 0) return null;
|
|
3001
3187
|
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
3002
|
-
/* @__PURE__ */
|
|
3188
|
+
/* @__PURE__ */ jsx5("ul", { className: "cards", ref: railRef, "aria-label": label, children: suggestions.map((card, index) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs4(
|
|
3003
3189
|
"button",
|
|
3004
3190
|
{
|
|
3005
3191
|
type: "button",
|
|
@@ -3008,13 +3194,13 @@ function SuggestionCards({ suggestions, label, onAsk }) {
|
|
|
3008
3194
|
onPointerEnter: () => take(index),
|
|
3009
3195
|
onClick: () => onAsk(card),
|
|
3010
3196
|
children: [
|
|
3011
|
-
/* @__PURE__ */
|
|
3012
|
-
/* @__PURE__ */
|
|
3013
|
-
/* @__PURE__ */
|
|
3197
|
+
/* @__PURE__ */ jsx5(Frame, { media: card.media, poster: true, playing: index === at }),
|
|
3198
|
+
/* @__PURE__ */ jsx5("span", { className: "card-wash", "aria-hidden": "true" }),
|
|
3199
|
+
/* @__PURE__ */ jsx5("span", { className: "card-prompt", children: card.prompt })
|
|
3014
3200
|
]
|
|
3015
3201
|
}
|
|
3016
3202
|
) }, card.prompt)) }),
|
|
3017
|
-
suggestions.length > 1 && /* @__PURE__ */
|
|
3203
|
+
suggestions.length > 1 && /* @__PURE__ */ jsx5("div", { className: "card-dots", role: "group", "aria-label": `${label} navigation`, children: suggestions.map((card, index) => /* @__PURE__ */ jsx5(
|
|
3018
3204
|
"button",
|
|
3019
3205
|
{
|
|
3020
3206
|
type: "button",
|
|
@@ -3029,30 +3215,30 @@ function SuggestionCards({ suggestions, label, onAsk }) {
|
|
|
3029
3215
|
}
|
|
3030
3216
|
|
|
3031
3217
|
// src/video-chat/welcome.tsx
|
|
3032
|
-
import { Fragment as Fragment3, jsx as
|
|
3218
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3033
3219
|
function Welcome({ data, onAsk, title }) {
|
|
3034
3220
|
return /* @__PURE__ */ jsxs5("div", { className: "welcome", children: [
|
|
3035
|
-
/* @__PURE__ */
|
|
3036
|
-
/* @__PURE__ */
|
|
3221
|
+
/* @__PURE__ */ jsx6(Frame, { media: data?.hero ?? null, poster: true }),
|
|
3222
|
+
/* @__PURE__ */ jsx6("div", { className: "welcome-wash", "aria-hidden": "true" }),
|
|
3037
3223
|
/* @__PURE__ */ jsxs5("div", { className: "welcome-body", children: [
|
|
3038
|
-
/* @__PURE__ */
|
|
3224
|
+
/* @__PURE__ */ jsx6("h1", { className: "welcome-title", children: title ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
3039
3225
|
"An AI chat that responds",
|
|
3040
|
-
/* @__PURE__ */
|
|
3041
|
-
/* @__PURE__ */
|
|
3226
|
+
/* @__PURE__ */ jsx6("br", {}),
|
|
3227
|
+
/* @__PURE__ */ jsx6("em", { children: "in video, not text." })
|
|
3042
3228
|
] }) }),
|
|
3043
|
-
/* @__PURE__ */
|
|
3229
|
+
/* @__PURE__ */ jsx6(SuggestionCards, { suggestions: data?.cards ?? [], label: "Suggested prompts", onAsk })
|
|
3044
3230
|
] })
|
|
3045
3231
|
] });
|
|
3046
3232
|
}
|
|
3047
3233
|
|
|
3048
3234
|
// src/video-chat/opening-chapter.tsx
|
|
3049
|
-
import { useLayoutEffect, useRef as
|
|
3050
|
-
import { jsx as
|
|
3235
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef6, useState as useState5 } from "react";
|
|
3236
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3051
3237
|
function OpeningChapter({ title, preparing = false }) {
|
|
3052
|
-
const root =
|
|
3053
|
-
const [size, setSize] =
|
|
3054
|
-
const [titleBottom, setTitleBottom] =
|
|
3055
|
-
|
|
3238
|
+
const root = useRef6(null);
|
|
3239
|
+
const [size, setSize] = useState5({ width: 1080, height: 1080 });
|
|
3240
|
+
const [titleBottom, setTitleBottom] = useState5(0);
|
|
3241
|
+
useLayoutEffect2(() => {
|
|
3056
3242
|
const element = root.current;
|
|
3057
3243
|
if (!element) return;
|
|
3058
3244
|
const measure = () => {
|
|
@@ -3070,7 +3256,7 @@ function OpeningChapter({ title, preparing = false }) {
|
|
|
3070
3256
|
return () => observer.disconnect();
|
|
3071
3257
|
}, [title]);
|
|
3072
3258
|
return /* @__PURE__ */ jsxs6("div", { ref: root, "data-opening-chapter": true, className: "opening-chapter", children: [
|
|
3073
|
-
/* @__PURE__ */
|
|
3259
|
+
/* @__PURE__ */ jsx7(
|
|
3074
3260
|
TitleSceneTemplate,
|
|
3075
3261
|
{
|
|
3076
3262
|
variables: { title },
|
|
@@ -3083,14 +3269,14 @@ function OpeningChapter({ title, preparing = false }) {
|
|
|
3083
3269
|
}
|
|
3084
3270
|
),
|
|
3085
3271
|
preparing && /* @__PURE__ */ jsxs6("div", { className: "video-preparation", style: { top: titleBottom + 24 }, role: "status", "aria-label": "Video preparation", children: [
|
|
3086
|
-
/* @__PURE__ */
|
|
3087
|
-
/* @__PURE__ */
|
|
3272
|
+
/* @__PURE__ */ jsx7("span", { className: "video-preparation-spinner", "aria-hidden": "true" }),
|
|
3273
|
+
/* @__PURE__ */ jsx7("span", { className: "video-preparation-text", children: "Preparing your video\u2026" })
|
|
3088
3274
|
] })
|
|
3089
3275
|
] });
|
|
3090
3276
|
}
|
|
3091
3277
|
|
|
3092
3278
|
// src/video-chat/use-voice-input.ts
|
|
3093
|
-
import { useCallback as useCallback4, useEffect as
|
|
3279
|
+
import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef7, useState as useState6 } from "react";
|
|
3094
3280
|
function recognitionConstructor() {
|
|
3095
3281
|
if (typeof window === "undefined") return void 0;
|
|
3096
3282
|
const holder = window;
|
|
@@ -3160,16 +3346,16 @@ async function recordAndTranscribe(signal, ready, captured, options) {
|
|
|
3160
3346
|
return clip ? transcribeRecording(clip, signal, options) : "";
|
|
3161
3347
|
}
|
|
3162
3348
|
function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {}) {
|
|
3163
|
-
const [supported, setSupported] =
|
|
3164
|
-
const [listening, setListening] =
|
|
3165
|
-
const [error, setError] =
|
|
3166
|
-
const [thinking, setThinking] =
|
|
3167
|
-
const recognitionRef =
|
|
3168
|
-
const useRecorderRef =
|
|
3169
|
-
const operationRef =
|
|
3170
|
-
const handlerRef =
|
|
3349
|
+
const [supported, setSupported] = useState6(false);
|
|
3350
|
+
const [listening, setListening] = useState6(false);
|
|
3351
|
+
const [error, setError] = useState6();
|
|
3352
|
+
const [thinking, setThinking] = useState6(false);
|
|
3353
|
+
const recognitionRef = useRef7(void 0);
|
|
3354
|
+
const useRecorderRef = useRef7(false);
|
|
3355
|
+
const operationRef = useRef7(void 0);
|
|
3356
|
+
const handlerRef = useRef7(onTranscript);
|
|
3171
3357
|
handlerRef.current = onTranscript;
|
|
3172
|
-
|
|
3358
|
+
useEffect8(() => {
|
|
3173
3359
|
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3174
3360
|
}, [transcriptionAvailable]);
|
|
3175
3361
|
const stop = useCallback4(() => {
|
|
@@ -3182,7 +3368,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3182
3368
|
setListening(false);
|
|
3183
3369
|
setThinking(false);
|
|
3184
3370
|
}, []);
|
|
3185
|
-
|
|
3371
|
+
useEffect8(() => () => {
|
|
3186
3372
|
recognitionRef.current?.abort();
|
|
3187
3373
|
recognitionRef.current = void 0;
|
|
3188
3374
|
const operation = operationRef.current;
|
|
@@ -3294,13 +3480,13 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3294
3480
|
}
|
|
3295
3481
|
|
|
3296
3482
|
// src/video-chat/use-immersive-controls.ts
|
|
3297
|
-
import { useCallback as useCallback5, useEffect as
|
|
3483
|
+
import { useCallback as useCallback5, useEffect as useEffect9, useRef as useRef8, useState as useState7 } from "react";
|
|
3298
3484
|
function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
3299
|
-
const [visible, setVisible] =
|
|
3300
|
-
const hovered =
|
|
3301
|
-
const focused =
|
|
3302
|
-
const previousCaptions =
|
|
3303
|
-
const timer =
|
|
3485
|
+
const [visible, setVisible] = useState7(true);
|
|
3486
|
+
const hovered = useRef8(false);
|
|
3487
|
+
const focused = useRef8(false);
|
|
3488
|
+
const previousCaptions = useRef8(false);
|
|
3489
|
+
const timer = useRef8(null);
|
|
3304
3490
|
const clearTimer = useCallback5(() => {
|
|
3305
3491
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
3306
3492
|
timer.current = null;
|
|
@@ -3315,7 +3501,7 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3315
3501
|
}, 2e3);
|
|
3316
3502
|
}
|
|
3317
3503
|
}, [clearTimer, playing, pinned]);
|
|
3318
|
-
|
|
3504
|
+
useEffect9(() => {
|
|
3319
3505
|
const firstCaption = hasCaptions && !previousCaptions.current;
|
|
3320
3506
|
previousCaptions.current = hasCaptions;
|
|
3321
3507
|
if (firstCaption && playing && !pinned && !focused.current) {
|
|
@@ -3351,9 +3537,9 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3351
3537
|
}
|
|
3352
3538
|
|
|
3353
3539
|
// src/video-chat/logo.tsx
|
|
3354
|
-
import { jsx as
|
|
3540
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
3355
3541
|
function Logo() {
|
|
3356
|
-
return /* @__PURE__ */
|
|
3542
|
+
return /* @__PURE__ */ jsx8("img", { className: "brand-logo", src: new URL("./assets/vanillasky-logo.svg", import.meta.url).href, alt: "VanillaSky", width: 144, height: 36 });
|
|
3357
3543
|
}
|
|
3358
3544
|
|
|
3359
3545
|
// src/video-chat/modes.ts
|
|
@@ -3364,11 +3550,11 @@ var visualModes = [
|
|
|
3364
3550
|
var defaultMode = visualModes[0];
|
|
3365
3551
|
|
|
3366
3552
|
// src/video-chat/video-chat.tsx
|
|
3367
|
-
import { Fragment as Fragment4, jsx as
|
|
3553
|
+
import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3368
3554
|
var DESKTOP_WIDTH = 900;
|
|
3369
3555
|
function useViewportOrientation() {
|
|
3370
|
-
const [portrait, setPortrait] =
|
|
3371
|
-
|
|
3556
|
+
const [portrait, setPortrait] = useState8(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`).matches : false);
|
|
3557
|
+
useEffect10(() => {
|
|
3372
3558
|
if (typeof window.matchMedia !== "function") return;
|
|
3373
3559
|
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`);
|
|
3374
3560
|
const update = () => setPortrait(query.matches);
|
|
@@ -3379,47 +3565,59 @@ function useViewportOrientation() {
|
|
|
3379
3565
|
return portrait ? "portrait" : "landscape";
|
|
3380
3566
|
}
|
|
3381
3567
|
function Waveform({ active, listening }) {
|
|
3382
|
-
return /* @__PURE__ */
|
|
3568
|
+
return /* @__PURE__ */ jsx9("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx9("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
|
|
3383
3569
|
}
|
|
3384
3570
|
function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice = false }) {
|
|
3385
|
-
const [dismissedNoticeTurn, setDismissedNoticeTurn] =
|
|
3386
|
-
const [draft, setDraft] =
|
|
3387
|
-
const [selectedMode, setSelectedMode] =
|
|
3388
|
-
const [savedSessions, setSavedSessions] =
|
|
3389
|
-
const [historyOpen, setHistoryOpen] =
|
|
3390
|
-
const [settingsOpen, setSettingsOpen] =
|
|
3391
|
-
const [aboutOpen, setAboutOpen] =
|
|
3392
|
-
const [captionsOn, setCaptionsOn] =
|
|
3393
|
-
const [captionsExpanded, setCaptionsExpanded] =
|
|
3394
|
-
const [alwaysShowControls, setAlwaysShowControls] =
|
|
3395
|
-
const [editing, setEditing] =
|
|
3396
|
-
const resumeAfterInput =
|
|
3397
|
-
const panelRef =
|
|
3398
|
-
const composerRef =
|
|
3571
|
+
const [dismissedNoticeTurn, setDismissedNoticeTurn] = useState8();
|
|
3572
|
+
const [draft, setDraft] = useState8("");
|
|
3573
|
+
const [selectedMode, setSelectedMode] = useState8();
|
|
3574
|
+
const [savedSessions, setSavedSessions] = useState8([]);
|
|
3575
|
+
const [historyOpen, setHistoryOpen] = useState8(false);
|
|
3576
|
+
const [settingsOpen, setSettingsOpen] = useState8(false);
|
|
3577
|
+
const [aboutOpen, setAboutOpen] = useState8(false);
|
|
3578
|
+
const [captionsOn, setCaptionsOn] = useState8(true);
|
|
3579
|
+
const [captionsExpanded, setCaptionsExpanded] = useState8(false);
|
|
3580
|
+
const [alwaysShowControls, setAlwaysShowControls] = useState8(false);
|
|
3581
|
+
const [editing, setEditing] = useState8(false);
|
|
3582
|
+
const resumeAfterInput = useRef9(false);
|
|
3583
|
+
const panelRef = useRef9(null);
|
|
3584
|
+
const composerRef = useRef9(null);
|
|
3399
3585
|
const viewportOrientation = useViewportOrientation();
|
|
3400
3586
|
const sessionOrientation = options.orientation ?? viewportOrientation;
|
|
3401
|
-
const { chat, restoreSession } = useVideoChatSession({
|
|
3587
|
+
const { chat, restoreSession, getCaptionProgress } = useVideoChatSession({
|
|
3402
3588
|
...options,
|
|
3403
3589
|
orientation: sessionOrientation,
|
|
3404
3590
|
mode: selectedMode ?? options.mode
|
|
3405
3591
|
});
|
|
3592
|
+
const observedMode = useRef9(void 0);
|
|
3593
|
+
useEffect10(() => {
|
|
3594
|
+
const turn = chat.currentTurn;
|
|
3595
|
+
if (!turn?.mode) {
|
|
3596
|
+
observedMode.current = void 0;
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
const previous = observedMode.current;
|
|
3600
|
+
observedMode.current = { id: turn.id, mode: turn.mode };
|
|
3601
|
+
if (turn.id !== chat.shownTurn?.id) return;
|
|
3602
|
+
if (previous?.id !== turn.id || previous.mode !== turn.mode) setSelectedMode(turn.mode);
|
|
3603
|
+
}, [chat.currentTurn, chat.shownTurn?.id]);
|
|
3406
3604
|
const instanceId = useId();
|
|
3407
3605
|
const historyId = `${instanceId}-history`;
|
|
3408
3606
|
const settingsId = `${instanceId}-settings`;
|
|
3409
3607
|
const promptId = `${instanceId}-prompt`;
|
|
3410
|
-
const inputRef =
|
|
3411
|
-
const historyRef =
|
|
3412
|
-
const historyButtonRef =
|
|
3413
|
-
const settingsRef =
|
|
3414
|
-
const settingsButtonRef =
|
|
3608
|
+
const inputRef = useRef9(null);
|
|
3609
|
+
const historyRef = useRef9(null);
|
|
3610
|
+
const historyButtonRef = useRef9(null);
|
|
3611
|
+
const settingsRef = useRef9(null);
|
|
3612
|
+
const settingsButtonRef = useRef9(null);
|
|
3415
3613
|
const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
|
|
3416
3614
|
endpoint: options.endpoint,
|
|
3417
3615
|
headers: options.headers,
|
|
3418
3616
|
credentials: options.credentials,
|
|
3419
3617
|
fetcher: options.fetcher
|
|
3420
3618
|
});
|
|
3421
|
-
const historySurfaces =
|
|
3422
|
-
const settingsSurfaces =
|
|
3619
|
+
const historySurfaces = useMemo3(() => [historyRef, historyButtonRef], []);
|
|
3620
|
+
const settingsSurfaces = useMemo3(() => [settingsRef, settingsButtonRef], []);
|
|
3423
3621
|
const closeHistory = useCallback6(() => setHistoryOpen(false), []);
|
|
3424
3622
|
const closeSettings = useCallback6(() => setSettingsOpen(false), []);
|
|
3425
3623
|
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
@@ -3463,19 +3661,19 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3463
3661
|
const shown = chat.shownTurn;
|
|
3464
3662
|
const showing = chat.playerProps != null;
|
|
3465
3663
|
const handoffKey = `${shown?.id ?? ""}:${chat.playerKey}`;
|
|
3466
|
-
const [presentedBody, setPresentedBody] =
|
|
3467
|
-
const handoff =
|
|
3664
|
+
const [presentedBody, setPresentedBody] = useState8();
|
|
3665
|
+
const handoff = useRef9({ key: handoffKey, live: showing, active: false, frame: 0 });
|
|
3468
3666
|
const handoffStopped = chat.status === "error" || chat.status === "cancelled";
|
|
3469
3667
|
const waitingForBody = showing && presentedBody !== handoffKey;
|
|
3470
3668
|
const openingChapter = Boolean(shown?.prompt) && (!showing || waitingForBody) && chat.status !== "error" && chat.status !== "cancelled" && chat.status !== "ended";
|
|
3471
|
-
const [preparingKey, setPreparingKey] =
|
|
3472
|
-
|
|
3669
|
+
const [preparingKey, setPreparingKey] = useState8();
|
|
3670
|
+
useEffect10(() => {
|
|
3473
3671
|
setPreparingKey(void 0);
|
|
3474
3672
|
if (!openingChapter || !shown?.opening || chat.speaking) return;
|
|
3475
3673
|
const timer = setTimeout(() => setPreparingKey(shown.id), 1e3);
|
|
3476
3674
|
return () => clearTimeout(timer);
|
|
3477
3675
|
}, [openingChapter, shown?.opening, shown?.id, chat.speaking]);
|
|
3478
|
-
|
|
3676
|
+
useLayoutEffect3(() => {
|
|
3479
3677
|
const current = { key: handoffKey, live: showing && !handoffStopped, active: openingChapter && showing, frame: 0 };
|
|
3480
3678
|
handoff.current = current;
|
|
3481
3679
|
return () => {
|
|
@@ -3508,9 +3706,12 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3508
3706
|
const status = chat.turns.length === 0 ? "idle" : chat.status === "composing" ? "drawing" : chat.status === "playing" ? "narrating" : chat.status === "error" || chat.status === "cancelled" ? "ended" : chat.status;
|
|
3509
3707
|
const shownOrientation = shown?.orientation ?? sessionOrientation;
|
|
3510
3708
|
const stageOrientation = shown?.fixedOrientation ? shownOrientation : sessionOrientation;
|
|
3709
|
+
useEffect10(() => {
|
|
3710
|
+
setCaptionsExpanded(false);
|
|
3711
|
+
}, [chat.playbackEnded, shown?.id, chat.playerKey]);
|
|
3511
3712
|
const line = chat.caption ?? "";
|
|
3512
3713
|
const fullTranscript = shown?.video ? [shown.opening, ...shown.video.scenes.map((scene) => scene.narration)].filter((entry) => Boolean(entry)) : chat.transcript;
|
|
3513
|
-
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */
|
|
3714
|
+
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx9(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx9(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx9(Replay, {}) } : void 0;
|
|
3514
3715
|
const cancelInput = useCallback6(() => {
|
|
3515
3716
|
listen.stop();
|
|
3516
3717
|
setDraft("");
|
|
@@ -3541,7 +3742,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3541
3742
|
onFocusCapture: controls.onFocusCapture,
|
|
3542
3743
|
onBlurCapture: controls.onBlurCapture
|
|
3543
3744
|
};
|
|
3544
|
-
|
|
3745
|
+
useLayoutEffect3(() => {
|
|
3545
3746
|
const composer = composerRef.current;
|
|
3546
3747
|
if (!composer) return;
|
|
3547
3748
|
const measure = () => panelRef.current?.style.setProperty("--composer-height", `${composer.getBoundingClientRect().height}px`);
|
|
@@ -3564,10 +3765,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3564
3765
|
onKeyDownCapture: controls.reveal,
|
|
3565
3766
|
children: [
|
|
3566
3767
|
/* @__PURE__ */ jsxs7("header", { className: "chrome", ...controlEvents, children: [
|
|
3567
|
-
/* @__PURE__ */
|
|
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" })
|
|
3570
|
-
] }),
|
|
3768
|
+
/* @__PURE__ */ jsx9("div", { className: "session-brand", children: /* @__PURE__ */ jsx9("a", { className: "home-link", href: "/", "aria-label": "Home", children: /* @__PURE__ */ jsx9(Logo, {}) }) }),
|
|
3571
3769
|
/* @__PURE__ */ jsxs7("div", { className: "group", children: [
|
|
3572
3770
|
/* @__PURE__ */ jsxs7(
|
|
3573
3771
|
"button",
|
|
@@ -3584,12 +3782,12 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3584
3782
|
setSettingsOpen(false);
|
|
3585
3783
|
},
|
|
3586
3784
|
children: [
|
|
3587
|
-
/* @__PURE__ */
|
|
3588
|
-
/* @__PURE__ */
|
|
3785
|
+
/* @__PURE__ */ jsx9(Sessions, {}),
|
|
3786
|
+
/* @__PURE__ */ jsx9("span", { className: "nav-label", children: "Sessions" })
|
|
3589
3787
|
]
|
|
3590
3788
|
}
|
|
3591
3789
|
),
|
|
3592
|
-
/* @__PURE__ */
|
|
3790
|
+
/* @__PURE__ */ jsx9(
|
|
3593
3791
|
"button",
|
|
3594
3792
|
{
|
|
3595
3793
|
ref: settingsButtonRef,
|
|
@@ -3603,10 +3801,10 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3603
3801
|
setSettingsOpen((open) => !open);
|
|
3604
3802
|
setHistoryOpen(false);
|
|
3605
3803
|
},
|
|
3606
|
-
children: /* @__PURE__ */
|
|
3804
|
+
children: /* @__PURE__ */ jsx9(Gear, {})
|
|
3607
3805
|
}
|
|
3608
3806
|
),
|
|
3609
|
-
/* @__PURE__ */
|
|
3807
|
+
/* @__PURE__ */ jsx9(
|
|
3610
3808
|
"button",
|
|
3611
3809
|
{
|
|
3612
3810
|
type: "button",
|
|
@@ -3614,16 +3812,16 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3614
3812
|
"aria-label": chat.muted ? "Turn the voice on" : "Turn the voice off",
|
|
3615
3813
|
"aria-pressed": chat.muted,
|
|
3616
3814
|
onClick: () => chat.setMuted(!chat.muted),
|
|
3617
|
-
children: chat.muted ? /* @__PURE__ */
|
|
3815
|
+
children: chat.muted ? /* @__PURE__ */ jsx9(Muted, {}) : /* @__PURE__ */ jsx9(Sound, {})
|
|
3618
3816
|
}
|
|
3619
3817
|
)
|
|
3620
3818
|
] })
|
|
3621
3819
|
] }),
|
|
3622
3820
|
/* @__PURE__ */ jsxs7("div", { className: "stage-area", children: [
|
|
3623
3821
|
/* @__PURE__ */ jsxs7("div", { className: "stage", style: { background: "#000" }, children: [
|
|
3624
|
-
openingChapter && /* @__PURE__ */
|
|
3625
|
-
!showing && chat.turns.length === 0 && /* @__PURE__ */
|
|
3626
|
-
chat.playerProps && /* @__PURE__ */
|
|
3822
|
+
openingChapter && /* @__PURE__ */ jsx9(OpeningChapter, { preparing: preparingKey === shown.id && !chat.speaking, title: openingTitle.length > 120 ? `${openingTitle.slice(0, 117).trimEnd()}\u2026` : openingTitle }, shown.id),
|
|
3823
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx9(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
|
|
3824
|
+
chat.playerProps && /* @__PURE__ */ jsx9("div", { className: "player-fit", style: { width: stageOrientation === "portrait" ? "min(100cqw, 56.25cqh)" : "min(100cqw, 177.7778cqh)" }, children: /* @__PURE__ */ jsx9(
|
|
3627
3825
|
VideoPlayer,
|
|
3628
3826
|
{
|
|
3629
3827
|
...chat.playerProps,
|
|
@@ -3636,27 +3834,27 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3636
3834
|
chat.playerKey
|
|
3637
3835
|
) }),
|
|
3638
3836
|
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "ending", children: [
|
|
3639
|
-
/* @__PURE__ */
|
|
3837
|
+
/* @__PURE__ */ jsx9("div", { className: "ending-wash", "aria-hidden": "true" }),
|
|
3640
3838
|
/* @__PURE__ */ jsxs7("div", { className: "ending-body", children: [
|
|
3641
|
-
/* @__PURE__ */
|
|
3642
|
-
/* @__PURE__ */
|
|
3839
|
+
/* @__PURE__ */ jsx9("p", { className: "ending-label", children: "Ask next" }),
|
|
3840
|
+
/* @__PURE__ */ jsx9(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
|
|
3643
3841
|
] })
|
|
3644
3842
|
] })
|
|
3645
3843
|
] }),
|
|
3646
3844
|
historyOpen && /* @__PURE__ */ jsxs7("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "Sessions", children: [
|
|
3647
3845
|
/* @__PURE__ */ jsxs7("div", { className: "popover-heading", children: [
|
|
3648
|
-
/* @__PURE__ */
|
|
3649
|
-
/* @__PURE__ */
|
|
3846
|
+
/* @__PURE__ */ jsx9("h2", { children: "Sessions" }),
|
|
3847
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: "round", "aria-label": "Close sessions", onClick: closeHistory, children: /* @__PURE__ */ jsx9(Close, {}) })
|
|
3650
3848
|
] }),
|
|
3651
3849
|
/* @__PURE__ */ jsxs7("button", { type: "button", className: "history-row session-new", "aria-label": "New session", onClick: newSession, children: [
|
|
3652
|
-
/* @__PURE__ */
|
|
3850
|
+
/* @__PURE__ */ jsx9(Plus, {}),
|
|
3653
3851
|
/* @__PURE__ */ jsxs7("span", { className: "prompt", children: [
|
|
3654
3852
|
"New session",
|
|
3655
|
-
/* @__PURE__ */
|
|
3853
|
+
/* @__PURE__ */ jsx9("small", { children: "Start a fresh conversation" })
|
|
3656
3854
|
] })
|
|
3657
3855
|
] }),
|
|
3658
|
-
/* @__PURE__ */
|
|
3659
|
-
chat.turns.length === 0 && /* @__PURE__ */
|
|
3856
|
+
/* @__PURE__ */ jsx9("h3", { className: "section-label", children: "Current session" }),
|
|
3857
|
+
chat.turns.length === 0 && /* @__PURE__ */ jsx9("p", { className: "history-empty", children: "Your questions will appear here." }),
|
|
3660
3858
|
chat.turns.map((turn, index) => /* @__PURE__ */ jsxs7(
|
|
3661
3859
|
"button",
|
|
3662
3860
|
{
|
|
@@ -3673,17 +3871,17 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3673
3871
|
setHistoryOpen(false);
|
|
3674
3872
|
},
|
|
3675
3873
|
children: [
|
|
3676
|
-
/* @__PURE__ */
|
|
3874
|
+
/* @__PURE__ */ jsx9("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
|
|
3677
3875
|
/* @__PURE__ */ jsxs7("span", { className: "prompt", children: [
|
|
3678
3876
|
turn.prompt,
|
|
3679
|
-
/* @__PURE__ */
|
|
3877
|
+
/* @__PURE__ */ jsx9("small", { children: turn.id === shown?.id ? "Now showing" : turn.completed ? "Play answer" : "Unfinished answer" })
|
|
3680
3878
|
] })
|
|
3681
3879
|
]
|
|
3682
3880
|
},
|
|
3683
3881
|
turn.id
|
|
3684
3882
|
)),
|
|
3685
3883
|
savedSessions.length > 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
|
|
3686
|
-
/* @__PURE__ */
|
|
3884
|
+
/* @__PURE__ */ jsx9("h3", { className: "section-label", children: "Earlier sessions" }),
|
|
3687
3885
|
savedSessions.map((session) => /* @__PURE__ */ jsxs7("button", { type: "button", className: "history-row", onClick: () => {
|
|
3688
3886
|
listen.stop();
|
|
3689
3887
|
setDraft("");
|
|
@@ -3695,7 +3893,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3695
3893
|
setHistoryOpen(false);
|
|
3696
3894
|
setCaptionsExpanded(false);
|
|
3697
3895
|
}, children: [
|
|
3698
|
-
/* @__PURE__ */
|
|
3896
|
+
/* @__PURE__ */ jsx9(Replay, {}),
|
|
3699
3897
|
/* @__PURE__ */ jsxs7("span", { className: "prompt", children: [
|
|
3700
3898
|
session.turns[0]?.prompt,
|
|
3701
3899
|
/* @__PURE__ */ jsxs7("small", { children: [
|
|
@@ -3718,17 +3916,17 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3718
3916
|
"aria-modal": "true",
|
|
3719
3917
|
children: [
|
|
3720
3918
|
/* @__PURE__ */ jsxs7("div", { className: "popover-heading", children: [
|
|
3721
|
-
/* @__PURE__ */
|
|
3722
|
-
/* @__PURE__ */
|
|
3919
|
+
/* @__PURE__ */ jsx9("h2", { children: "Settings" }),
|
|
3920
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: "round", "aria-label": "Close settings", onClick: closeSettings, children: /* @__PURE__ */ jsx9(Close, {}) })
|
|
3723
3921
|
] }),
|
|
3724
3922
|
/* @__PURE__ */ jsxs7("fieldset", { className: "playback-options", children: [
|
|
3725
|
-
/* @__PURE__ */
|
|
3923
|
+
/* @__PURE__ */ jsx9("legend", { children: "Video source" }),
|
|
3726
3924
|
visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */ jsxs7("label", { className: "switch-row", children: [
|
|
3727
3925
|
/* @__PURE__ */ jsxs7("span", { children: [
|
|
3728
|
-
/* @__PURE__ */
|
|
3729
|
-
/* @__PURE__ */
|
|
3926
|
+
/* @__PURE__ */ jsx9("strong", { children: mode.label }),
|
|
3927
|
+
/* @__PURE__ */ jsx9("small", { children: mode.note })
|
|
3730
3928
|
] }),
|
|
3731
|
-
/* @__PURE__ */
|
|
3929
|
+
/* @__PURE__ */ jsx9(
|
|
3732
3930
|
"input",
|
|
3733
3931
|
{
|
|
3734
3932
|
type: "radio",
|
|
@@ -3741,47 +3939,50 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3741
3939
|
] }, mode.id))
|
|
3742
3940
|
] }),
|
|
3743
3941
|
/* @__PURE__ */ jsxs7("fieldset", { className: "playback-options", children: [
|
|
3744
|
-
/* @__PURE__ */
|
|
3942
|
+
/* @__PURE__ */ jsx9("legend", { children: "Watching" }),
|
|
3745
3943
|
/* @__PURE__ */ jsxs7("label", { className: "switch-row", children: [
|
|
3746
3944
|
/* @__PURE__ */ jsxs7("span", { children: [
|
|
3747
|
-
/* @__PURE__ */
|
|
3748
|
-
/* @__PURE__ */
|
|
3945
|
+
/* @__PURE__ */ jsx9("strong", { children: "Subtitles" }),
|
|
3946
|
+
/* @__PURE__ */ jsx9("small", { children: "Read along with the answer" })
|
|
3749
3947
|
] }),
|
|
3750
|
-
/* @__PURE__ */
|
|
3948
|
+
/* @__PURE__ */ jsx9("input", { type: "checkbox", role: "switch", checked: captionsOn, onChange: (event) => {
|
|
3751
3949
|
setCaptionsOn(event.target.checked);
|
|
3752
3950
|
setCaptionsExpanded(false);
|
|
3753
3951
|
} })
|
|
3754
3952
|
] }),
|
|
3755
3953
|
/* @__PURE__ */ jsxs7("label", { className: "switch-row", children: [
|
|
3756
3954
|
/* @__PURE__ */ jsxs7("span", { children: [
|
|
3757
|
-
/* @__PURE__ */
|
|
3758
|
-
/* @__PURE__ */
|
|
3955
|
+
/* @__PURE__ */ jsx9("strong", { children: "Keep controls visible" }),
|
|
3956
|
+
/* @__PURE__ */ jsx9("small", { children: "Keep the input bar on screen" })
|
|
3759
3957
|
] }),
|
|
3760
|
-
/* @__PURE__ */
|
|
3958
|
+
/* @__PURE__ */ jsx9("input", { type: "checkbox", role: "switch", checked: alwaysShowControls, onChange: (event) => setAlwaysShowControls(event.target.checked) })
|
|
3761
3959
|
] })
|
|
3762
3960
|
] }),
|
|
3763
3961
|
/* @__PURE__ */ jsxs7("nav", { className: "developer-links", "aria-label": "Build with VanillaSky", children: [
|
|
3764
|
-
/* @__PURE__ */
|
|
3962
|
+
/* @__PURE__ */ jsx9("p", { className: "section-label", children: "Build with VanillaSky" }),
|
|
3765
3963
|
/* @__PURE__ */ jsxs7("a", { href: "https://github.com/VanillaSkyAi/video/blob/main/docs/getting-started.md", target: "_blank", rel: "noopener noreferrer", children: [
|
|
3766
3964
|
"Docs",
|
|
3767
|
-
/* @__PURE__ */
|
|
3965
|
+
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2197" })
|
|
3768
3966
|
] }),
|
|
3769
3967
|
/* @__PURE__ */ jsxs7("button", { type: "button", "aria-expanded": aboutOpen, "aria-controls": `${instanceId}-about`, onClick: () => setAboutOpen((open) => !open), children: [
|
|
3770
3968
|
"About",
|
|
3771
|
-
/* @__PURE__ */
|
|
3969
|
+
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: aboutOpen ? "\u2212" : "+" })
|
|
3772
3970
|
] }),
|
|
3773
|
-
/* @__PURE__ */
|
|
3971
|
+
/* @__PURE__ */ jsx9("div", { id: `${instanceId}-about`, className: "developer-about", role: "region", "aria-label": "About VanillaSky", hidden: !aboutOpen, children: /* @__PURE__ */ jsx9("p", { children: "VanillaSky is an open-source SDK for conversations that answer in video. Developers connect their own AI providers through their application server." }) }),
|
|
3774
3972
|
/* @__PURE__ */ jsxs7("a", { href: "https://github.com/VanillaSkyAi/video", target: "_blank", rel: "noopener noreferrer", children: [
|
|
3775
3973
|
"GitHub",
|
|
3776
|
-
/* @__PURE__ */
|
|
3974
|
+
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2197" })
|
|
3777
3975
|
] })
|
|
3778
3976
|
] })
|
|
3779
3977
|
]
|
|
3780
3978
|
}
|
|
3781
3979
|
)
|
|
3782
3980
|
] }),
|
|
3783
|
-
/* @__PURE__ */
|
|
3784
|
-
/* @__PURE__ */
|
|
3981
|
+
/* @__PURE__ */ jsx9("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs7("div", { className: "panel-inner", children: [
|
|
3982
|
+
/* @__PURE__ */ jsx9("div", { className: "caption-slot", "data-captions": captionsOn && Boolean(line), "aria-hidden": !captionsOn || !line, children: /* @__PURE__ */ jsx9("div", { className: "caption-clip", children: chat.playbackEnded && !captionsExpanded ? /* @__PURE__ */ jsxs7("button", { type: "button", className: "transcript-toggle", "aria-expanded": false, onClick: () => setCaptionsExpanded(true), children: [
|
|
3983
|
+
"Show transcript",
|
|
3984
|
+
/* @__PURE__ */ jsx9(ChevronUp, {})
|
|
3985
|
+
] }) : /* @__PURE__ */ jsxs7(
|
|
3785
3986
|
"div",
|
|
3786
3987
|
{
|
|
3787
3988
|
className: "line-row",
|
|
@@ -3797,41 +3998,41 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3797
3998
|
captionsOn && line && /* @__PURE__ */ jsxs7("div", { className: "caption-actions", children: [
|
|
3798
3999
|
/* @__PURE__ */ jsxs7("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
|
|
3799
4000
|
captionsExpanded ? "Collapse" : "Expand",
|
|
3800
|
-
/* @__PURE__ */
|
|
4001
|
+
/* @__PURE__ */ jsx9(ChevronUp, {})
|
|
3801
4002
|
] }),
|
|
3802
|
-
/* @__PURE__ */
|
|
4003
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: "caption-action", "aria-label": "Hide subtitles", onClick: () => {
|
|
3803
4004
|
setCaptionsOn(false);
|
|
3804
4005
|
setCaptionsExpanded(false);
|
|
3805
|
-
}, children: /* @__PURE__ */
|
|
4006
|
+
}, children: /* @__PURE__ */ jsx9(Close, {}) })
|
|
3806
4007
|
] }),
|
|
3807
|
-
captionsExpanded ? /* @__PURE__ */
|
|
4008
|
+
captionsExpanded ? /* @__PURE__ */ jsx9("div", { className: "expanded-captions", role: "region", tabIndex: 0, "aria-label": "Expanded subtitles", children: fullTranscript.map((entry, index) => /* @__PURE__ */ jsx9("p", { children: entry }, index)) }) : /* @__PURE__ */ jsx9(CaptionPages, { text: openingChapter && line === shown?.opening ? "" : line, getProgress: getCaptionProgress }, `${shown?.id}:${chat.playerKey}`)
|
|
3808
4009
|
]
|
|
3809
4010
|
}
|
|
3810
4011
|
) }) }),
|
|
3811
4012
|
showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */ jsxs7("div", { className: "recovery-notice", children: [
|
|
3812
|
-
/* @__PURE__ */
|
|
3813
|
-
/* @__PURE__ */
|
|
4013
|
+
/* @__PURE__ */ jsx9("p", { role: "status", children: MEDIA_RECOVERY_NOTICE }),
|
|
4014
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: "round", "aria-label": "Dismiss notice", onClick: () => setDismissedNoticeTurn(chat.shownTurn?.id), children: /* @__PURE__ */ jsx9(Close, {}) })
|
|
3814
4015
|
] }),
|
|
3815
4016
|
(chat.error || listen.error) && /* @__PURE__ */ jsxs7("p", { className: "error", role: "status", children: [
|
|
3816
|
-
/* @__PURE__ */
|
|
3817
|
-
/* @__PURE__ */
|
|
4017
|
+
/* @__PURE__ */ jsx9(Warning, {}),
|
|
4018
|
+
/* @__PURE__ */ jsx9("span", { children: chat.error?.message ?? listen.error })
|
|
3818
4019
|
] }),
|
|
3819
4020
|
/* @__PURE__ */ jsxs7("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
|
|
3820
|
-
(listen.listening || listen.thinking) && /* @__PURE__ */
|
|
4021
|
+
(listen.listening || listen.thinking) && /* @__PURE__ */ jsx9("div", { className: "composer-meta", children: /* @__PURE__ */ jsx9("span", { role: "status", children: listen.listening ? "Listening\u2026 Tap the mic to finish, then review and send." : "Turning your words into a draft\u2026" }) }),
|
|
3821
4022
|
/* @__PURE__ */ jsxs7("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
|
|
3822
4023
|
event.preventDefault();
|
|
3823
4024
|
ask(draft);
|
|
3824
4025
|
}, children: [
|
|
3825
|
-
transport && /* @__PURE__ */
|
|
4026
|
+
transport && /* @__PURE__ */ jsx9("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: () => {
|
|
3826
4027
|
listen.stop();
|
|
3827
4028
|
setEditing(false);
|
|
3828
4029
|
resumeAfterInput.current = false;
|
|
3829
4030
|
inputRef.current?.blur();
|
|
3830
4031
|
transport.action();
|
|
3831
4032
|
}, children: transport.icon }),
|
|
3832
|
-
/* @__PURE__ */
|
|
3833
|
-
/* @__PURE__ */
|
|
3834
|
-
/* @__PURE__ */
|
|
4033
|
+
/* @__PURE__ */ jsx9(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
|
|
4034
|
+
/* @__PURE__ */ jsx9("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
|
|
4035
|
+
/* @__PURE__ */ jsx9(
|
|
3835
4036
|
"textarea",
|
|
3836
4037
|
{
|
|
3837
4038
|
id: promptId,
|
|
@@ -3859,12 +4060,12 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3859
4060
|
}
|
|
3860
4061
|
}
|
|
3861
4062
|
),
|
|
3862
|
-
listen.supported && /* @__PURE__ */
|
|
3863
|
-
/* @__PURE__ */
|
|
3864
|
-
editing && /* @__PURE__ */
|
|
4063
|
+
listen.supported && /* @__PURE__ */ jsx9("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__ */ jsx9(Mic, {}) }),
|
|
4064
|
+
/* @__PURE__ */ jsx9("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || listen.thinking, children: /* @__PURE__ */ jsx9(Send, {}) }),
|
|
4065
|
+
editing && /* @__PURE__ */ jsx9("button", { type: "button", className: "ghost", "aria-label": "Cancel question", onClick: cancelInput, children: /* @__PURE__ */ jsx9(Close, {}) })
|
|
3865
4066
|
] })
|
|
3866
4067
|
] }),
|
|
3867
|
-
!showing && chat.turns.length === 0 && /* @__PURE__ */
|
|
4068
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx9("p", { className: "dock-hint", children: "Speak or type. See where it takes you." })
|
|
3868
4069
|
] }) })
|
|
3869
4070
|
]
|
|
3870
4071
|
}
|
|
@@ -116,3 +116,13 @@ Grouped playback requires prepared speech with `supportsOffsets: true`. The gene
|
|
|
116
116
|
For standalone playback, provide a synchronous `narrationReady` callback alongside your `onSceneChange` narration handler. Return false while a new grouped paragraph awaits actual audio onset, then true from the voice's `onStart` callback; also release readiness on completion, failure, or interruption. Abort pending narration from the player's `onError` handler. `VideoChat` wires this automatically through its internal narration hook. For grouped paragraphs, the voice must invoke `onStart` when audio actually begins, not when audio is prepared or `play()` is requested. The first visual cue starts narration, then the playhead waits for that onset without pausing the voice. A missing onset stops the player with an error after eight seconds of active waiting. For prepared audio, also provide `narrationTime(scene)` using the active voice's optional `getCurrentTime()`: return paragraph-relative seconds (including the seek offset) for a group, or scene-relative seconds otherwise. This makes the actual audio clock authoritative through cold-start delays and mid-speech stalls. Return `undefined` for silent scenes or unavailable clocks; after ordinary narration completes, release to wall time so the authored reading hold can finish. `VideoChat` coordinates these callbacks automatically. A clock that stops advancing for eight active seconds produces an error; visual-readiness holds and deliberate pauses do not consume that timeout. Voices without an observable playback clock retain wall-time playback.
|
|
117
117
|
|
|
118
118
|
Readiness holds pause narration together with the picture. The same audio continues across adjacent group scenes; replay starts a new playback session. This preserves words through delayed media rather than promising uninterrupted playback on every network.
|
|
119
|
+
|
|
120
|
+
### Host-resolved chat footage mode
|
|
121
|
+
|
|
122
|
+
A chat host that changes the requested footage mode before planning may return
|
|
123
|
+
`x-vanillasky-resolved-video-mode: pexels` or `cinematic` on a successful SSE
|
|
124
|
+
response. The default chat client records that mode for the active turn and its
|
|
125
|
+
playback metrics. Unknown values are ignored. Authorization, allowance checks
|
|
126
|
+
and provider selection remain host-owned; the header never grants access or
|
|
127
|
+
changes a spending limit. A mixed response that changes footage source partway
|
|
128
|
+
through retains its initially resolved mode.
|
package/package.json
CHANGED
|
@@ -53,53 +53,59 @@ 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:
|
|
56
|
+
const key = JSON.stringify({version: 5, 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
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
let selected: StockVideo | null = null;
|
|
62
|
+
// Reuse the caller's deadline signal; broadening never starts a new timeout.
|
|
63
|
+
const queries = [...new Set([normalized, ...(selection ? [selection.subject] : [])])];
|
|
64
|
+
for (const searchQuery of queries) {
|
|
65
|
+
signal.throwIfAborted();
|
|
66
|
+
const url = new URL("https://api.pexels.com/v1/videos/search");
|
|
67
|
+
url.search = new URLSearchParams({ query: searchQuery, per_page: "12", size: "medium" }).toString();
|
|
68
|
+
const response = await fetch(url, { headers: { Authorization: apiKey }, signal });
|
|
69
|
+
signal.throwIfAborted();
|
|
70
|
+
if (!response.ok) return null;
|
|
71
|
+
const result = await response.json() as { videos?: PexelsVideo[] };
|
|
72
|
+
signal.throwIfAborted();
|
|
73
|
+
let bestScore = -1, bestOrientation = -1;
|
|
74
|
+
const matchesOrientation = (file: {width?: number; height?: number}) => orientation === "portrait"
|
|
75
|
+
? file.height! > file.width! : file.width! >= file.height!;
|
|
76
|
+
for (const video of (Array.isArray(result.videos) ? result.videos : []).slice(0, 12)) {
|
|
77
|
+
if (!pexelsUrl(video.url)) continue;
|
|
78
|
+
const slug = new URL(video.url).pathname.replace(/^\/video\//, "");
|
|
79
|
+
const title = typeof video.title === "string" ? video.title : "";
|
|
80
|
+
const tags = Array.isArray(video.tags) ? video.tags.filter((tag): tag is string => typeof tag === "string").join(" ") : "";
|
|
81
|
+
const subject = words(`${slug} ${title} ${typeof video.description === "string" ? video.description : ""} ${tags}`).filter(token => !/^\d+$/.test(token)).map(wordForm);
|
|
82
|
+
let matches = tokens.filter(token => subject.includes(wordForm(token))).length;
|
|
83
|
+
if (selection && subject.length) {
|
|
84
|
+
const covers = (phrase: string) => terms(phrase).every(word => subject.includes(wordForm(word)));
|
|
85
|
+
if (selection.exclude?.some(covers)) continue;
|
|
86
|
+
// Query context breaks equal hint matches without outweighing a hint.
|
|
87
|
+
const contextScore = matches / (tokens.length + 1);
|
|
88
|
+
matches = covers(selection.subject) ? 2 + contextScore + Number(Boolean(selection.activity && covers(selection.activity)))
|
|
89
|
+
+ Number(Boolean(selection.equipment && covers(selection.equipment))) : 0;
|
|
90
|
+
}
|
|
91
|
+
// The documented Video resource can have only a numeric page URL and no
|
|
92
|
+
// editorial metadata. Preserve provider search order for unknown relevance;
|
|
93
|
+
// positive overlap ranks above provider-ranked illustrative alternatives.
|
|
94
|
+
const files = (Array.isArray(video.video_files) ? video.video_files : []).filter(file =>
|
|
95
|
+
file.file_type === "video/mp4" && pexelsUrl(file.link)
|
|
96
|
+
&& Number.isFinite(file.width) && Number.isFinite(file.height)
|
|
97
|
+
&& Math.min(file.width!, file.height!) >= 360,
|
|
98
|
+
).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));
|
|
99
|
+
const file = files[0];
|
|
100
|
+
if (!file) continue;
|
|
101
|
+
const orientationScore = Number(matchesOrientation(file));
|
|
102
|
+
// Prefer composition fit only when subject relevance is equal.
|
|
103
|
+
if (matches < bestScore || (matches === bestScore && orientationScore <= bestOrientation)) continue;
|
|
104
|
+
bestScore = matches;
|
|
105
|
+
bestOrientation = orientationScore;
|
|
106
|
+
selected = { url: file.link!, type: "video", ...(pexelsUrl(video.image) ? { posterUrl: video.image } : {}) };
|
|
85
107
|
}
|
|
86
|
-
|
|
87
|
-
// editorial metadata. Preserve provider search order for unknown relevance;
|
|
88
|
-
// positive overlap ranks above it, while explicitly unrelated copy is skipped.
|
|
89
|
-
if (subject.length > 0 && matches === 0) continue;
|
|
90
|
-
const files = (Array.isArray(video.video_files) ? video.video_files : []).filter(file =>
|
|
91
|
-
file.file_type === "video/mp4" && pexelsUrl(file.link)
|
|
92
|
-
&& Number.isFinite(file.width) && Number.isFinite(file.height)
|
|
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));
|
|
95
|
-
const file = files[0];
|
|
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;
|
|
100
|
-
bestScore = matches;
|
|
101
|
-
bestOrientation = orientationScore;
|
|
102
|
-
selected = { url: file.link!, type: "video", ...(pexelsUrl(video.image) ? { posterUrl: video.image } : {}) };
|
|
108
|
+
if (selected) break;
|
|
103
109
|
}
|
|
104
110
|
// Bounded process-local cache; no request signal or credentials are retained.
|
|
105
111
|
if (cache.size >= 128) cache.delete(cache.keys().next().value!);
|
package/styles/video-chat.css
CHANGED
|
@@ -1065,7 +1065,6 @@
|
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
1067
|
.vanillasky-video-chat .opening-chapter { animation: vanillasky-chapter-enter 450ms ease-out both; }
|
|
1068
|
-
.vanillasky-video-chat .media-credit { color: var(--vs-fg); font-size: 11px; margin-left: 12px; text-underline-offset: 3px; }
|
|
1069
1068
|
@keyframes vanillasky-chapter-enter { from { opacity: 0; } to { opacity: 1; } }
|
|
1070
1069
|
@media (prefers-reduced-motion: reduce) { .vanillasky-video-chat .opening-chapter { animation: none; } }
|
|
1071
1070
|
|
|
@@ -1124,3 +1123,7 @@
|
|
|
1124
1123
|
.vanillasky-video-chat .card-prompt { position: relative; inset: auto; overflow-wrap: anywhere; flex-shrink: 0; }
|
|
1125
1124
|
|
|
1126
1125
|
.vanillasky-video-chat .cards { flex-shrink: 0; }
|
|
1126
|
+
|
|
1127
|
+
.vanillasky-video-chat .transcript-toggle { display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 8px 12px; border: 0; border-radius: 10px; background: var(--vs-media-glass); color: var(--vs-media-muted); font: inherit; font-size: 13px; cursor: pointer; pointer-events: auto; }
|
|
1128
|
+
.vanillasky-video-chat .transcript-toggle svg { width: 16px; height: 16px; }
|
|
1129
|
+
.vanillasky-video-chat .transcript-toggle:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
|