@vanillaskyai/video 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -0
- package/PUBLIC-API.md +123 -9
- package/README.md +69 -115
- package/dist/{bg-confetti-SHQI7ATB.js → bg-confetti-HITCGLGD.js} +1 -1
- package/dist/{bg-emoji-XYFRA63Q.js → bg-emoji-VU4UZYZN.js} +1 -1
- package/dist/{bg-media-G4XOQEAS.js → bg-media-L34PDQXJ.js} +2 -2
- package/dist/{brand-message-XHONEDNW.js → brand-message-FUJ4STFO.js} +1 -1
- package/dist/{builtin-server-YHEZ2JRF.js → builtin-server-KB7FKF6A.js} +2 -2
- package/dist/{chart-bar-HRHD57ML.js → chart-bar-EGPHJATL.js} +2 -2
- package/dist/{chart-counter-KFPV44RB.js → chart-counter-5FFWQNZO.js} +2 -2
- package/dist/{chart-progress-ring-OR2VOIHV.js → chart-progress-ring-JMXQLMLC.js} +2 -2
- package/dist/{chunk-66MNRUCR.js → chunk-2PYO6VAC.js} +51 -3
- package/dist/{chunk-ZQQQAAKP.js → chunk-2XT4MZ76.js} +33 -2
- package/dist/{chunk-PNS52FL4.js → chunk-5SLENAJW.js} +19 -6
- package/dist/{chunk-EFL34TXF.js → chunk-AITKH6QT.js} +24 -8
- package/dist/{chunk-FZHMQFG3.js → chunk-GAIOHGNR.js} +5 -21
- package/dist/{chunk-3RV4YKB3.js → chunk-JNN3EYVP.js} +51 -26
- package/dist/{chunk-HVCPEAQF.js → chunk-LKBZX7GV.js} +9 -6
- package/dist/{chunk-34O5BY6X.js → chunk-LLPMVDJZ.js} +21 -4
- package/dist/{chunk-G66Z5CWR.js → chunk-MMUXVA47.js} +27 -3
- package/dist/{chunk-7BGAU6C3.js → chunk-MXZSDGZQ.js} +2 -1
- package/dist/{chunk-3OU7HIEB.js → chunk-RE4IMWJR.js} +15 -31
- package/dist/{chunk-FVTMYS6U.js → chunk-RGF452LL.js} +1 -1
- package/dist/chunk-SKRGRKHY.js +142 -0
- package/dist/{chunk-TCEHK2BW.js → chunk-YJJC4N4D.js} +2 -1
- package/dist/cli.js +438 -30
- package/dist/{compose-video-JWIEDTEJ.js → compose-video-PD6LKKRK.js} +4 -4
- package/dist/{cta-logo-JJOGK4UC.js → cta-logo-YR7LYUR5.js} +1 -1
- package/dist/{cta-media-IQ3MAWEH.js → cta-media-DQJBKUO2.js} +2 -2
- package/dist/{events-BBP30j3c.d.ts → events-B6qS1Lsb.d.ts} +2 -2
- package/dist/{incoming-call-KXF67OT2.js → incoming-call-54KQ7O5A.js} +1 -1
- package/dist/index.d.ts +89 -3
- package/dist/index.js +12 -1
- package/dist/{infographic-before-after-2I7YZVDD.js → infographic-before-after-ROS52GOW.js} +1 -1
- package/dist/{infographic-feature-list-QDM4I7XE.js → infographic-feature-list-E7MGUDAN.js} +2 -2
- package/dist/{infographic-problem-solution-FMLJCPG6.js → infographic-problem-solution-3S6NGO5N.js} +2 -2
- package/dist/{infographic-stat-row-VFTIXS5F.js → infographic-stat-row-SYB7AB7H.js} +2 -2
- package/dist/{infographic-steps-L7W24Z35.js → infographic-steps-KCE6JWE2.js} +2 -2
- package/dist/{kit-DEG3fcaL.d.ts → kit-8g46H2RZ.d.ts} +1 -1
- package/dist/{prompt-input-F3CYU2XH.js → prompt-input-MHX4O42G.js} +1 -1
- package/dist/react.d.ts +194 -6
- package/dist/react.js +1849 -40
- package/dist/{reaction-2XCLOJOK.js → reaction-SLEGR3BE.js} +2 -2
- package/dist/server.d.ts +102 -4
- package/dist/server.js +972 -27
- package/dist/{showcase-code-KHSMIPG6.js → showcase-code-2HLAVKBM.js} +2 -2
- package/dist/{showcase-phone-M4CEANIW.js → showcase-phone-Q4HEO4LH.js} +2 -2
- package/dist/{showcase-terminal-26I3DYF7.js → showcase-terminal-DL45LUFG.js} +2 -2
- package/dist/{showcase-web-TY6CZV2Z.js → showcase-web-2QZO73FZ.js} +2 -2
- package/dist/{social-milestone-TF6MX7ET.js → social-milestone-GEFUQSBF.js} +1 -1
- package/dist/{social-notification-2RJDAZSF.js → social-notification-FJ5VHVLK.js} +1 -1
- package/dist/{social-review-stack-2ICKJQG7.js → social-review-stack-KXQCKFRP.js} +1 -1
- package/dist/{social-testimonial-X7OGADA3.js → social-testimonial-JZ7CHC7S.js} +1 -1
- package/dist/{social-tweet-TK7SH2F6.js → social-tweet-UCDXM25F.js} +1 -1
- package/dist/{system-prompt-4I6Z5HK3.js → system-prompt-RRXIWDDD.js} +5 -3
- package/dist/template-catalog.js +1 -1
- package/dist/templates.d.ts +3 -3
- package/dist/test.d.ts +2 -2
- package/dist/test.js +3 -3
- package/dist/{text-stream-J6EDDJP4.js → text-stream-ROTNGSD4.js} +2 -2
- package/dist/{types-BV9IqExh.d.ts → types-2wHBqtg8.d.ts} +29 -1
- package/dist/types-DrABlRa7.d.ts +46 -0
- package/docs/agent-integration.md +49 -25
- package/docs/architecture.md +37 -22
- package/docs/branding-and-personalization.md +1 -1
- package/docs/concepts.md +14 -0
- package/docs/custom-templates.md +9 -9
- package/docs/customization.md +41 -5
- package/docs/getting-started.md +81 -84
- package/docs/media-and-audio.md +103 -166
- package/docs/persistence.md +1 -1
- package/docs/production.md +81 -92
- package/docs/prompt-and-input.md +71 -161
- package/docs/provider-integration.md +91 -49
- package/docs/responsive-orientation.md +1 -1
- package/docs/security.md +6 -5
- package/docs/streaming-protocol.md +1 -1
- package/docs/testing.md +51 -43
- package/examples/custom-template/README.md +1 -1
- package/package.json +20 -22
- package/registry/items/barChart.json +6 -4
- package/registry/items/beforeAfter.json +1 -1
- package/registry/items/bigNumber.json +4 -3
- package/registry/items/brandMessage.json +3 -2
- package/registry/items/cardList.json +6 -4
- package/registry/items/codeEditor.json +4 -3
- package/registry/items/confetti.json +1 -1
- package/registry/items/ctaLogo.json +3 -2
- package/registry/items/ctaMedia.json +4 -3
- package/registry/items/emojiBurst.json +1 -1
- package/registry/items/incomingCall.json +3 -2
- package/registry/items/media.json +4 -3
- package/registry/items/milestone.json +3 -2
- package/registry/items/notification.json +3 -2
- package/registry/items/phoneMockup.json +4 -3
- package/registry/items/problemSolution.json +4 -3
- package/registry/items/progressRing.json +4 -3
- package/registry/items/promptInput.json +3 -2
- package/registry/items/reaction.json +2 -2
- package/registry/items/reviewStack.json +3 -2
- package/registry/items/steps.json +6 -4
- package/registry/items/terminal.json +5 -4
- package/registry/items/testimonial.json +3 -2
- package/registry/items/tripleStats.json +4 -3
- package/registry/items/tweet.json +3 -2
- package/registry/items/webMockup.json +4 -3
- package/starters/video-chat/.env.example +16 -0
- package/starters/video-chat/README.md +63 -0
- package/starters/video-chat/index.html +12 -0
- package/starters/video-chat/package.json +28 -0
- package/starters/video-chat/server.ts +152 -0
- package/starters/video-chat/src/main.tsx +8 -0
- package/starters/video-chat/stock.ts +139 -0
- package/starters/video-chat/tsconfig.json +20 -0
- package/starters/video-chat/vite.config.ts +81 -0
- package/styles/video-chat.css +713 -0
- package/docs/input-and-first-scene.md +0 -64
- package/docs/integrate-nextjs.md +0 -86
- package/docs/live-channels.md +0 -149
- package/docs/use-cases.md +0 -59
- package/examples/nextjs-quickstart/.env.example +0 -2
- package/examples/nextjs-quickstart/README.md +0 -27
- package/examples/nextjs-quickstart/next-env.d.ts +0 -4
- package/examples/nextjs-quickstart/package.json +0 -25
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +0 -33
- package/examples/nextjs-quickstart/src/app/layout.tsx +0 -5
- package/examples/nextjs-quickstart/src/app/page.tsx +0 -31
- package/examples/nextjs-quickstart/tsconfig.json +0 -26
- package/examples/server-integrations/README.md +0 -20
- package/examples/server-integrations/src/ai-sdk-media.ts +0 -90
package/dist/react.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSceneTimeline,
|
|
3
|
+
getSceneDuration
|
|
4
|
+
} from "./chunk-SKRGRKHY.js";
|
|
1
5
|
import {
|
|
2
6
|
VIDEO_STREAM_CONTENT_TYPE,
|
|
3
7
|
VIDEO_STREAM_HEADER,
|
|
4
|
-
decodeVideoSse
|
|
5
|
-
|
|
8
|
+
decodeVideoSse,
|
|
9
|
+
sanitizeVideoChatMedia
|
|
10
|
+
} from "./chunk-2XT4MZ76.js";
|
|
6
11
|
import {
|
|
7
12
|
applyVideoEvent,
|
|
8
13
|
createReplayableStream,
|
|
9
14
|
createVideoState
|
|
10
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-GAIOHGNR.js";
|
|
11
16
|
import {
|
|
12
17
|
safePublicDiagnostic
|
|
13
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MMUXVA47.js";
|
|
14
19
|
import {
|
|
15
20
|
parseVideo
|
|
16
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-2PYO6VAC.js";
|
|
17
22
|
import {
|
|
18
23
|
VIDEO_PROTOCOL_VERSION,
|
|
19
24
|
createVideoRequest
|
|
20
25
|
} from "./chunk-AOWSXU2K.js";
|
|
26
|
+
import {
|
|
27
|
+
getBuiltinTemplateMetadata
|
|
28
|
+
} from "./chunk-JNN3EYVP.js";
|
|
29
|
+
import "./chunk-3O7OMMMF.js";
|
|
21
30
|
import {
|
|
22
31
|
assertTemplateTransitionMetadata
|
|
23
32
|
} from "./chunk-2E6T633S.js";
|
|
@@ -79,34 +88,34 @@ import { createElement } from "react";
|
|
|
79
88
|
|
|
80
89
|
// src/visual-system/catalog/builtin-loaders.generated.ts
|
|
81
90
|
var GENERATED_BUILTIN_TEMPLATE_LOADERS = {
|
|
82
|
-
"media": () => import("./bg-media-
|
|
83
|
-
"reaction": () => import("./reaction-
|
|
84
|
-
"confetti": () => import("./bg-confetti-
|
|
85
|
-
"emojiBurst": () => import("./bg-emoji-
|
|
86
|
-
"bigNumber": () => import("./chart-counter-
|
|
87
|
-
"barChart": () => import("./chart-bar-
|
|
88
|
-
"progressRing": () => import("./chart-progress-ring-
|
|
89
|
-
"phoneMockup": () => import("./showcase-phone-
|
|
90
|
-
"webMockup": () => import("./showcase-web-
|
|
91
|
-
"codeEditor": () => import("./showcase-code-
|
|
92
|
-
"terminal": () => import("./showcase-terminal-
|
|
93
|
-
"tweet": () => import("./social-tweet-
|
|
94
|
-
"notification": () => import("./social-notification-
|
|
91
|
+
"media": () => import("./bg-media-L34PDQXJ.js").then((module) => ({ default: module.BgMediaTemplate })),
|
|
92
|
+
"reaction": () => import("./reaction-SLEGR3BE.js").then((module) => ({ default: module.ReactionTemplate })),
|
|
93
|
+
"confetti": () => import("./bg-confetti-HITCGLGD.js").then((module) => ({ default: module.BgConfettiTemplate })),
|
|
94
|
+
"emojiBurst": () => import("./bg-emoji-VU4UZYZN.js").then((module) => ({ default: module.BgEmojiTemplate })),
|
|
95
|
+
"bigNumber": () => import("./chart-counter-5FFWQNZO.js").then((module) => ({ default: module.ChartCounterTemplate })),
|
|
96
|
+
"barChart": () => import("./chart-bar-EGPHJATL.js").then((module) => ({ default: module.ChartBarTemplate })),
|
|
97
|
+
"progressRing": () => import("./chart-progress-ring-JMXQLMLC.js").then((module) => ({ default: module.ChartProgressRingTemplate })),
|
|
98
|
+
"phoneMockup": () => import("./showcase-phone-Q4HEO4LH.js").then((module) => ({ default: module.ShowcasePhoneTemplate })),
|
|
99
|
+
"webMockup": () => import("./showcase-web-2QZO73FZ.js").then((module) => ({ default: module.ShowcaseWebTemplate })),
|
|
100
|
+
"codeEditor": () => import("./showcase-code-2HLAVKBM.js").then((module) => ({ default: module.ShowcaseCodeTemplate })),
|
|
101
|
+
"terminal": () => import("./showcase-terminal-DL45LUFG.js").then((module) => ({ default: module.ShowcaseTerminalTemplate })),
|
|
102
|
+
"tweet": () => import("./social-tweet-UCDXM25F.js").then((module) => ({ default: module.SocialTweetTemplate })),
|
|
103
|
+
"notification": () => import("./social-notification-FJ5VHVLK.js").then((module) => ({ default: module.SocialNotificationTemplate })),
|
|
95
104
|
"chatMessenger": () => import("./social-conversation-75SD2637.js").then((module) => ({ default: module.SocialConversationTemplate })),
|
|
96
105
|
"chatWhatsapp": () => import("./social-conversation-75SD2637.js").then((module) => ({ default: module.SocialConversationTemplate })),
|
|
97
|
-
"milestone": () => import("./social-milestone-
|
|
98
|
-
"reviewStack": () => import("./social-review-stack-
|
|
99
|
-
"testimonial": () => import("./social-testimonial-
|
|
100
|
-
"incomingCall": () => import("./incoming-call-
|
|
101
|
-
"brandMessage": () => import("./brand-message-
|
|
102
|
-
"promptInput": () => import("./prompt-input-
|
|
103
|
-
"beforeAfter": () => import("./infographic-before-after-
|
|
104
|
-
"tripleStats": () => import("./infographic-stat-row-
|
|
105
|
-
"problemSolution": () => import("./infographic-problem-solution-
|
|
106
|
-
"cardList": () => import("./infographic-feature-list-
|
|
107
|
-
"steps": () => import("./infographic-steps-
|
|
108
|
-
"ctaLogo": () => import("./cta-logo-
|
|
109
|
-
"ctaMedia": () => import("./cta-media-
|
|
106
|
+
"milestone": () => import("./social-milestone-GEFUQSBF.js").then((module) => ({ default: module.SocialMilestoneTemplate })),
|
|
107
|
+
"reviewStack": () => import("./social-review-stack-KXQCKFRP.js").then((module) => ({ default: module.SocialReviewStackTemplate })),
|
|
108
|
+
"testimonial": () => import("./social-testimonial-JZ7CHC7S.js").then((module) => ({ default: module.SocialTestimonialTemplate })),
|
|
109
|
+
"incomingCall": () => import("./incoming-call-54KQ7O5A.js").then((module) => ({ default: module.IncomingCallTemplate })),
|
|
110
|
+
"brandMessage": () => import("./brand-message-FUJ4STFO.js").then((module) => ({ default: module.BrandMessageTemplate })),
|
|
111
|
+
"promptInput": () => import("./prompt-input-MHX4O42G.js").then((module) => ({ default: module.PromptInputTemplate })),
|
|
112
|
+
"beforeAfter": () => import("./infographic-before-after-ROS52GOW.js").then((module) => ({ default: module.InfographicBeforeAfterTemplate })),
|
|
113
|
+
"tripleStats": () => import("./infographic-stat-row-SYB7AB7H.js").then((module) => ({ default: module.InfographicStatRowTemplate })),
|
|
114
|
+
"problemSolution": () => import("./infographic-problem-solution-3S6NGO5N.js").then((module) => ({ default: module.InfographicProblemSolutionTemplate })),
|
|
115
|
+
"cardList": () => import("./infographic-feature-list-E7MGUDAN.js").then((module) => ({ default: module.InfographicFeatureListTemplate })),
|
|
116
|
+
"steps": () => import("./infographic-steps-KCE6JWE2.js").then((module) => ({ default: module.InfographicStepsTemplate })),
|
|
117
|
+
"ctaLogo": () => import("./cta-logo-YR7LYUR5.js").then((module) => ({ default: module.CtaLogoTemplate })),
|
|
118
|
+
"ctaMedia": () => import("./cta-media-DQJBKUO2.js").then((module) => ({ default: module.CtaMediaTemplate }))
|
|
110
119
|
};
|
|
111
120
|
|
|
112
121
|
// src/visual-system/catalog/builtin-player.generated.ts
|
|
@@ -322,7 +331,7 @@ var GENERATED_BUILTIN_PLAYER_TEMPLATES = [
|
|
|
322
331
|
"id": "terminal",
|
|
323
332
|
"defaults": {
|
|
324
333
|
"texts": "Ship it.",
|
|
325
|
-
"command": "npx vanillasky check",
|
|
334
|
+
"command": "npx vanillasky templates check",
|
|
326
335
|
"output": [],
|
|
327
336
|
"promptPrefix": "$",
|
|
328
337
|
"mediaUrl": "",
|
|
@@ -1090,7 +1099,10 @@ function VideoPlayerRuntime({
|
|
|
1090
1099
|
className,
|
|
1091
1100
|
style,
|
|
1092
1101
|
ariaLabel = "Video response",
|
|
1102
|
+
controls = true,
|
|
1103
|
+
paused,
|
|
1093
1104
|
loop = false,
|
|
1105
|
+
onPlaybackEnd,
|
|
1094
1106
|
onComplete,
|
|
1095
1107
|
onError,
|
|
1096
1108
|
onSceneChange,
|
|
@@ -1128,12 +1140,13 @@ function VideoPlayerRuntime({
|
|
|
1128
1140
|
const timeRef = useRef(currentTime);
|
|
1129
1141
|
const audioRef = useRef(null);
|
|
1130
1142
|
const introStartedAtRef = useRef(autoStartGeneration ? performance.now() : null);
|
|
1131
|
-
const callbacksRef = useRef({ onComplete, onError, onSceneChange, onStateChange });
|
|
1143
|
+
const callbacksRef = useRef({ onComplete, onPlaybackEnd, onError, onSceneChange, onStateChange });
|
|
1132
1144
|
const loopRef = useRef(loop);
|
|
1133
1145
|
const sceneIndexRef = useRef(-1);
|
|
1146
|
+
const playbackEndedRef = useRef(false);
|
|
1134
1147
|
stateRef.current = state;
|
|
1135
1148
|
timeRef.current = currentTime;
|
|
1136
|
-
callbacksRef.current = { onComplete, onError, onSceneChange, onStateChange };
|
|
1149
|
+
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, onStateChange };
|
|
1137
1150
|
loopRef.current = loop;
|
|
1138
1151
|
const primeSoundtrack = () => {
|
|
1139
1152
|
const audio = audioRef.current;
|
|
@@ -1183,6 +1196,23 @@ function VideoPlayerRuntime({
|
|
|
1183
1196
|
import("./control-visibility-NQOWTKOQ.js");
|
|
1184
1197
|
}, []);
|
|
1185
1198
|
useEffect2(() => setIsMuted(resolvedStartMuted), [resolvedStartMuted]);
|
|
1199
|
+
const pausedRef = useRef(paused);
|
|
1200
|
+
useEffect2(() => {
|
|
1201
|
+
const previous = pausedRef.current;
|
|
1202
|
+
pausedRef.current = paused;
|
|
1203
|
+
if (paused === void 0 || paused === previous) return;
|
|
1204
|
+
if (paused) {
|
|
1205
|
+
setIsPlaying(false);
|
|
1206
|
+
audioRef.current?.pause();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const config2 = stateRef.current.config;
|
|
1210
|
+
if (!config2?.scenes.length) return;
|
|
1211
|
+
if (timeRef.current >= getVideoDuration(config2) - 1e-3) return;
|
|
1212
|
+
setIsPlaying(true);
|
|
1213
|
+
const audio = audioRef.current;
|
|
1214
|
+
if (audio?.paused) void audio.play().catch(Boolean);
|
|
1215
|
+
}, [paused]);
|
|
1186
1216
|
useEffect2(() => {
|
|
1187
1217
|
let cancelled = false;
|
|
1188
1218
|
let started = false;
|
|
@@ -1336,7 +1366,17 @@ function VideoPlayerRuntime({
|
|
|
1336
1366
|
const displayConfig = config && config.orientation !== orientation ? { ...config, orientation } : config;
|
|
1337
1367
|
const duration = config ? getVideoDuration(config) : 0;
|
|
1338
1368
|
const terminal = state.status === "complete" || state.status === "error" || state.status === "aborted";
|
|
1339
|
-
const
|
|
1369
|
+
const playheadAtEnd = terminal && duration > 0 && currentTime >= duration - 1e-3;
|
|
1370
|
+
const ended = !loop && playheadAtEnd;
|
|
1371
|
+
useEffect2(() => {
|
|
1372
|
+
if (!playheadAtEnd) {
|
|
1373
|
+
playbackEndedRef.current = false;
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (loop || playbackEndedRef.current) return;
|
|
1377
|
+
playbackEndedRef.current = true;
|
|
1378
|
+
callbacksRef.current.onPlaybackEnd?.(stateRef.current);
|
|
1379
|
+
}, [loop, playheadAtEnd]);
|
|
1340
1380
|
const generationIntroWaiting = Boolean(playbackMode && stream && !generationIntroComplete);
|
|
1341
1381
|
const firstSceneRange = config ? resolveVideoTimeline(config)[0] : void 0;
|
|
1342
1382
|
const hasSuppliedOpening = firstSceneRange?.scene.id === "supplied-opening";
|
|
@@ -1497,11 +1537,11 @@ function VideoPlayerRuntime({
|
|
|
1497
1537
|
},
|
|
1498
1538
|
config.audio.audioUrl
|
|
1499
1539
|
) : null,
|
|
1500
|
-
/* @__PURE__ */ jsx2(EndedOverlay, { ended, onReplay: togglePlayback }),
|
|
1540
|
+
/* @__PURE__ */ jsx2(EndedOverlay, { ended: controls && ended, onReplay: togglePlayback }),
|
|
1501
1541
|
/* @__PURE__ */ jsx2(
|
|
1502
1542
|
PlayerControls,
|
|
1503
1543
|
{
|
|
1504
|
-
visible: !generationCoverVisible && !showStartPoster && Boolean(config?.scenes.length),
|
|
1544
|
+
visible: controls && !generationCoverVisible && !showStartPoster && Boolean(config?.scenes.length),
|
|
1505
1545
|
displayWidth,
|
|
1506
1546
|
ended,
|
|
1507
1547
|
isPlaying,
|
|
@@ -1524,6 +1564,7 @@ function VideoPlayer({
|
|
|
1524
1564
|
templates,
|
|
1525
1565
|
stream,
|
|
1526
1566
|
video,
|
|
1567
|
+
onPlaybackEnd,
|
|
1527
1568
|
onComplete,
|
|
1528
1569
|
onError,
|
|
1529
1570
|
...props
|
|
@@ -1541,6 +1582,9 @@ function VideoPlayer({
|
|
|
1541
1582
|
kit,
|
|
1542
1583
|
stream,
|
|
1543
1584
|
video: savedVideo,
|
|
1585
|
+
onPlaybackEnd: (state) => {
|
|
1586
|
+
if (state.config) onPlaybackEnd?.(state.config);
|
|
1587
|
+
},
|
|
1544
1588
|
onComplete: (state) => {
|
|
1545
1589
|
if (state.config) onComplete?.(state.config);
|
|
1546
1590
|
},
|
|
@@ -1767,8 +1811,8 @@ function useVideo(options = {}) {
|
|
|
1767
1811
|
generation.current = currentGeneration;
|
|
1768
1812
|
activeRun.current?.abort("replaced by a new composition");
|
|
1769
1813
|
const currentOptions = optionsRef.current;
|
|
1770
|
-
const
|
|
1771
|
-
setState({ ...
|
|
1814
|
+
const initialState2 = createVideoState();
|
|
1815
|
+
setState({ ...initialState2, status: "streaming" });
|
|
1772
1816
|
setError(void 0);
|
|
1773
1817
|
const nextRun = streamVideo({
|
|
1774
1818
|
endpoint: currentOptions.endpoint ?? "/api/video",
|
|
@@ -1833,8 +1877,1773 @@ function useVideo(options = {}) {
|
|
|
1833
1877
|
}
|
|
1834
1878
|
};
|
|
1835
1879
|
}
|
|
1880
|
+
|
|
1881
|
+
// src/player/use-narration.ts
|
|
1882
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
|
|
1883
|
+
function useNarration(options) {
|
|
1884
|
+
const [speaking, setSpeaking] = useState3(false);
|
|
1885
|
+
const optionsRef = useRef3(options);
|
|
1886
|
+
optionsRef.current = options;
|
|
1887
|
+
const currentRef = useRef3(void 0);
|
|
1888
|
+
const spokenIndexRef = useRef3(void 0);
|
|
1889
|
+
const stop = useCallback2(() => {
|
|
1890
|
+
currentRef.current?.abort();
|
|
1891
|
+
currentRef.current = void 0;
|
|
1892
|
+
setSpeaking(false);
|
|
1893
|
+
}, []);
|
|
1894
|
+
const interrupt = useCallback2(() => {
|
|
1895
|
+
spokenIndexRef.current = void 0;
|
|
1896
|
+
stop();
|
|
1897
|
+
}, [stop]);
|
|
1898
|
+
useEffect4(() => () => currentRef.current?.abort(), []);
|
|
1899
|
+
const onSceneChange = useCallback2((scene, index) => {
|
|
1900
|
+
const { voice, enabled = true } = optionsRef.current;
|
|
1901
|
+
if (!enabled) return;
|
|
1902
|
+
if (spokenIndexRef.current === index) return;
|
|
1903
|
+
stop();
|
|
1904
|
+
spokenIndexRef.current = index;
|
|
1905
|
+
const line = scene.narration?.trim();
|
|
1906
|
+
if (!line) return;
|
|
1907
|
+
const controller = new AbortController();
|
|
1908
|
+
currentRef.current = controller;
|
|
1909
|
+
setSpeaking(true);
|
|
1910
|
+
void (async () => {
|
|
1911
|
+
try {
|
|
1912
|
+
await voice.speak(line, { signal: controller.signal });
|
|
1913
|
+
} catch {
|
|
1914
|
+
} finally {
|
|
1915
|
+
if (currentRef.current === controller) {
|
|
1916
|
+
currentRef.current = void 0;
|
|
1917
|
+
setSpeaking(false);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
})();
|
|
1921
|
+
}, [stop]);
|
|
1922
|
+
return { onSceneChange, interrupt, speaking };
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// src/video-chat/use-video-chat.ts
|
|
1926
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useReducer, useRef as useRef4 } from "react";
|
|
1927
|
+
|
|
1928
|
+
// src/video-chat/voice.ts
|
|
1929
|
+
var DEFAULT_MAX_CACHED_LINES = 60;
|
|
1930
|
+
var FALLBACK_BITS_PER_SECOND = 128e3;
|
|
1931
|
+
var sharedContext;
|
|
1932
|
+
function actionEndpoint(endpoint, action) {
|
|
1933
|
+
const value = String(endpoint);
|
|
1934
|
+
return `${value}${value.includes("?") ? "&" : "?"}action=${encodeURIComponent(action)}`;
|
|
1935
|
+
}
|
|
1936
|
+
function estimatedBrowserSeconds(text) {
|
|
1937
|
+
const words = text.trim().split(/\s+/u).filter(Boolean).length;
|
|
1938
|
+
return Math.max(1, words / 2.5);
|
|
1939
|
+
}
|
|
1940
|
+
async function measureSeconds(bytes) {
|
|
1941
|
+
try {
|
|
1942
|
+
sharedContext ??= new AudioContext();
|
|
1943
|
+
const decoded = await sharedContext.decodeAudioData(bytes.slice(0));
|
|
1944
|
+
if (decoded.duration > 0) return decoded.duration;
|
|
1945
|
+
} catch {
|
|
1946
|
+
}
|
|
1947
|
+
return bytes.byteLength * 8 / FALLBACK_BITS_PER_SECOND;
|
|
1948
|
+
}
|
|
1949
|
+
function createVideoChatVoice(options = {}) {
|
|
1950
|
+
const endpoint = options.endpoint ?? "/api/video-chat";
|
|
1951
|
+
const fetcher = options.fetcher ?? fetch;
|
|
1952
|
+
const maximum = options.maxCachedLines ?? DEFAULT_MAX_CACHED_LINES;
|
|
1953
|
+
if (!Number.isInteger(maximum) || maximum <= 0) throw new Error("maxCachedLines must be a positive integer");
|
|
1954
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1955
|
+
const pendingLoads = /* @__PURE__ */ new Set();
|
|
1956
|
+
let sounding;
|
|
1957
|
+
let browserFinish;
|
|
1958
|
+
let held = false;
|
|
1959
|
+
let silent = false;
|
|
1960
|
+
let disposed = false;
|
|
1961
|
+
let generatedSpeechUnavailable = false;
|
|
1962
|
+
const forgetOldest = () => {
|
|
1963
|
+
while (lines.size > maximum) {
|
|
1964
|
+
const oldest = lines.keys().next();
|
|
1965
|
+
if (oldest.done) return;
|
|
1966
|
+
const line = lines.get(oldest.value);
|
|
1967
|
+
lines.delete(oldest.value);
|
|
1968
|
+
if (line?.source === "generated") URL.revokeObjectURL(line.src);
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
const load = async (text, signal) => {
|
|
1972
|
+
if (disposed) throw new DOMException("Video chat voice was disposed", "AbortError");
|
|
1973
|
+
const normalized = text.trim();
|
|
1974
|
+
const cached = lines.get(normalized);
|
|
1975
|
+
if (cached) {
|
|
1976
|
+
lines.delete(normalized);
|
|
1977
|
+
lines.set(normalized, cached);
|
|
1978
|
+
return cached;
|
|
1979
|
+
}
|
|
1980
|
+
const headers = new Headers(options.headers);
|
|
1981
|
+
headers.set("content-type", "application/json");
|
|
1982
|
+
const controller = new AbortController();
|
|
1983
|
+
const forwardAbort = () => controller.abort(signal?.reason);
|
|
1984
|
+
if (signal?.aborted) forwardAbort();
|
|
1985
|
+
else signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1986
|
+
pendingLoads.add(controller);
|
|
1987
|
+
let prepared;
|
|
1988
|
+
let createdSrc;
|
|
1989
|
+
try {
|
|
1990
|
+
try {
|
|
1991
|
+
if (generatedSpeechUnavailable) {
|
|
1992
|
+
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
1993
|
+
} else {
|
|
1994
|
+
const response = await fetcher(actionEndpoint(endpoint, "speech"), {
|
|
1995
|
+
method: "POST",
|
|
1996
|
+
headers,
|
|
1997
|
+
credentials: options.credentials,
|
|
1998
|
+
signal: controller.signal,
|
|
1999
|
+
body: JSON.stringify({ text: normalized })
|
|
2000
|
+
});
|
|
2001
|
+
if (controller.signal.aborted) {
|
|
2002
|
+
throw controller.signal.reason ?? new DOMException("Speech preparation cancelled", "AbortError");
|
|
2003
|
+
}
|
|
2004
|
+
if (response.status === 204 || response.status === 404) {
|
|
2005
|
+
generatedSpeechUnavailable = true;
|
|
2006
|
+
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2007
|
+
} else if (!response.ok) {
|
|
2008
|
+
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2009
|
+
} else {
|
|
2010
|
+
const bytes = await response.arrayBuffer();
|
|
2011
|
+
if (controller.signal.aborted) {
|
|
2012
|
+
throw controller.signal.reason ?? new DOMException("Speech preparation cancelled", "AbortError");
|
|
2013
|
+
}
|
|
2014
|
+
createdSrc = URL.createObjectURL(new Blob([bytes], {
|
|
2015
|
+
type: response.headers.get("content-type") || "audio/mpeg"
|
|
2016
|
+
}));
|
|
2017
|
+
prepared = { source: "generated", src: createdSrc, seconds: await measureSeconds(bytes) };
|
|
2018
|
+
if (controller.signal.aborted) {
|
|
2019
|
+
throw controller.signal.reason ?? new DOMException("Speech preparation cancelled", "AbortError");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
} catch (cause) {
|
|
2024
|
+
if (createdSrc) URL.revokeObjectURL(createdSrc);
|
|
2025
|
+
if (controller.signal.aborted) throw controller.signal.reason ?? cause;
|
|
2026
|
+
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2027
|
+
}
|
|
2028
|
+
if (disposed || controller.signal.aborted) {
|
|
2029
|
+
if (prepared.source === "generated") URL.revokeObjectURL(prepared.src);
|
|
2030
|
+
throw controller.signal.reason ?? new DOMException("Video chat voice was disposed", "AbortError");
|
|
2031
|
+
}
|
|
2032
|
+
const existing = lines.get(normalized);
|
|
2033
|
+
if (existing) {
|
|
2034
|
+
if (prepared.source === "generated") URL.revokeObjectURL(prepared.src);
|
|
2035
|
+
lines.delete(normalized);
|
|
2036
|
+
lines.set(normalized, existing);
|
|
2037
|
+
return existing;
|
|
2038
|
+
}
|
|
2039
|
+
lines.set(normalized, prepared);
|
|
2040
|
+
forgetOldest();
|
|
2041
|
+
return prepared;
|
|
2042
|
+
} finally {
|
|
2043
|
+
pendingLoads.delete(controller);
|
|
2044
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
const stopBrowser = () => {
|
|
2048
|
+
globalThis.speechSynthesis?.cancel();
|
|
2049
|
+
browserFinish?.();
|
|
2050
|
+
};
|
|
2051
|
+
return {
|
|
2052
|
+
async prepare(text, preparation = {}) {
|
|
2053
|
+
const line = await load(text, preparation.signal);
|
|
2054
|
+
return { seconds: line.seconds };
|
|
2055
|
+
},
|
|
2056
|
+
pause() {
|
|
2057
|
+
held = true;
|
|
2058
|
+
sounding?.pause();
|
|
2059
|
+
globalThis.speechSynthesis?.pause();
|
|
2060
|
+
},
|
|
2061
|
+
resume() {
|
|
2062
|
+
held = false;
|
|
2063
|
+
if (sounding && !silent) void sounding.play().catch(() => void 0);
|
|
2064
|
+
if (!silent) globalThis.speechSynthesis?.resume();
|
|
2065
|
+
},
|
|
2066
|
+
setMuted(muted) {
|
|
2067
|
+
silent = muted;
|
|
2068
|
+
if (sounding) sounding.muted = muted;
|
|
2069
|
+
if (muted) stopBrowser();
|
|
2070
|
+
},
|
|
2071
|
+
async speak(text, { signal }) {
|
|
2072
|
+
const line = await load(text, signal);
|
|
2073
|
+
if (disposed || signal.aborted || silent) return;
|
|
2074
|
+
if (line.source === "browser") {
|
|
2075
|
+
const synthesis = globalThis.speechSynthesis;
|
|
2076
|
+
if (!synthesis || typeof SpeechSynthesisUtterance === "undefined") return;
|
|
2077
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
2078
|
+
utterance.rate = 1;
|
|
2079
|
+
await new Promise((resolve) => {
|
|
2080
|
+
let finished = false;
|
|
2081
|
+
const finish = () => {
|
|
2082
|
+
if (finished) return;
|
|
2083
|
+
finished = true;
|
|
2084
|
+
if (browserFinish === finish) browserFinish = void 0;
|
|
2085
|
+
resolve();
|
|
2086
|
+
};
|
|
2087
|
+
browserFinish = finish;
|
|
2088
|
+
utterance.onend = finish;
|
|
2089
|
+
utterance.onerror = finish;
|
|
2090
|
+
signal.addEventListener("abort", () => {
|
|
2091
|
+
synthesis.cancel();
|
|
2092
|
+
finish();
|
|
2093
|
+
}, { once: true });
|
|
2094
|
+
synthesis.speak(utterance);
|
|
2095
|
+
if (held) synthesis.pause();
|
|
2096
|
+
});
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const element = new Audio(line.src);
|
|
2100
|
+
element.muted = silent;
|
|
2101
|
+
sounding = element;
|
|
2102
|
+
await new Promise((resolve) => {
|
|
2103
|
+
let finished = false;
|
|
2104
|
+
const finish = () => {
|
|
2105
|
+
if (finished) return;
|
|
2106
|
+
finished = true;
|
|
2107
|
+
if (sounding === element) sounding = void 0;
|
|
2108
|
+
resolve();
|
|
2109
|
+
};
|
|
2110
|
+
element.onended = finish;
|
|
2111
|
+
element.onerror = finish;
|
|
2112
|
+
signal.addEventListener("abort", () => {
|
|
2113
|
+
element.pause();
|
|
2114
|
+
finish();
|
|
2115
|
+
}, { once: true });
|
|
2116
|
+
if (!held) void element.play().catch(finish);
|
|
2117
|
+
});
|
|
2118
|
+
},
|
|
2119
|
+
dispose() {
|
|
2120
|
+
disposed = true;
|
|
2121
|
+
for (const controller of pendingLoads) {
|
|
2122
|
+
controller.abort(new DOMException("Video chat voice was disposed", "AbortError"));
|
|
2123
|
+
}
|
|
2124
|
+
pendingLoads.clear();
|
|
2125
|
+
sounding?.pause();
|
|
2126
|
+
sounding = void 0;
|
|
2127
|
+
stopBrowser();
|
|
2128
|
+
for (const line of lines.values()) {
|
|
2129
|
+
if (line.source === "generated") URL.revokeObjectURL(line.src);
|
|
2130
|
+
}
|
|
2131
|
+
lines.clear();
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// src/video-chat/use-video-chat.ts
|
|
2137
|
+
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
2138
|
+
function initialState(muted) {
|
|
2139
|
+
return {
|
|
2140
|
+
turns: [],
|
|
2141
|
+
status: "idle",
|
|
2142
|
+
resumeStatus: "idle",
|
|
2143
|
+
spokenUpTo: -1,
|
|
2144
|
+
muted,
|
|
2145
|
+
playbackEnded: false,
|
|
2146
|
+
playerKey: 0,
|
|
2147
|
+
openingSpeaking: false
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function replaceTurn(turns, id, update) {
|
|
2151
|
+
return turns.map((turn) => turn.id === id ? update(turn) : turn);
|
|
2152
|
+
}
|
|
2153
|
+
function reducer(state, action) {
|
|
2154
|
+
switch (action.type) {
|
|
2155
|
+
case "capabilities":
|
|
2156
|
+
return { ...state, capabilities: action.value };
|
|
2157
|
+
case "welcome":
|
|
2158
|
+
return { ...state, welcome: action.value };
|
|
2159
|
+
case "start":
|
|
2160
|
+
return {
|
|
2161
|
+
...state,
|
|
2162
|
+
turns: [...state.turns, action.turn],
|
|
2163
|
+
shownTurnId: action.turn.id,
|
|
2164
|
+
status: "composing",
|
|
2165
|
+
resumeStatus: "composing",
|
|
2166
|
+
error: void 0,
|
|
2167
|
+
caption: void 0,
|
|
2168
|
+
spokenUpTo: -1,
|
|
2169
|
+
playbackEnded: false,
|
|
2170
|
+
playback: void 0,
|
|
2171
|
+
openingSpeaking: false
|
|
2172
|
+
};
|
|
2173
|
+
case "opening-start":
|
|
2174
|
+
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
2175
|
+
return {
|
|
2176
|
+
...state,
|
|
2177
|
+
turns: replaceTurn(state.turns, action.id, (turn) => ({
|
|
2178
|
+
...turn,
|
|
2179
|
+
opening: turn.opening ? `${turn.opening} ${action.line}` : action.line
|
|
2180
|
+
})),
|
|
2181
|
+
status: state.status === "paused" ? "paused" : "playing",
|
|
2182
|
+
resumeStatus: "playing",
|
|
2183
|
+
caption: action.line,
|
|
2184
|
+
openingSpeaking: true
|
|
2185
|
+
};
|
|
2186
|
+
case "opening-media":
|
|
2187
|
+
if (state.turns.at(-1)?.id !== action.id || state.playback) return state;
|
|
2188
|
+
return {
|
|
2189
|
+
...state,
|
|
2190
|
+
turns: replaceTurn(state.turns, action.id, (turn) => ({ ...turn, openingMedia: action.media }))
|
|
2191
|
+
};
|
|
2192
|
+
case "opening-end":
|
|
2193
|
+
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
2194
|
+
return {
|
|
2195
|
+
...state,
|
|
2196
|
+
status: state.status === "paused" ? "paused" : state.playback ? "playing" : "composing",
|
|
2197
|
+
resumeStatus: state.playback ? "playing" : "composing",
|
|
2198
|
+
openingSpeaking: false
|
|
2199
|
+
};
|
|
2200
|
+
case "player":
|
|
2201
|
+
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
2202
|
+
return {
|
|
2203
|
+
...state,
|
|
2204
|
+
status: state.status === "paused" ? "paused" : "playing",
|
|
2205
|
+
resumeStatus: "playing",
|
|
2206
|
+
playback: { kind: "stream", stream: action.stream },
|
|
2207
|
+
playerKey: state.playerKey + 1,
|
|
2208
|
+
playbackEnded: false
|
|
2209
|
+
};
|
|
2210
|
+
case "partial":
|
|
2211
|
+
return { ...state, turns: replaceTurn(state.turns, action.id, (turn) => ({ ...turn, video: action.video })) };
|
|
2212
|
+
case "complete":
|
|
2213
|
+
return {
|
|
2214
|
+
...state,
|
|
2215
|
+
turns: replaceTurn(state.turns, action.id, (turn) => ({
|
|
2216
|
+
...turn,
|
|
2217
|
+
completed: true,
|
|
2218
|
+
video: action.video,
|
|
2219
|
+
suggestions: action.suggestions
|
|
2220
|
+
}))
|
|
2221
|
+
};
|
|
2222
|
+
case "scene":
|
|
2223
|
+
if (state.playerKey !== action.key) return state;
|
|
2224
|
+
return {
|
|
2225
|
+
...state,
|
|
2226
|
+
caption: action.scene.narration?.trim() || state.caption,
|
|
2227
|
+
spokenUpTo: Math.max(state.spokenUpTo, action.index)
|
|
2228
|
+
};
|
|
2229
|
+
case "playback-end":
|
|
2230
|
+
if (state.playerKey !== action.key) return state;
|
|
2231
|
+
return { ...state, status: "ended", resumeStatus: "ended", playbackEnded: true, openingSpeaking: false };
|
|
2232
|
+
case "error":
|
|
2233
|
+
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
2234
|
+
return {
|
|
2235
|
+
...state,
|
|
2236
|
+
status: "error",
|
|
2237
|
+
resumeStatus: "error",
|
|
2238
|
+
error: action.error,
|
|
2239
|
+
openingSpeaking: false,
|
|
2240
|
+
playback: void 0
|
|
2241
|
+
};
|
|
2242
|
+
case "cancelled":
|
|
2243
|
+
return {
|
|
2244
|
+
...state,
|
|
2245
|
+
status: "cancelled",
|
|
2246
|
+
resumeStatus: "cancelled",
|
|
2247
|
+
openingSpeaking: false,
|
|
2248
|
+
playback: void 0
|
|
2249
|
+
};
|
|
2250
|
+
case "pause":
|
|
2251
|
+
if (state.status === "idle" || state.status === "paused" || state.status === "ended" || state.status === "cancelled" || state.status === "error") return state;
|
|
2252
|
+
return { ...state, resumeStatus: state.status, status: "paused" };
|
|
2253
|
+
case "resume":
|
|
2254
|
+
return state.status === "paused" ? { ...state, status: state.resumeStatus } : state;
|
|
2255
|
+
case "mute":
|
|
2256
|
+
return { ...state, muted: action.value };
|
|
2257
|
+
case "select": {
|
|
2258
|
+
const turn = state.turns.find((entry) => entry.id === action.id);
|
|
2259
|
+
if (!turn?.video) return state;
|
|
2260
|
+
return {
|
|
2261
|
+
...state,
|
|
2262
|
+
shownTurnId: turn.id,
|
|
2263
|
+
playback: { kind: "video", video: turn.video },
|
|
2264
|
+
status: "playing",
|
|
2265
|
+
resumeStatus: "playing",
|
|
2266
|
+
playerKey: state.playerKey + 1,
|
|
2267
|
+
playbackEnded: false,
|
|
2268
|
+
spokenUpTo: -1,
|
|
2269
|
+
caption: turn.opening,
|
|
2270
|
+
error: void 0
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
case "replay": {
|
|
2274
|
+
const turn = state.turns.find((entry) => entry.id === state.shownTurnId);
|
|
2275
|
+
if (!turn?.video) return state;
|
|
2276
|
+
return {
|
|
2277
|
+
...state,
|
|
2278
|
+
playback: { kind: "video", video: turn.video },
|
|
2279
|
+
status: "playing",
|
|
2280
|
+
resumeStatus: "playing",
|
|
2281
|
+
playerKey: state.playerKey + 1,
|
|
2282
|
+
playbackEnded: false,
|
|
2283
|
+
spokenUpTo: -1,
|
|
2284
|
+
caption: turn.opening,
|
|
2285
|
+
error: void 0
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
case "reset":
|
|
2289
|
+
return { ...initialState(state.muted), capabilities: state.capabilities, welcome: state.welcome };
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function actionEndpoint2(endpoint, action) {
|
|
2293
|
+
const value = String(endpoint);
|
|
2294
|
+
return `${value}${value.includes("?") ? "&" : "?"}action=${encodeURIComponent(action)}`;
|
|
2295
|
+
}
|
|
2296
|
+
function defaultTurnId() {
|
|
2297
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
2298
|
+
}
|
|
2299
|
+
function monotonicNow() {
|
|
2300
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
2301
|
+
}
|
|
2302
|
+
function transcriptFor(turn) {
|
|
2303
|
+
return [
|
|
2304
|
+
...turn.opening ? [turn.opening] : [],
|
|
2305
|
+
...turn.video?.scenes.flatMap((entry) => entry.narration?.trim() ? [entry.narration.trim()] : []) ?? []
|
|
2306
|
+
];
|
|
2307
|
+
}
|
|
2308
|
+
function conversationFor(turns) {
|
|
2309
|
+
return turns.filter((turn) => turn.completed && turn.video).slice(-12).map((turn) => ({
|
|
2310
|
+
prompt: turn.prompt,
|
|
2311
|
+
response: [...transcriptFor(turn).join(" ")].slice(0, 8e3).join("")
|
|
2312
|
+
}));
|
|
2313
|
+
}
|
|
2314
|
+
function errorFrom2(cause) {
|
|
2315
|
+
if (cause instanceof VideoError) return cause;
|
|
2316
|
+
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
2317
|
+
return new VideoError("Video chat was cancelled", { code: "aborted", recoverable: false });
|
|
2318
|
+
}
|
|
2319
|
+
return new VideoError(safePublicDiagnostic(
|
|
2320
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
2321
|
+
"Video chat failed"
|
|
2322
|
+
), { code: "video_chat_failed", recoverable: false });
|
|
2323
|
+
}
|
|
2324
|
+
async function responseError2(response) {
|
|
2325
|
+
let code = "http_error";
|
|
2326
|
+
let message = `Video chat endpoint failed (${response.status})`;
|
|
2327
|
+
try {
|
|
2328
|
+
const body = await response.json();
|
|
2329
|
+
if (typeof body.error?.code === "string") code = body.error.code.slice(0, 80);
|
|
2330
|
+
if (typeof body.error?.message === "string") {
|
|
2331
|
+
message = safePublicDiagnostic(body.error.message, message);
|
|
2332
|
+
}
|
|
2333
|
+
} catch {
|
|
2334
|
+
}
|
|
2335
|
+
return new VideoError(message, { code, status: response.status, recoverable: false });
|
|
2336
|
+
}
|
|
2337
|
+
function pacedScene(scene, spokenSeconds, templates) {
|
|
2338
|
+
const held = spokenSeconds && spokenSeconds > 0 ? spokenSeconds + 0.8 : getSceneDuration(scene, templates?.getTemplateMetadata(scene.templateId) ?? getBuiltinTemplateMetadata(scene.templateId));
|
|
2339
|
+
const {
|
|
2340
|
+
startTime: _startTime,
|
|
2341
|
+
endTime: _endTime,
|
|
2342
|
+
beatStart: _beatStart,
|
|
2343
|
+
beatEnd: _beatEnd,
|
|
2344
|
+
...timing
|
|
2345
|
+
} = scene.timing ?? {};
|
|
2346
|
+
return { ...scene, timing: { ...timing, fixedDuration: held } };
|
|
2347
|
+
}
|
|
2348
|
+
function prepareVisualScene(scene, mode) {
|
|
2349
|
+
const overMedia = typeof scene.variables.mediaUrl === "string";
|
|
2350
|
+
const filmed = mode === "full" && overMedia;
|
|
2351
|
+
const shown = filmed ? {
|
|
2352
|
+
...scene,
|
|
2353
|
+
backgroundEffect: "static",
|
|
2354
|
+
variables: {
|
|
2355
|
+
...scene.variables,
|
|
2356
|
+
texts: "",
|
|
2357
|
+
mediaTreatment: "none",
|
|
2358
|
+
confetti: false
|
|
2359
|
+
}
|
|
2360
|
+
} : scene;
|
|
2361
|
+
return overMedia ? { ...shown, textArchetype: "subtle" } : shown;
|
|
2362
|
+
}
|
|
2363
|
+
function useVideoChat(options = {}) {
|
|
2364
|
+
const optionsRef = useRef4(options);
|
|
2365
|
+
optionsRef.current = options;
|
|
2366
|
+
const ownedVoiceRef = useRef4(void 0);
|
|
2367
|
+
if (!options.voice && !ownedVoiceRef.current) {
|
|
2368
|
+
ownedVoiceRef.current = createVideoChatVoice({
|
|
2369
|
+
endpoint: options.endpoint,
|
|
2370
|
+
headers: options.headers,
|
|
2371
|
+
credentials: options.credentials,
|
|
2372
|
+
fetcher: options.fetcher
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
const voice = options.voice ?? ownedVoiceRef.current;
|
|
2376
|
+
const voiceRef = useRef4(voice);
|
|
2377
|
+
voiceRef.current = voice;
|
|
2378
|
+
const narration = useNarration({ voice });
|
|
2379
|
+
const narrationRef = useRef4(narration);
|
|
2380
|
+
narrationRef.current = narration;
|
|
2381
|
+
const [state, dispatch] = useReducer(reducer, options.initialMuted ?? false, initialState);
|
|
2382
|
+
const stateRef = useRef4(state);
|
|
2383
|
+
stateRef.current = state;
|
|
2384
|
+
const runRef = useRef4(0);
|
|
2385
|
+
const inFlightRef = useRef4(void 0);
|
|
2386
|
+
const openingRef = useRef4(void 0);
|
|
2387
|
+
const timelineRef = useRef4(void 0);
|
|
2388
|
+
const heldRef = useRef4(false);
|
|
2389
|
+
const flushRef = useRef4(void 0);
|
|
2390
|
+
const mountedRef = useRef4(true);
|
|
2391
|
+
const firstFrameRef = useRef4(void 0);
|
|
2392
|
+
const request = useCallback3(async (action, init = {}, signal) => {
|
|
2393
|
+
const current = optionsRef.current;
|
|
2394
|
+
const headers = new Headers(current.headers);
|
|
2395
|
+
new Headers(init.headers).forEach((header, name) => headers.set(name, header));
|
|
2396
|
+
if (init.body && !(init.body instanceof FormData) && !(init.body instanceof Blob)) {
|
|
2397
|
+
headers.set("content-type", "application/json");
|
|
2398
|
+
}
|
|
2399
|
+
return (current.fetcher ?? fetch)(actionEndpoint2(current.endpoint ?? "/api/video-chat", action), {
|
|
2400
|
+
...init,
|
|
2401
|
+
headers,
|
|
2402
|
+
credentials: current.credentials,
|
|
2403
|
+
signal
|
|
2404
|
+
});
|
|
2405
|
+
}, []);
|
|
2406
|
+
useEffect5(() => {
|
|
2407
|
+
mountedRef.current = true;
|
|
2408
|
+
const controller = new AbortController();
|
|
2409
|
+
void request("capabilities", {}, controller.signal).then(async (response) => {
|
|
2410
|
+
if (!response.ok) throw await responseError2(response);
|
|
2411
|
+
return response.json();
|
|
2412
|
+
}).then((value) => {
|
|
2413
|
+
if (mountedRef.current) dispatch({ type: "capabilities", value });
|
|
2414
|
+
}).catch(() => void 0);
|
|
2415
|
+
void request("welcome", {}, controller.signal).then(async (response) => {
|
|
2416
|
+
if (!response.ok) throw await responseError2(response);
|
|
2417
|
+
return response.json();
|
|
2418
|
+
}).then((value) => {
|
|
2419
|
+
if (mountedRef.current) dispatch({ type: "welcome", value });
|
|
2420
|
+
}).catch(() => void 0);
|
|
2421
|
+
return () => {
|
|
2422
|
+
mountedRef.current = false;
|
|
2423
|
+
controller.abort();
|
|
2424
|
+
runRef.current += 1;
|
|
2425
|
+
inFlightRef.current?.abort(new DOMException("Component unmounted", "AbortError"));
|
|
2426
|
+
openingRef.current?.abort(new DOMException("Component unmounted", "AbortError"));
|
|
2427
|
+
timelineRef.current?.complete();
|
|
2428
|
+
timelineRef.current = void 0;
|
|
2429
|
+
narrationRef.current.interrupt();
|
|
2430
|
+
queueMicrotask(() => {
|
|
2431
|
+
if (!mountedRef.current) ownedVoiceRef.current?.dispose?.();
|
|
2432
|
+
});
|
|
2433
|
+
};
|
|
2434
|
+
}, [request]);
|
|
2435
|
+
useEffect5(() => voice.setMuted(state.muted), [state.muted, voice]);
|
|
2436
|
+
const cancel = useCallback3((reason = "Video chat was cancelled") => {
|
|
2437
|
+
runRef.current += 1;
|
|
2438
|
+
inFlightRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
2439
|
+
openingRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
2440
|
+
inFlightRef.current = void 0;
|
|
2441
|
+
openingRef.current = void 0;
|
|
2442
|
+
timelineRef.current?.complete();
|
|
2443
|
+
timelineRef.current = void 0;
|
|
2444
|
+
flushRef.current = void 0;
|
|
2445
|
+
narrationRef.current.interrupt();
|
|
2446
|
+
dispatch({ type: "cancelled" });
|
|
2447
|
+
}, []);
|
|
2448
|
+
const ask = useCallback3(async (value, askOptions = {}) => {
|
|
2449
|
+
const prompt = value.trim();
|
|
2450
|
+
if (!prompt) return void 0;
|
|
2451
|
+
inFlightRef.current?.abort(new DOMException("Replaced by a new prompt", "AbortError"));
|
|
2452
|
+
openingRef.current?.abort(new DOMException("Replaced by a new prompt", "AbortError"));
|
|
2453
|
+
timelineRef.current?.complete();
|
|
2454
|
+
timelineRef.current = void 0;
|
|
2455
|
+
narrationRef.current.interrupt();
|
|
2456
|
+
voiceRef.current.resume();
|
|
2457
|
+
heldRef.current = false;
|
|
2458
|
+
const controller = new AbortController();
|
|
2459
|
+
const openingController = new AbortController();
|
|
2460
|
+
inFlightRef.current = controller;
|
|
2461
|
+
openingRef.current = openingController;
|
|
2462
|
+
controller.signal.addEventListener("abort", () => {
|
|
2463
|
+
openingController.abort(controller.signal.reason);
|
|
2464
|
+
}, { once: true });
|
|
2465
|
+
const run = runRef.current + 1;
|
|
2466
|
+
runRef.current = run;
|
|
2467
|
+
const currentOptions = optionsRef.current;
|
|
2468
|
+
const timeoutMs = currentOptions.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2469
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
2470
|
+
inFlightRef.current = void 0;
|
|
2471
|
+
throw new VideoError("timeoutMs must be positive", { code: "invalid_option" });
|
|
2472
|
+
}
|
|
2473
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("Video chat timed out", "TimeoutError")), timeoutMs);
|
|
2474
|
+
const requestedMode = currentOptions.mode ?? "templates";
|
|
2475
|
+
const knownModes = stateRef.current.capabilities?.modes;
|
|
2476
|
+
const mode = knownModes && !knownModes.includes(requestedMode) ? "templates" : requestedMode;
|
|
2477
|
+
const orientation = currentOptions.orientation ?? "landscape";
|
|
2478
|
+
const id = (currentOptions.createTurnId ?? defaultTurnId)();
|
|
2479
|
+
const conversation = conversationFor(stateRef.current.turns);
|
|
2480
|
+
const openingMedia = sanitizeVideoChatMedia(askOptions.openingMedia);
|
|
2481
|
+
const suppliedOpening = typeof askOptions.opening === "string" ? askOptions.opening.trim().slice(0, 300) : "";
|
|
2482
|
+
const turn = {
|
|
2483
|
+
id,
|
|
2484
|
+
prompt,
|
|
2485
|
+
completed: false,
|
|
2486
|
+
orientation,
|
|
2487
|
+
fixedOrientation: mode === "full",
|
|
2488
|
+
suggestions: [],
|
|
2489
|
+
...openingMedia ? { openingMedia } : {}
|
|
2490
|
+
};
|
|
2491
|
+
firstFrameRef.current = { turnId: id, mode, startedAt: monotonicNow(), reported: false };
|
|
2492
|
+
dispatch({ type: "start", turn });
|
|
2493
|
+
let timeline;
|
|
2494
|
+
let style;
|
|
2495
|
+
let openingActive = false;
|
|
2496
|
+
let openingRequested = false;
|
|
2497
|
+
let spokenHook = suppliedOpening;
|
|
2498
|
+
let planDone = false;
|
|
2499
|
+
let timelineCompleted = false;
|
|
2500
|
+
let terminal = false;
|
|
2501
|
+
let attempt = 0;
|
|
2502
|
+
let ready = [];
|
|
2503
|
+
let appended = 0;
|
|
2504
|
+
const isCurrent = () => mountedRef.current && runRef.current === run && !controller.signal.aborted;
|
|
2505
|
+
const isOpeningCurrent = () => isCurrent() && !terminal && openingRef.current === openingController && !openingController.signal.aborted;
|
|
2506
|
+
const flush = () => {
|
|
2507
|
+
if (!isCurrent()) return;
|
|
2508
|
+
let available = appended;
|
|
2509
|
+
while (ready[available]) available += 1;
|
|
2510
|
+
if (available === appended && timeline) {
|
|
2511
|
+
if (planDone && !timelineCompleted) {
|
|
2512
|
+
timelineCompleted = true;
|
|
2513
|
+
timeline.complete();
|
|
2514
|
+
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2515
|
+
}
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (!timeline) {
|
|
2519
|
+
if (!style || openingActive || heldRef.current || available === appended) return;
|
|
2520
|
+
timeline = createSceneTimeline({ style, orientation });
|
|
2521
|
+
timelineRef.current = timeline;
|
|
2522
|
+
openingController.abort(new DOMException("Opening replaced by response", "AbortError"));
|
|
2523
|
+
if (openingRef.current === openingController) openingRef.current = void 0;
|
|
2524
|
+
dispatch({ type: "player", id, stream: timeline.stream });
|
|
2525
|
+
}
|
|
2526
|
+
while (ready[appended]) timeline.add(ready[appended++]);
|
|
2527
|
+
dispatch({
|
|
2528
|
+
type: "partial",
|
|
2529
|
+
id,
|
|
2530
|
+
video: { schemaVersion: "0.1", orientation, scenes: ready.slice(0, appended), style }
|
|
2531
|
+
});
|
|
2532
|
+
if (planDone && !timelineCompleted) {
|
|
2533
|
+
timelineCompleted = true;
|
|
2534
|
+
timeline.complete();
|
|
2535
|
+
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
flushRef.current = flush;
|
|
2539
|
+
const resolveOpeningMedia = async (keyword) => {
|
|
2540
|
+
try {
|
|
2541
|
+
const response = await request("opening-media", {
|
|
2542
|
+
method: "POST",
|
|
2543
|
+
body: JSON.stringify({ keyword, orientation })
|
|
2544
|
+
}, openingController.signal);
|
|
2545
|
+
if (!response.ok || !isOpeningCurrent() || timeline) return;
|
|
2546
|
+
const payload = await response.json();
|
|
2547
|
+
const media = sanitizeVideoChatMedia(payload.media);
|
|
2548
|
+
if (media && isOpeningCurrent() && !timeline) dispatch({ type: "opening-media", id, media });
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
};
|
|
2552
|
+
const speakOpening = async (text) => {
|
|
2553
|
+
try {
|
|
2554
|
+
await voiceRef.current.prepare(text, { signal: openingController.signal });
|
|
2555
|
+
if (!isOpeningCurrent()) return;
|
|
2556
|
+
dispatch({ type: "opening-start", id, line: text });
|
|
2557
|
+
await voiceRef.current.speak(text, { signal: openingController.signal });
|
|
2558
|
+
} catch {
|
|
2559
|
+
} finally {
|
|
2560
|
+
openingActive = false;
|
|
2561
|
+
if (isOpeningCurrent()) dispatch({ type: "opening-end", id });
|
|
2562
|
+
flush();
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
if (spokenHook) {
|
|
2566
|
+
openingRequested = true;
|
|
2567
|
+
openingActive = true;
|
|
2568
|
+
void speakOpening(spokenHook);
|
|
2569
|
+
}
|
|
2570
|
+
const runAttempt = async (currentAttempt) => {
|
|
2571
|
+
const response = await request("response", {
|
|
2572
|
+
method: "POST",
|
|
2573
|
+
headers: { accept: "text/event-stream" },
|
|
2574
|
+
body: JSON.stringify({
|
|
2575
|
+
prompt,
|
|
2576
|
+
...spokenHook ? { opening: spokenHook } : {},
|
|
2577
|
+
mode,
|
|
2578
|
+
orientation,
|
|
2579
|
+
conversation,
|
|
2580
|
+
...currentOptions.brand ? { brand: currentOptions.brand } : {},
|
|
2581
|
+
...currentOptions.style ? { style: currentOptions.style } : {}
|
|
2582
|
+
})
|
|
2583
|
+
}, controller.signal);
|
|
2584
|
+
if (!response.ok) throw await responseError2(response);
|
|
2585
|
+
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
2586
|
+
throw new VideoError("Video chat endpoint did not return a video stream", { code: "invalid_response" });
|
|
2587
|
+
}
|
|
2588
|
+
const planned = [];
|
|
2589
|
+
const lines = spokenHook ? [spokenHook] : [];
|
|
2590
|
+
const pending = [];
|
|
2591
|
+
let narrating = Promise.resolve();
|
|
2592
|
+
let terminalError;
|
|
2593
|
+
for await (const event of decodeVideoSse(response.body)) {
|
|
2594
|
+
if (!isCurrent() || currentAttempt !== attempt) return { video: { schemaVersion: "0.1", orientation, scenes: [], style }, suggestions: [] };
|
|
2595
|
+
if (event.type === "response.start") style = event.data.style;
|
|
2596
|
+
if (event.type === "data.video-chat-opening") {
|
|
2597
|
+
const payload = event.data && typeof event.data === "object" && !Array.isArray(event.data) ? event.data : {};
|
|
2598
|
+
const line = typeof payload.line === "string" ? payload.line.trim().slice(0, 300) : "";
|
|
2599
|
+
const keyword = typeof payload.keyword === "string" ? payload.keyword.trim().slice(0, 80) : "";
|
|
2600
|
+
if (!openingMedia && keyword) void resolveOpeningMedia(keyword);
|
|
2601
|
+
if (!openingRequested && line) {
|
|
2602
|
+
spokenHook = line;
|
|
2603
|
+
lines.push(line);
|
|
2604
|
+
openingRequested = true;
|
|
2605
|
+
openingActive = true;
|
|
2606
|
+
void speakOpening(line);
|
|
2607
|
+
}
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (event.type === "response.error" && event.data.terminal) {
|
|
2611
|
+
terminalError = new VideoError(event.data.error.message, {
|
|
2612
|
+
code: event.data.error.code,
|
|
2613
|
+
requestId: event.data.snapshot ? void 0 : id,
|
|
2614
|
+
recoverable: event.data.error.recoverable
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
if (event.type === "response.abort") {
|
|
2618
|
+
terminalError = new VideoError(event.data.reason, { code: "aborted", recoverable: false });
|
|
2619
|
+
}
|
|
2620
|
+
if (event.type !== "scene.add") continue;
|
|
2621
|
+
const position = event.data.position;
|
|
2622
|
+
const plannedScene = event.data.scene;
|
|
2623
|
+
planned[position] = plannedScene;
|
|
2624
|
+
if (!currentOptions.templates?.getTemplate(plannedScene.templateId)) {
|
|
2625
|
+
preloadBuiltinTemplate(plannedScene.templateId);
|
|
2626
|
+
}
|
|
2627
|
+
warmSceneMedia(plannedScene.variables);
|
|
2628
|
+
const narrated = narrating.then(async () => {
|
|
2629
|
+
const supplied = plannedScene.narration?.trim();
|
|
2630
|
+
if (supplied) return supplied;
|
|
2631
|
+
const narrationResponse = await request("narration", {
|
|
2632
|
+
method: "POST",
|
|
2633
|
+
body: JSON.stringify({ prompt, scene: plannedScene, earlier: [...lines] })
|
|
2634
|
+
}, controller.signal);
|
|
2635
|
+
if (!narrationResponse.ok) return "";
|
|
2636
|
+
const payload = await narrationResponse.json();
|
|
2637
|
+
return typeof payload.line === "string" ? payload.line.trim() : "";
|
|
2638
|
+
}).then((line) => {
|
|
2639
|
+
if (line) lines.push(line);
|
|
2640
|
+
return line;
|
|
2641
|
+
});
|
|
2642
|
+
narrating = narrated.catch(() => "");
|
|
2643
|
+
pending.push(narrated.then(async (line) => {
|
|
2644
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2645
|
+
const visual = prepareVisualScene(plannedScene, mode);
|
|
2646
|
+
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
2647
|
+
const spoken = line ? await voiceRef.current.prepare(line, { signal: controller.signal }) : void 0;
|
|
2648
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2649
|
+
ready[position] = pacedScene(withNarration, spoken?.seconds, currentOptions.templates);
|
|
2650
|
+
flush();
|
|
2651
|
+
}));
|
|
2652
|
+
}
|
|
2653
|
+
await Promise.all(pending);
|
|
2654
|
+
if (terminalError) throw terminalError;
|
|
2655
|
+
if (planned.length === 0 || ready.filter(Boolean).length === 0 || !style) {
|
|
2656
|
+
throw new VideoError("The video response contained no playable scenes", { code: "empty_response" });
|
|
2657
|
+
}
|
|
2658
|
+
let suggestions2 = [];
|
|
2659
|
+
try {
|
|
2660
|
+
const suggestionsResponse = await request("suggestions", {
|
|
2661
|
+
method: "POST",
|
|
2662
|
+
body: JSON.stringify({ prompt, lines })
|
|
2663
|
+
}, controller.signal);
|
|
2664
|
+
if (suggestionsResponse.ok) {
|
|
2665
|
+
suggestions2 = (await suggestionsResponse.json()).suggestions ?? [];
|
|
2666
|
+
}
|
|
2667
|
+
} catch (cause) {
|
|
2668
|
+
if (controller.signal.aborted) throw cause;
|
|
2669
|
+
}
|
|
2670
|
+
return {
|
|
2671
|
+
video: {
|
|
2672
|
+
schemaVersion: "0.1",
|
|
2673
|
+
orientation,
|
|
2674
|
+
scenes: ready.filter((entry) => entry != null),
|
|
2675
|
+
style
|
|
2676
|
+
},
|
|
2677
|
+
suggestions: suggestions2
|
|
2678
|
+
};
|
|
2679
|
+
};
|
|
2680
|
+
try {
|
|
2681
|
+
let response;
|
|
2682
|
+
try {
|
|
2683
|
+
response = await runAttempt(attempt);
|
|
2684
|
+
} catch (cause) {
|
|
2685
|
+
if (controller.signal.aborted || timeline) throw cause;
|
|
2686
|
+
attempt += 1;
|
|
2687
|
+
ready = [];
|
|
2688
|
+
appended = 0;
|
|
2689
|
+
style = void 0;
|
|
2690
|
+
response = await runAttempt(attempt);
|
|
2691
|
+
}
|
|
2692
|
+
if (!isCurrent()) return void 0;
|
|
2693
|
+
planDone = true;
|
|
2694
|
+
flush();
|
|
2695
|
+
dispatch({ type: "complete", id, video: response.video, suggestions: response.suggestions });
|
|
2696
|
+
return response.video;
|
|
2697
|
+
} catch (cause) {
|
|
2698
|
+
if (runRef.current !== run) return void 0;
|
|
2699
|
+
if (controller.signal.aborted && !(controller.signal.reason instanceof DOMException && controller.signal.reason.name === "TimeoutError")) {
|
|
2700
|
+
return void 0;
|
|
2701
|
+
}
|
|
2702
|
+
terminal = true;
|
|
2703
|
+
openingController.abort(new DOMException("Response failed", "AbortError"));
|
|
2704
|
+
timeline?.complete();
|
|
2705
|
+
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2706
|
+
const error = errorFrom2(cause);
|
|
2707
|
+
dispatch({ type: "error", id, error });
|
|
2708
|
+
return void 0;
|
|
2709
|
+
} finally {
|
|
2710
|
+
clearTimeout(timeout);
|
|
2711
|
+
if (runRef.current === run) inFlightRef.current = void 0;
|
|
2712
|
+
}
|
|
2713
|
+
}, [request]);
|
|
2714
|
+
const pause = useCallback3(() => {
|
|
2715
|
+
heldRef.current = true;
|
|
2716
|
+
voiceRef.current.pause();
|
|
2717
|
+
dispatch({ type: "pause" });
|
|
2718
|
+
}, []);
|
|
2719
|
+
const resume = useCallback3(() => {
|
|
2720
|
+
heldRef.current = false;
|
|
2721
|
+
voiceRef.current.resume();
|
|
2722
|
+
dispatch({ type: "resume" });
|
|
2723
|
+
flushRef.current?.();
|
|
2724
|
+
}, []);
|
|
2725
|
+
const replay = useCallback3(() => {
|
|
2726
|
+
const turn = stateRef.current.turns.find((entry) => entry.id === stateRef.current.shownTurnId);
|
|
2727
|
+
if (!turn?.completed || !turn.video) return;
|
|
2728
|
+
if (inFlightRef.current) {
|
|
2729
|
+
runRef.current += 1;
|
|
2730
|
+
inFlightRef.current.abort(new DOMException("Replaying a saved response", "AbortError"));
|
|
2731
|
+
inFlightRef.current = void 0;
|
|
2732
|
+
}
|
|
2733
|
+
openingRef.current?.abort(new DOMException("Replaying a saved response", "AbortError"));
|
|
2734
|
+
openingRef.current = void 0;
|
|
2735
|
+
timelineRef.current?.complete();
|
|
2736
|
+
timelineRef.current = void 0;
|
|
2737
|
+
narrationRef.current.interrupt();
|
|
2738
|
+
heldRef.current = false;
|
|
2739
|
+
voiceRef.current.resume();
|
|
2740
|
+
dispatch({ type: "replay" });
|
|
2741
|
+
}, []);
|
|
2742
|
+
const selectTurn = useCallback3((id) => {
|
|
2743
|
+
const turn = stateRef.current.turns.find((entry) => entry.id === id);
|
|
2744
|
+
if (!turn?.completed || !turn.video) return;
|
|
2745
|
+
if (inFlightRef.current) {
|
|
2746
|
+
runRef.current += 1;
|
|
2747
|
+
inFlightRef.current.abort(new DOMException("Viewing a saved response", "AbortError"));
|
|
2748
|
+
inFlightRef.current = void 0;
|
|
2749
|
+
}
|
|
2750
|
+
openingRef.current?.abort(new DOMException("Viewing a saved response", "AbortError"));
|
|
2751
|
+
openingRef.current = void 0;
|
|
2752
|
+
timelineRef.current?.complete();
|
|
2753
|
+
timelineRef.current = void 0;
|
|
2754
|
+
narrationRef.current.interrupt();
|
|
2755
|
+
heldRef.current = false;
|
|
2756
|
+
voiceRef.current.resume();
|
|
2757
|
+
dispatch({ type: "select", id });
|
|
2758
|
+
}, []);
|
|
2759
|
+
const reset = useCallback3(() => {
|
|
2760
|
+
cancel("Session reset");
|
|
2761
|
+
heldRef.current = false;
|
|
2762
|
+
voiceRef.current.resume();
|
|
2763
|
+
dispatch({ type: "reset" });
|
|
2764
|
+
}, [cancel]);
|
|
2765
|
+
const setMuted = useCallback3((muted) => dispatch({ type: "mute", value: muted }), []);
|
|
2766
|
+
const currentTurn = state.turns.at(-1);
|
|
2767
|
+
const shownTurn = state.turns.find((turn) => turn.id === state.shownTurnId) ?? currentTurn;
|
|
2768
|
+
const availableModes = state.capabilities?.modes ?? ["templates"];
|
|
2769
|
+
const suggestions = shownTurn?.suggestions ?? [];
|
|
2770
|
+
const fullTranscript = shownTurn ? transcriptFor(shownTurn) : [];
|
|
2771
|
+
const transcript = shownTurn && shownTurn === currentTurn && state.playback?.kind !== "video" ? fullTranscript.slice(0, (shownTurn.opening ? 1 : 0) + state.spokenUpTo + 1) : fullTranscript;
|
|
2772
|
+
const playbackKey = state.playerKey;
|
|
2773
|
+
const playerProps = state.playback ? {
|
|
2774
|
+
...options.templates ? { templates: options.templates } : {},
|
|
2775
|
+
...state.playback.kind === "stream" ? { stream: state.playback.stream } : { video: state.playback.video },
|
|
2776
|
+
autoPlay: true,
|
|
2777
|
+
paused: state.status === "paused",
|
|
2778
|
+
controls: false,
|
|
2779
|
+
orientation: shownTurn?.fixedOrientation ? shownTurn.orientation : "auto",
|
|
2780
|
+
onSceneChange: (scene, index) => {
|
|
2781
|
+
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2782
|
+
const firstFrame = firstFrameRef.current;
|
|
2783
|
+
if (index === 0 && firstFrame && !firstFrame.reported && state.playback?.kind === "stream") {
|
|
2784
|
+
firstFrame.reported = true;
|
|
2785
|
+
try {
|
|
2786
|
+
void optionsRef.current.onFirstFrame?.({
|
|
2787
|
+
turnId: firstFrame.turnId,
|
|
2788
|
+
mode: firstFrame.mode,
|
|
2789
|
+
timeToFirstFrameMs: Math.max(0, Math.round(monotonicNow() - firstFrame.startedAt))
|
|
2790
|
+
});
|
|
2791
|
+
} catch {
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
dispatch({ type: "scene", key: playbackKey, scene, index });
|
|
2795
|
+
narrationRef.current.onSceneChange(scene, index);
|
|
2796
|
+
},
|
|
2797
|
+
onPlaybackEnd: () => dispatch({ type: "playback-end", key: playbackKey }),
|
|
2798
|
+
onError: (cause) => {
|
|
2799
|
+
const id = stateRef.current.turns.at(-1)?.id;
|
|
2800
|
+
if (id) dispatch({ type: "error", id, error: errorFrom2(cause) });
|
|
2801
|
+
}
|
|
2802
|
+
} : void 0;
|
|
2803
|
+
return {
|
|
2804
|
+
ask,
|
|
2805
|
+
cancel,
|
|
2806
|
+
pause,
|
|
2807
|
+
resume,
|
|
2808
|
+
replay,
|
|
2809
|
+
selectTurn,
|
|
2810
|
+
reset,
|
|
2811
|
+
turns: state.turns,
|
|
2812
|
+
currentTurn,
|
|
2813
|
+
shownTurn,
|
|
2814
|
+
capabilities: state.capabilities,
|
|
2815
|
+
welcome: state.welcome,
|
|
2816
|
+
availableModes,
|
|
2817
|
+
status: state.status,
|
|
2818
|
+
error: state.error,
|
|
2819
|
+
suggestions,
|
|
2820
|
+
caption: state.caption,
|
|
2821
|
+
transcript,
|
|
2822
|
+
speaking: narration.speaking || state.openingSpeaking,
|
|
2823
|
+
muted: state.muted,
|
|
2824
|
+
setMuted,
|
|
2825
|
+
playbackEnded: state.playbackEnded,
|
|
2826
|
+
playerKey: state.playerKey,
|
|
2827
|
+
playerProps
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
// src/video-chat/video-chat.tsx
|
|
2832
|
+
import { useCallback as useCallback6, useEffect as useEffect9, useId, useMemo as useMemo3, useRef as useRef7, useState as useState6 } from "react";
|
|
2833
|
+
|
|
2834
|
+
// src/video-chat/modes.ts
|
|
2835
|
+
var visualModes = [
|
|
2836
|
+
{ id: "templates", label: "Templates only", note: "Rendered scenes \xB7 free and instant" },
|
|
2837
|
+
{ id: "full", label: "Full AI video", note: "Every beat filmed \xB7 billed per clip" }
|
|
2838
|
+
];
|
|
2839
|
+
var defaultMode = visualModes[0];
|
|
2840
|
+
var modeById = (id) => visualModes.find((mode) => mode.id === id) ?? defaultMode;
|
|
2841
|
+
|
|
2842
|
+
// src/video-chat/themes.ts
|
|
2843
|
+
var GROUND = {
|
|
2844
|
+
font: "Inter",
|
|
2845
|
+
scriptFont: "Caveat",
|
|
2846
|
+
// The brand's indigo into blue. Deeper than it looks like it wants to be:
|
|
2847
|
+
// the renderer holds captions to 4.5:1 against this ground, and the brand's
|
|
2848
|
+
// own bright blue (#00A5FF) measures 2.68:1 under white - so the light end
|
|
2849
|
+
// stays at a blue that clears the bar (5.17:1) and the bright one is spent
|
|
2850
|
+
// on accents instead, where it sits on the dark ground rather than under
|
|
2851
|
+
// white type.
|
|
2852
|
+
background: { colors: ["#1C0277", "#2563eb"] },
|
|
2853
|
+
foreground: "#ffffff",
|
|
2854
|
+
surface: "#1C0277",
|
|
2855
|
+
surfaceElevated: "#2f4fc4",
|
|
2856
|
+
// Not the brand's #7065A5: muted text still has to be read, and that violet
|
|
2857
|
+
// measures 3.66:1 on the dark end of this ground and worse on the light one.
|
|
2858
|
+
// The brand violet earns its place as a surface, not as type.
|
|
2859
|
+
muted: "#d7d3f0"
|
|
2860
|
+
};
|
|
2861
|
+
function look(primary, secondary) {
|
|
2862
|
+
const { font, scriptFont, background, ...colors } = GROUND;
|
|
2863
|
+
return { font, scriptFont, background, colors: { ...colors, primary, secondary } };
|
|
2864
|
+
}
|
|
2865
|
+
var themes = [
|
|
2866
|
+
{
|
|
2867
|
+
id: "documentary",
|
|
2868
|
+
label: "Documentary",
|
|
2869
|
+
brand: look("#FF3A71", "#FFB100"),
|
|
2870
|
+
generatedLook: "Natural daylight, documentary realism, shallow depth of field, fine 35mm grain, muted natural palette. Photoreal, no illustration."
|
|
2871
|
+
},
|
|
2872
|
+
{
|
|
2873
|
+
id: "illustrated",
|
|
2874
|
+
label: "Illustrated",
|
|
2875
|
+
brand: look("#00A5FF", "#FF006F"),
|
|
2876
|
+
generatedLook: "Hand-drawn cel animation, rough ink contours, visible paper grain, flat cobalt, crimson and ochre against a deep ground. No photorealism."
|
|
2877
|
+
},
|
|
2878
|
+
{
|
|
2879
|
+
id: "blueprint",
|
|
2880
|
+
label: "Blueprint",
|
|
2881
|
+
brand: look("#00A5FF", "#FFB100"),
|
|
2882
|
+
generatedLook: "Technical blueprint drawing, precise white linework on deep cyanotype blue, faint measurement grid, one warm accent. Schematic, no photorealism."
|
|
2883
|
+
}
|
|
2884
|
+
];
|
|
2885
|
+
function themeBackground(brand) {
|
|
2886
|
+
const background = brand.background;
|
|
2887
|
+
if (background?.colors && background.colors.length >= 2) {
|
|
2888
|
+
return `linear-gradient(175deg, ${background.colors[0]} 0%, ${background.colors[1]} 100%)`;
|
|
2889
|
+
}
|
|
2890
|
+
return background?.color ?? "#4c2ba8";
|
|
2891
|
+
}
|
|
2892
|
+
var defaultTheme = themes[0];
|
|
2893
|
+
var themeById = (id) => themes.find((theme) => theme.id === id) ?? defaultTheme;
|
|
2894
|
+
|
|
2895
|
+
// src/video-chat/icons.tsx
|
|
2896
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2897
|
+
var stroke = {
|
|
2898
|
+
fill: "none",
|
|
2899
|
+
stroke: "currentColor",
|
|
2900
|
+
strokeWidth: 1.75,
|
|
2901
|
+
strokeLinecap: "round",
|
|
2902
|
+
strokeLinejoin: "round"
|
|
2903
|
+
};
|
|
2904
|
+
function Glyph({ children }) {
|
|
2905
|
+
return /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", ...stroke, children });
|
|
2906
|
+
}
|
|
2907
|
+
var Plus = () => /* @__PURE__ */ jsx3(Glyph, { children: /* @__PURE__ */ jsx3("path", { d: "M12 5v14M5 12h14" }) });
|
|
2908
|
+
var Gear = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2909
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "3" }),
|
|
2910
|
+
/* @__PURE__ */ jsx3("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" })
|
|
2911
|
+
] });
|
|
2912
|
+
var Sound = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2913
|
+
/* @__PURE__ */ jsx3("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
2914
|
+
/* @__PURE__ */ jsx3("path", { d: "M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13" })
|
|
2915
|
+
] });
|
|
2916
|
+
var Muted = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2917
|
+
/* @__PURE__ */ jsx3("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
2918
|
+
/* @__PURE__ */ jsx3("path", { d: "m22 9-6 6M16 9l6 6" })
|
|
2919
|
+
] });
|
|
2920
|
+
var Mic = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2921
|
+
/* @__PURE__ */ jsx3("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3" }),
|
|
2922
|
+
/* @__PURE__ */ jsx3("path", { d: "M5 11a7 7 0 0 0 14 0M12 18v4" })
|
|
2923
|
+
] });
|
|
2924
|
+
var Send = () => /* @__PURE__ */ jsx3(Glyph, { children: /* @__PURE__ */ jsx3("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
|
|
2925
|
+
var Stop = () => /* @__PURE__ */ jsxs3("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: [
|
|
2926
|
+
/* @__PURE__ */ jsx3("rect", { x: "7", y: "5", width: "3.4", height: "14", rx: "1.4" }),
|
|
2927
|
+
/* @__PURE__ */ jsx3("rect", { x: "13.6", y: "5", width: "3.4", height: "14", rx: "1.4" })
|
|
2928
|
+
] });
|
|
2929
|
+
var Play = () => /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: /* @__PURE__ */ jsx3("path", { d: "M8 5.5v13l11-6.5z" }) });
|
|
2930
|
+
var Replay = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2931
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
2932
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 3v5h5" })
|
|
2933
|
+
] });
|
|
2934
|
+
var ChevronUp = () => /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", width: "14", height: "14", "aria-hidden": "true", ...stroke, children: /* @__PURE__ */ jsx3("path", { d: "m6 15 6-6 6 6" }) });
|
|
2935
|
+
var Close = () => /* @__PURE__ */ jsx3(Glyph, { children: /* @__PURE__ */ jsx3("path", { d: "M6 6l12 12M18 6 6 18" }) });
|
|
2936
|
+
var Warning = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
2937
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "9" }),
|
|
2938
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 8v5M12 16.2v.1" })
|
|
2939
|
+
] });
|
|
2940
|
+
|
|
2941
|
+
// src/video-chat/use-dismiss.ts
|
|
2942
|
+
import { useEffect as useEffect6 } from "react";
|
|
2943
|
+
function useDismiss(open, close, surfaces) {
|
|
2944
|
+
useEffect6(() => {
|
|
2945
|
+
if (!open) return;
|
|
2946
|
+
const onKeyDown = (event) => {
|
|
2947
|
+
if (event.key !== "Escape") return;
|
|
2948
|
+
event.stopPropagation();
|
|
2949
|
+
close();
|
|
2950
|
+
};
|
|
2951
|
+
const onPointerDown = (event) => {
|
|
2952
|
+
const target = event.target;
|
|
2953
|
+
if (!target) return;
|
|
2954
|
+
if (surfaces.some((surface) => surface.current?.contains(target))) return;
|
|
2955
|
+
close();
|
|
2956
|
+
};
|
|
2957
|
+
document.addEventListener("keydown", onKeyDown);
|
|
2958
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
2959
|
+
return () => {
|
|
2960
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
2961
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
2962
|
+
};
|
|
2963
|
+
}, [open, close, surfaces]);
|
|
2964
|
+
}
|
|
2965
|
+
function useFocusTrap(active, surface) {
|
|
2966
|
+
useEffect6(() => {
|
|
2967
|
+
if (!active) return;
|
|
2968
|
+
const returnTo = document.activeElement;
|
|
2969
|
+
const focusable = () => Array.from(
|
|
2970
|
+
surface.current?.querySelectorAll(
|
|
2971
|
+
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
|
2972
|
+
) ?? []
|
|
2973
|
+
).filter((node) => node.offsetParent !== null);
|
|
2974
|
+
focusable()[0]?.focus();
|
|
2975
|
+
const onKeyDown = (event) => {
|
|
2976
|
+
if (event.key !== "Tab") return;
|
|
2977
|
+
const nodes = focusable();
|
|
2978
|
+
if (nodes.length === 0) return;
|
|
2979
|
+
const first = nodes[0];
|
|
2980
|
+
const last = nodes[nodes.length - 1];
|
|
2981
|
+
const active2 = document.activeElement;
|
|
2982
|
+
if (event.shiftKey && (active2 === first || !surface.current?.contains(active2))) {
|
|
2983
|
+
event.preventDefault();
|
|
2984
|
+
last.focus();
|
|
2985
|
+
} else if (!event.shiftKey && active2 === last) {
|
|
2986
|
+
event.preventDefault();
|
|
2987
|
+
first.focus();
|
|
2988
|
+
}
|
|
2989
|
+
};
|
|
2990
|
+
document.addEventListener("keydown", onKeyDown);
|
|
2991
|
+
return () => {
|
|
2992
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
2993
|
+
returnTo?.focus?.();
|
|
2994
|
+
};
|
|
2995
|
+
}, [active, surface]);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// src/video-chat/suggestion-cards.tsx
|
|
2999
|
+
import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState4 } from "react";
|
|
3000
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3001
|
+
function Frame({ media, poster, playing }) {
|
|
3002
|
+
const video = useRef5(null);
|
|
3003
|
+
useEffect7(() => {
|
|
3004
|
+
const element = video.current;
|
|
3005
|
+
if (!element || playing === void 0) return;
|
|
3006
|
+
if (playing) void element.play().catch(() => void 0);
|
|
3007
|
+
else element.pause();
|
|
3008
|
+
}, [playing]);
|
|
3009
|
+
if (!media) return null;
|
|
3010
|
+
if (media.type === "image") return /* @__PURE__ */ jsx4("img", { className: "frame-media", src: media.url, alt: "" });
|
|
3011
|
+
return /* @__PURE__ */ jsx4(
|
|
3012
|
+
"video",
|
|
3013
|
+
{
|
|
3014
|
+
ref: video,
|
|
3015
|
+
className: "frame-media",
|
|
3016
|
+
src: media.url,
|
|
3017
|
+
poster: poster ? media.posterUrl : void 0,
|
|
3018
|
+
autoPlay: playing !== false,
|
|
3019
|
+
muted: true,
|
|
3020
|
+
loop: true,
|
|
3021
|
+
playsInline: true,
|
|
3022
|
+
preload: "metadata",
|
|
3023
|
+
"aria-hidden": "true"
|
|
3024
|
+
}
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
function SuggestionCards({ suggestions, label, onAsk }) {
|
|
3028
|
+
const railRef = useRef5(null);
|
|
3029
|
+
const [at, setAt] = useState4(0);
|
|
3030
|
+
const [taken, setTaken] = useState4(false);
|
|
3031
|
+
const take = useCallback4((index) => {
|
|
3032
|
+
setTaken(true);
|
|
3033
|
+
setAt(index);
|
|
3034
|
+
}, []);
|
|
3035
|
+
useEffect7(() => {
|
|
3036
|
+
if (taken || suggestions.length < 2) return;
|
|
3037
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
3038
|
+
const tour = window.setInterval(() => setAt((index) => (index + 1) % suggestions.length), 5e3);
|
|
3039
|
+
return () => window.clearInterval(tour);
|
|
3040
|
+
}, [taken, suggestions.length]);
|
|
3041
|
+
useEffect7(() => {
|
|
3042
|
+
const rail = railRef.current;
|
|
3043
|
+
const card = rail?.children[at];
|
|
3044
|
+
if (!rail || !card) return;
|
|
3045
|
+
if (rail.scrollWidth <= rail.clientWidth + 4) return;
|
|
3046
|
+
const centred = card.offsetLeft - (rail.clientWidth - card.offsetWidth) / 2;
|
|
3047
|
+
rail.scrollTo({ left: Math.max(0, centred), behavior: "smooth" });
|
|
3048
|
+
}, [at]);
|
|
3049
|
+
if (suggestions.length === 0) return null;
|
|
3050
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
3051
|
+
/* @__PURE__ */ jsx4("ul", { className: "cards", ref: railRef, "aria-label": label, children: suggestions.map((card, index) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsxs4(
|
|
3052
|
+
"button",
|
|
3053
|
+
{
|
|
3054
|
+
type: "button",
|
|
3055
|
+
"data-active": index === at ? "" : void 0,
|
|
3056
|
+
onFocus: () => take(index),
|
|
3057
|
+
onPointerEnter: () => take(index),
|
|
3058
|
+
onClick: () => onAsk(card),
|
|
3059
|
+
children: [
|
|
3060
|
+
/* @__PURE__ */ jsx4(Frame, { media: card.media, playing: index === at }),
|
|
3061
|
+
/* @__PURE__ */ jsx4("span", { className: "card-wash", "aria-hidden": "true" }),
|
|
3062
|
+
/* @__PURE__ */ jsx4("span", { className: "card-prompt", children: card.prompt })
|
|
3063
|
+
]
|
|
3064
|
+
}
|
|
3065
|
+
) }, card.prompt)) }),
|
|
3066
|
+
suggestions.length > 1 && /* @__PURE__ */ jsx4("div", { className: "card-dots", role: "group", "aria-label": `${label} navigation`, children: suggestions.map((card, index) => /* @__PURE__ */ jsx4(
|
|
3067
|
+
"button",
|
|
3068
|
+
{
|
|
3069
|
+
type: "button",
|
|
3070
|
+
"aria-pressed": index === at,
|
|
3071
|
+
"aria-label": `Show suggestion ${index + 1}: ${card.prompt}`,
|
|
3072
|
+
className: index === at ? "on" : void 0,
|
|
3073
|
+
onClick: () => take(index)
|
|
3074
|
+
},
|
|
3075
|
+
card.prompt
|
|
3076
|
+
)) })
|
|
3077
|
+
] });
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// src/video-chat/welcome.tsx
|
|
3081
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3082
|
+
function Welcome({ data, onAsk, title }) {
|
|
3083
|
+
return /* @__PURE__ */ jsxs5("div", { className: "welcome", children: [
|
|
3084
|
+
/* @__PURE__ */ jsx5(Frame, { media: data?.hero ?? null, poster: true }),
|
|
3085
|
+
/* @__PURE__ */ jsx5("div", { className: "welcome-wash", "aria-hidden": "true" }),
|
|
3086
|
+
/* @__PURE__ */ jsxs5("div", { className: "welcome-body", children: [
|
|
3087
|
+
/* @__PURE__ */ jsx5("h1", { className: "welcome-title", children: title ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
3088
|
+
"An AI chat that responds",
|
|
3089
|
+
/* @__PURE__ */ jsx5("br", {}),
|
|
3090
|
+
/* @__PURE__ */ jsx5("em", { children: "in video, not text." })
|
|
3091
|
+
] }) }),
|
|
3092
|
+
/* @__PURE__ */ jsx5(SuggestionCards, { suggestions: data?.cards ?? [], label: "Suggested prompts", onAsk })
|
|
3093
|
+
] })
|
|
3094
|
+
] });
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// src/video-chat/use-voice-input.ts
|
|
3098
|
+
import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef6, useState as useState5 } from "react";
|
|
3099
|
+
function recognitionConstructor() {
|
|
3100
|
+
if (typeof window === "undefined") return void 0;
|
|
3101
|
+
const holder = window;
|
|
3102
|
+
return holder.SpeechRecognition ?? holder.webkitSpeechRecognition;
|
|
3103
|
+
}
|
|
3104
|
+
function recorderAvailable() {
|
|
3105
|
+
return typeof window !== "undefined" && typeof window.MediaRecorder === "function" && Boolean(window.navigator.mediaDevices?.getUserMedia);
|
|
3106
|
+
}
|
|
3107
|
+
function supportsVoiceInput(transcriptionAvailable) {
|
|
3108
|
+
return recognitionConstructor() !== void 0 || transcriptionAvailable && recorderAvailable();
|
|
3109
|
+
}
|
|
3110
|
+
var WHY = {
|
|
3111
|
+
"not-allowed": "The microphone is blocked for this site. Allow it in your browser's settings, then try again.",
|
|
3112
|
+
"service-not-allowed": "The microphone is blocked for this site. Allow it in your browser's settings, then try again.",
|
|
3113
|
+
"no-speech": "I did not catch that.",
|
|
3114
|
+
"audio-capture": "No microphone was found."
|
|
3115
|
+
};
|
|
3116
|
+
function actionEndpoint3(endpoint) {
|
|
3117
|
+
const value = String(endpoint);
|
|
3118
|
+
return `${value}${value.includes("?") ? "&" : "?"}action=transcription`;
|
|
3119
|
+
}
|
|
3120
|
+
async function transcribeRecording(clip, signal, options) {
|
|
3121
|
+
const headers = new Headers(options.headers);
|
|
3122
|
+
headers.set("content-type", clip.type);
|
|
3123
|
+
const response = await (options.fetcher ?? fetch)(actionEndpoint3(options.endpoint ?? "/api/video-chat"), {
|
|
3124
|
+
method: "POST",
|
|
3125
|
+
headers,
|
|
3126
|
+
body: clip,
|
|
3127
|
+
signal,
|
|
3128
|
+
credentials: options.credentials
|
|
3129
|
+
});
|
|
3130
|
+
if (!response.ok) {
|
|
3131
|
+
const detail = await response.json().catch(() => ({}));
|
|
3132
|
+
const message = typeof detail.error === "string" ? detail.error : detail.error?.message;
|
|
3133
|
+
throw new Error(message ?? "The recording could not be transcribed.");
|
|
3134
|
+
}
|
|
3135
|
+
return String((await response.json()).text ?? "").trim();
|
|
3136
|
+
}
|
|
3137
|
+
async function recordAndTranscribe(signal, ready, captured, options) {
|
|
3138
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
3139
|
+
let clip;
|
|
3140
|
+
try {
|
|
3141
|
+
if (signal.aborted) return "";
|
|
3142
|
+
const chunks = [];
|
|
3143
|
+
const recorder = new MediaRecorder(stream);
|
|
3144
|
+
recorder.ondataavailable = (event) => {
|
|
3145
|
+
if (event.data.size) chunks.push(event.data);
|
|
3146
|
+
};
|
|
3147
|
+
const finished = new Promise((resolve) => {
|
|
3148
|
+
recorder.onstop = () => resolve();
|
|
3149
|
+
});
|
|
3150
|
+
const finish = () => {
|
|
3151
|
+
if (recorder.state !== "inactive") recorder.stop();
|
|
3152
|
+
};
|
|
3153
|
+
ready(finish);
|
|
3154
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
3155
|
+
if (signal.aborted) return "";
|
|
3156
|
+
recorder.start(250);
|
|
3157
|
+
await finished;
|
|
3158
|
+
signal.removeEventListener("abort", finish);
|
|
3159
|
+
if (signal.aborted || chunks.length === 0) return "";
|
|
3160
|
+
captured();
|
|
3161
|
+
clip = new Blob(chunks, { type: recorder.mimeType || "audio/webm" });
|
|
3162
|
+
} finally {
|
|
3163
|
+
for (const track of stream.getTracks()) track.stop();
|
|
3164
|
+
}
|
|
3165
|
+
return clip ? transcribeRecording(clip, signal, options) : "";
|
|
3166
|
+
}
|
|
3167
|
+
function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {}) {
|
|
3168
|
+
const [supported, setSupported] = useState5(false);
|
|
3169
|
+
const [listening, setListening] = useState5(false);
|
|
3170
|
+
const [error, setError] = useState5();
|
|
3171
|
+
const [thinking, setThinking] = useState5(false);
|
|
3172
|
+
const recognitionRef = useRef6(void 0);
|
|
3173
|
+
const useRecorderRef = useRef6(false);
|
|
3174
|
+
const operationRef = useRef6(void 0);
|
|
3175
|
+
const handlerRef = useRef6(onTranscript);
|
|
3176
|
+
handlerRef.current = onTranscript;
|
|
3177
|
+
useEffect8(() => {
|
|
3178
|
+
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3179
|
+
}, [transcriptionAvailable]);
|
|
3180
|
+
const stop = useCallback5(() => {
|
|
3181
|
+
recognitionRef.current?.abort();
|
|
3182
|
+
recognitionRef.current = void 0;
|
|
3183
|
+
const operation = operationRef.current;
|
|
3184
|
+
operationRef.current = void 0;
|
|
3185
|
+
operation?.controller.abort();
|
|
3186
|
+
operation?.finish?.();
|
|
3187
|
+
setListening(false);
|
|
3188
|
+
setThinking(false);
|
|
3189
|
+
}, []);
|
|
3190
|
+
useEffect8(() => () => {
|
|
3191
|
+
recognitionRef.current?.abort();
|
|
3192
|
+
recognitionRef.current = void 0;
|
|
3193
|
+
const operation = operationRef.current;
|
|
3194
|
+
operationRef.current = void 0;
|
|
3195
|
+
operation?.controller.abort();
|
|
3196
|
+
operation?.finish?.();
|
|
3197
|
+
}, []);
|
|
3198
|
+
const finish = useCallback5(() => {
|
|
3199
|
+
const recognition = recognitionRef.current;
|
|
3200
|
+
if (recognition) {
|
|
3201
|
+
recognition.stop();
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const operation = operationRef.current;
|
|
3205
|
+
if (operation?.finish) {
|
|
3206
|
+
operation.finish();
|
|
3207
|
+
setListening(false);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
stop();
|
|
3211
|
+
}, [stop]);
|
|
3212
|
+
const record = useCallback5(async () => {
|
|
3213
|
+
if (!transcriptionAvailable || !recorderAvailable()) {
|
|
3214
|
+
setError("Server transcription is not configured.");
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
if (operationRef.current) return;
|
|
3218
|
+
const operation = { controller: new AbortController(), finish: void 0 };
|
|
3219
|
+
operationRef.current = operation;
|
|
3220
|
+
setError(void 0);
|
|
3221
|
+
setListening(true);
|
|
3222
|
+
try {
|
|
3223
|
+
const heard = await recordAndTranscribe(
|
|
3224
|
+
operation.controller.signal,
|
|
3225
|
+
(halt) => {
|
|
3226
|
+
if (operationRef.current === operation) operation.finish = halt;
|
|
3227
|
+
},
|
|
3228
|
+
() => {
|
|
3229
|
+
if (operationRef.current !== operation) return;
|
|
3230
|
+
setListening(false);
|
|
3231
|
+
setThinking(true);
|
|
3232
|
+
},
|
|
3233
|
+
options
|
|
3234
|
+
);
|
|
3235
|
+
if (!heard || operation.controller.signal.aborted || operationRef.current !== operation) return;
|
|
3236
|
+
handlerRef.current(heard);
|
|
3237
|
+
} catch (cause) {
|
|
3238
|
+
if (operation.controller.signal.aborted || operationRef.current !== operation) return;
|
|
3239
|
+
setListening(false);
|
|
3240
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3241
|
+
setError(/denied|not allowed/i.test(message) ? "The microphone is blocked for this site. Allow it in your browser's settings, then try again." : message);
|
|
3242
|
+
} finally {
|
|
3243
|
+
if (operationRef.current === operation) {
|
|
3244
|
+
operationRef.current = void 0;
|
|
3245
|
+
setListening(false);
|
|
3246
|
+
setThinking(false);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}, [transcriptionAvailable, options.endpoint, options.headers, options.credentials, options.fetcher]);
|
|
3250
|
+
const toggle = useCallback5(() => {
|
|
3251
|
+
if (listening) {
|
|
3252
|
+
finish();
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
const Recognition = recognitionConstructor();
|
|
3256
|
+
if (!Recognition || useRecorderRef.current) {
|
|
3257
|
+
void record();
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
setError(void 0);
|
|
3261
|
+
const recognition = new Recognition();
|
|
3262
|
+
recognition.lang = document.documentElement.lang || "en-US";
|
|
3263
|
+
recognition.continuous = false;
|
|
3264
|
+
recognition.interimResults = true;
|
|
3265
|
+
recognition.onresult = (event) => {
|
|
3266
|
+
if (recognitionRef.current !== recognition) return;
|
|
3267
|
+
let heard = "";
|
|
3268
|
+
for (let index = event.resultIndex; index < event.results.length; index += 1) {
|
|
3269
|
+
heard += event.results[index]?.[0]?.transcript ?? "";
|
|
3270
|
+
}
|
|
3271
|
+
if (heard.trim()) handlerRef.current(heard.trim());
|
|
3272
|
+
};
|
|
3273
|
+
recognition.onend = () => {
|
|
3274
|
+
if (recognitionRef.current !== recognition) return;
|
|
3275
|
+
recognitionRef.current = void 0;
|
|
3276
|
+
setListening(false);
|
|
3277
|
+
};
|
|
3278
|
+
recognition.onerror = (event) => {
|
|
3279
|
+
if (recognitionRef.current !== recognition) return;
|
|
3280
|
+
recognitionRef.current = void 0;
|
|
3281
|
+
setListening(false);
|
|
3282
|
+
if ((event.error === "network" || event.error === "service-not-allowed") && transcriptionAvailable && recorderAvailable()) {
|
|
3283
|
+
useRecorderRef.current = true;
|
|
3284
|
+
void record();
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
setError(event.error === "network" ? "Speech recognition is unavailable in this browser." : WHY[event.error ?? ""] ?? "The microphone stopped unexpectedly.");
|
|
3288
|
+
};
|
|
3289
|
+
recognitionRef.current = recognition;
|
|
3290
|
+
try {
|
|
3291
|
+
recognition.start();
|
|
3292
|
+
setListening(true);
|
|
3293
|
+
} catch {
|
|
3294
|
+
setListening(false);
|
|
3295
|
+
setError("The microphone could not be started.");
|
|
3296
|
+
}
|
|
3297
|
+
}, [listening, finish, record, transcriptionAvailable]);
|
|
3298
|
+
return { supported, listening, thinking, error, toggle, stop };
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// src/video-chat/video-chat.tsx
|
|
3302
|
+
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3303
|
+
var DESKTOP_WIDTH = 900;
|
|
3304
|
+
function useViewportOrientation() {
|
|
3305
|
+
const [portrait, setPortrait] = useState6(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`).matches : false);
|
|
3306
|
+
useEffect9(() => {
|
|
3307
|
+
if (typeof window.matchMedia !== "function") return;
|
|
3308
|
+
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`);
|
|
3309
|
+
const update = () => setPortrait(query.matches);
|
|
3310
|
+
update();
|
|
3311
|
+
query.addEventListener("change", update);
|
|
3312
|
+
return () => query.removeEventListener("change", update);
|
|
3313
|
+
}, []);
|
|
3314
|
+
return portrait ? "portrait" : "landscape";
|
|
3315
|
+
}
|
|
3316
|
+
var STEPS = [
|
|
3317
|
+
"Rolling camera\u2026",
|
|
3318
|
+
"Framing the shots\u2026",
|
|
3319
|
+
"Filming your response\u2026",
|
|
3320
|
+
"Still filming\u2026"
|
|
3321
|
+
];
|
|
3322
|
+
function useFilmingStep(active, key) {
|
|
3323
|
+
const [step, setStep] = useState6(0);
|
|
3324
|
+
useEffect9(() => {
|
|
3325
|
+
if (!active) return;
|
|
3326
|
+
setStep(0);
|
|
3327
|
+
const stepper = window.setInterval(() => setStep((current) => current + 1), 3e3);
|
|
3328
|
+
return () => window.clearInterval(stepper);
|
|
3329
|
+
}, [active, key]);
|
|
3330
|
+
return STEPS[Math.min(step, STEPS.length - 1)];
|
|
3331
|
+
}
|
|
3332
|
+
function Waveform({ active, listening }) {
|
|
3333
|
+
return /* @__PURE__ */ jsx6("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx6("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
|
|
3334
|
+
}
|
|
3335
|
+
function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
3336
|
+
const [draft, setDraft] = useState6("");
|
|
3337
|
+
const [themeId, setThemeId] = useState6(defaultTheme.id);
|
|
3338
|
+
const [modeId, setModeId] = useState6(options.mode ?? defaultMode.id);
|
|
3339
|
+
const [historyOpen, setHistoryOpen] = useState6(false);
|
|
3340
|
+
const [settingsOpen, setSettingsOpen] = useState6(false);
|
|
3341
|
+
const [sheetOpen, setSheetOpen] = useState6(false);
|
|
3342
|
+
const viewportOrientation = useViewportOrientation();
|
|
3343
|
+
const [themeChoice, setThemeChoice] = useState6("system");
|
|
3344
|
+
const theme = themeById(themeId);
|
|
3345
|
+
const sessionOrientation = options.orientation ?? viewportOrientation;
|
|
3346
|
+
const { brand: configuredBrand, style: configuredStyle, ...sessionOptions } = options;
|
|
3347
|
+
const presentationBrand = configuredBrand ?? theme.brand;
|
|
3348
|
+
const chat = useVideoChat({
|
|
3349
|
+
...sessionOptions,
|
|
3350
|
+
mode: modeId,
|
|
3351
|
+
orientation: sessionOrientation,
|
|
3352
|
+
brand: presentationBrand,
|
|
3353
|
+
style: { generatedLook: theme.generatedLook, ...configuredStyle }
|
|
3354
|
+
});
|
|
3355
|
+
const instanceId = useId();
|
|
3356
|
+
const historyId = `${instanceId}-history`;
|
|
3357
|
+
const settingsId = `${instanceId}-settings`;
|
|
3358
|
+
const promptId = `${instanceId}-prompt`;
|
|
3359
|
+
const responseTitleId = `${instanceId}-response-title`;
|
|
3360
|
+
const appearanceName = `${instanceId}-appearance`;
|
|
3361
|
+
const visualsName = `${instanceId}-visuals`;
|
|
3362
|
+
const styleName = `${instanceId}-style`;
|
|
3363
|
+
const inputRef = useRef7(null);
|
|
3364
|
+
const historyRef = useRef7(null);
|
|
3365
|
+
const historyButtonRef = useRef7(null);
|
|
3366
|
+
const settingsRef = useRef7(null);
|
|
3367
|
+
const settingsButtonRef = useRef7(null);
|
|
3368
|
+
const sheetRef = useRef7(null);
|
|
3369
|
+
const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
|
|
3370
|
+
endpoint: options.endpoint,
|
|
3371
|
+
headers: options.headers,
|
|
3372
|
+
credentials: options.credentials,
|
|
3373
|
+
fetcher: options.fetcher
|
|
3374
|
+
});
|
|
3375
|
+
const historySurfaces = useMemo3(() => [historyRef, historyButtonRef], []);
|
|
3376
|
+
const settingsSurfaces = useMemo3(() => [settingsRef, settingsButtonRef], []);
|
|
3377
|
+
const noSurfaces = useMemo3(() => [], []);
|
|
3378
|
+
const closeHistory = useCallback6(() => setHistoryOpen(false), []);
|
|
3379
|
+
const closeSettings = useCallback6(() => setSettingsOpen(false), []);
|
|
3380
|
+
const closeSheet = useCallback6(() => setSheetOpen(false), []);
|
|
3381
|
+
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
3382
|
+
useDismiss(settingsOpen, closeSettings, settingsSurfaces);
|
|
3383
|
+
useDismiss(sheetOpen, closeSheet, noSurfaces);
|
|
3384
|
+
useFocusTrap(sheetOpen, sheetRef);
|
|
3385
|
+
const ask = useCallback6((value) => {
|
|
3386
|
+
const prompt = (typeof value === "string" ? value : value.prompt).trim();
|
|
3387
|
+
if (!prompt) return;
|
|
3388
|
+
setDraft("");
|
|
3389
|
+
listen.stop();
|
|
3390
|
+
setHistoryOpen(false);
|
|
3391
|
+
setSettingsOpen(false);
|
|
3392
|
+
setSheetOpen(false);
|
|
3393
|
+
void chat.ask(prompt, typeof value === "string" ? void 0 : {
|
|
3394
|
+
openingMedia: value.media,
|
|
3395
|
+
opening: value.opening
|
|
3396
|
+
});
|
|
3397
|
+
}, [chat, listen]);
|
|
3398
|
+
const newSession = useCallback6(() => {
|
|
3399
|
+
if (chat.turns.length === 0) {
|
|
3400
|
+
inputRef.current?.focus();
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
chat.reset();
|
|
3404
|
+
setHistoryOpen(false);
|
|
3405
|
+
setSettingsOpen(false);
|
|
3406
|
+
setSheetOpen(false);
|
|
3407
|
+
}, [chat]);
|
|
3408
|
+
const current = chat.currentTurn;
|
|
3409
|
+
const shown = chat.shownTurn;
|
|
3410
|
+
const showing = chat.playerProps != null;
|
|
3411
|
+
const waitingForPicture = chat.turns.length > 0 && !showing && (chat.status === "composing" || chat.status === "playing" || chat.status === "paused");
|
|
3412
|
+
const filmingStep = useFilmingStep(waitingForPicture, current?.id);
|
|
3413
|
+
const status = chat.turns.length === 0 ? "idle" : chat.status === "composing" ? "drawing" : chat.status === "playing" ? "narrating" : chat.status === "error" || chat.status === "cancelled" ? "ended" : chat.status;
|
|
3414
|
+
const shownOrientation = shown?.orientation ?? sessionOrientation;
|
|
3415
|
+
const stageOrientation = shown?.fixedOrientation ? shownOrientation : sessionOrientation;
|
|
3416
|
+
const availableModes = visualModes.filter((option) => chat.availableModes.includes(option.id));
|
|
3417
|
+
const selectedMode = modeById(modeId);
|
|
3418
|
+
const line = chat.caption ?? "";
|
|
3419
|
+
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx6(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx6(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx6(Replay, {}) } : void 0;
|
|
3420
|
+
return /* @__PURE__ */ jsxs6(
|
|
3421
|
+
"div",
|
|
3422
|
+
{
|
|
3423
|
+
className: `vanillasky-video-chat${className ? ` ${className}` : ""}`,
|
|
3424
|
+
"data-orientation": stageOrientation,
|
|
3425
|
+
"data-theme": themeChoice,
|
|
3426
|
+
children: [
|
|
3427
|
+
/* @__PURE__ */ jsxs6("header", { className: "chrome", children: [
|
|
3428
|
+
/* @__PURE__ */ jsxs6("div", { className: "group", children: [
|
|
3429
|
+
/* @__PURE__ */ jsx6(
|
|
3430
|
+
"button",
|
|
3431
|
+
{
|
|
3432
|
+
type: "button",
|
|
3433
|
+
className: "round",
|
|
3434
|
+
"aria-label": chat.turns.length === 0 ? "Ask a prompt" : "New session",
|
|
3435
|
+
onClick: newSession,
|
|
3436
|
+
children: /* @__PURE__ */ jsx6(Plus, {})
|
|
3437
|
+
}
|
|
3438
|
+
),
|
|
3439
|
+
chat.turns.length > 0 && /* @__PURE__ */ jsxs6(
|
|
3440
|
+
"button",
|
|
3441
|
+
{
|
|
3442
|
+
ref: historyButtonRef,
|
|
3443
|
+
type: "button",
|
|
3444
|
+
className: "pill",
|
|
3445
|
+
"aria-expanded": historyOpen,
|
|
3446
|
+
"aria-controls": historyId,
|
|
3447
|
+
onClick: () => {
|
|
3448
|
+
setHistoryOpen((open) => !open);
|
|
3449
|
+
setSettingsOpen(false);
|
|
3450
|
+
},
|
|
3451
|
+
children: [
|
|
3452
|
+
chat.turns.length,
|
|
3453
|
+
" asked"
|
|
3454
|
+
]
|
|
3455
|
+
}
|
|
3456
|
+
)
|
|
3457
|
+
] }),
|
|
3458
|
+
/* @__PURE__ */ jsxs6("div", { className: "group", children: [
|
|
3459
|
+
/* @__PURE__ */ jsx6(
|
|
3460
|
+
"button",
|
|
3461
|
+
{
|
|
3462
|
+
ref: settingsButtonRef,
|
|
3463
|
+
type: "button",
|
|
3464
|
+
className: "round",
|
|
3465
|
+
"aria-label": "Settings",
|
|
3466
|
+
"aria-expanded": settingsOpen,
|
|
3467
|
+
"aria-controls": settingsId,
|
|
3468
|
+
"aria-haspopup": "dialog",
|
|
3469
|
+
onClick: () => {
|
|
3470
|
+
setSettingsOpen((open) => !open);
|
|
3471
|
+
setHistoryOpen(false);
|
|
3472
|
+
},
|
|
3473
|
+
children: /* @__PURE__ */ jsx6(Gear, {})
|
|
3474
|
+
}
|
|
3475
|
+
),
|
|
3476
|
+
/* @__PURE__ */ jsx6(
|
|
3477
|
+
"button",
|
|
3478
|
+
{
|
|
3479
|
+
type: "button",
|
|
3480
|
+
className: "round",
|
|
3481
|
+
"aria-label": chat.muted ? "Turn the voice on" : "Turn the voice off",
|
|
3482
|
+
"aria-pressed": chat.muted,
|
|
3483
|
+
onClick: () => chat.setMuted(!chat.muted),
|
|
3484
|
+
children: chat.muted ? /* @__PURE__ */ jsx6(Muted, {}) : /* @__PURE__ */ jsx6(Sound, {})
|
|
3485
|
+
}
|
|
3486
|
+
)
|
|
3487
|
+
] })
|
|
3488
|
+
] }),
|
|
3489
|
+
/* @__PURE__ */ jsxs6("div", { className: "stage-area", children: [
|
|
3490
|
+
/* @__PURE__ */ jsxs6("div", { className: "stage", style: { background: themeBackground(presentationBrand) }, children: [
|
|
3491
|
+
!showing && /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
3492
|
+
/* @__PURE__ */ jsx6("div", { className: "ground", "aria-hidden": "true" }),
|
|
3493
|
+
shown?.openingMedia && /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
3494
|
+
/* @__PURE__ */ jsx6(Frame, { media: shown.openingMedia, poster: true }),
|
|
3495
|
+
/* @__PURE__ */ jsx6("div", { className: "opening-wash", "aria-hidden": "true" })
|
|
3496
|
+
] }),
|
|
3497
|
+
chat.turns.length === 0 && /* @__PURE__ */ jsx6(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
|
|
3498
|
+
shown?.prompt && /* @__PURE__ */ jsxs6("div", { className: "asked", children: [
|
|
3499
|
+
/* @__PURE__ */ jsx6("p", { className: "asked-prompt", children: shown.prompt }),
|
|
3500
|
+
waitingForPicture && /* @__PURE__ */ jsx6("p", { className: "asked-step", "aria-live": "polite", children: filmingStep })
|
|
3501
|
+
] })
|
|
3502
|
+
] }),
|
|
3503
|
+
chat.playerProps && /* @__PURE__ */ jsx6(
|
|
3504
|
+
VideoPlayer,
|
|
3505
|
+
{
|
|
3506
|
+
...chat.playerProps,
|
|
3507
|
+
templates: options.templates,
|
|
3508
|
+
responsiveBreakpoint: DESKTOP_WIDTH,
|
|
3509
|
+
ariaLabel: chat.playerProps.video ? "Replay" : "The response"
|
|
3510
|
+
},
|
|
3511
|
+
chat.playerKey
|
|
3512
|
+
),
|
|
3513
|
+
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "ending", children: [
|
|
3514
|
+
/* @__PURE__ */ jsx6("div", { className: "ending-wash", "aria-hidden": "true" }),
|
|
3515
|
+
/* @__PURE__ */ jsxs6("div", { className: "ending-body", children: [
|
|
3516
|
+
/* @__PURE__ */ jsx6("p", { className: "ending-label", children: "Ask next" }),
|
|
3517
|
+
/* @__PURE__ */ jsx6(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
|
|
3518
|
+
] })
|
|
3519
|
+
] })
|
|
3520
|
+
] }),
|
|
3521
|
+
historyOpen && /* @__PURE__ */ jsx6("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", "aria-label": "Prompts this session", children: chat.turns.map((turn, index) => /* @__PURE__ */ jsxs6(
|
|
3522
|
+
"button",
|
|
3523
|
+
{
|
|
3524
|
+
type: "button",
|
|
3525
|
+
className: turn.id === shown?.id ? "current" : void 0,
|
|
3526
|
+
disabled: !turn.completed || !turn.video,
|
|
3527
|
+
onClick: () => {
|
|
3528
|
+
chat.selectTurn(turn.id);
|
|
3529
|
+
setHistoryOpen(false);
|
|
3530
|
+
},
|
|
3531
|
+
children: [
|
|
3532
|
+
/* @__PURE__ */ jsx6("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
|
|
3533
|
+
/* @__PURE__ */ jsx6("span", { className: "prompt", children: turn.prompt })
|
|
3534
|
+
]
|
|
3535
|
+
},
|
|
3536
|
+
turn.id
|
|
3537
|
+
)) }),
|
|
3538
|
+
settingsOpen && /* @__PURE__ */ jsxs6(
|
|
3539
|
+
"div",
|
|
3540
|
+
{
|
|
3541
|
+
ref: settingsRef,
|
|
3542
|
+
id: settingsId,
|
|
3543
|
+
className: "sheet-popover settings",
|
|
3544
|
+
role: "dialog",
|
|
3545
|
+
"aria-label": "Settings",
|
|
3546
|
+
children: [
|
|
3547
|
+
/* @__PURE__ */ jsxs6("fieldset", { children: [
|
|
3548
|
+
/* @__PURE__ */ jsx6("legend", { children: "Appearance" }),
|
|
3549
|
+
[["system", "Match system"], ["light", "Light"], ["dark", "Dark"]].map(([value, label]) => /* @__PURE__ */ jsxs6("label", { children: [
|
|
3550
|
+
/* @__PURE__ */ jsx6("input", { type: "radio", name: appearanceName, checked: value === themeChoice, onChange: () => setThemeChoice(value) }),
|
|
3551
|
+
/* @__PURE__ */ jsx6("span", { children: /* @__PURE__ */ jsx6("strong", { children: label }) })
|
|
3552
|
+
] }, value))
|
|
3553
|
+
] }),
|
|
3554
|
+
/* @__PURE__ */ jsx6("p", { className: "settings-note", children: "Visuals and style apply to your next prompt." }),
|
|
3555
|
+
/* @__PURE__ */ jsxs6("fieldset", { children: [
|
|
3556
|
+
/* @__PURE__ */ jsx6("legend", { children: "Visuals" }),
|
|
3557
|
+
availableModes.map((option) => /* @__PURE__ */ jsxs6("label", { children: [
|
|
3558
|
+
/* @__PURE__ */ jsx6("input", { type: "radio", name: visualsName, checked: option.id === selectedMode.id, onChange: () => setModeId(option.id) }),
|
|
3559
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
3560
|
+
/* @__PURE__ */ jsx6("strong", { children: option.label }),
|
|
3561
|
+
option.note
|
|
3562
|
+
] })
|
|
3563
|
+
] }, option.id))
|
|
3564
|
+
] }),
|
|
3565
|
+
/* @__PURE__ */ jsxs6("fieldset", { children: [
|
|
3566
|
+
/* @__PURE__ */ jsx6("legend", { children: "Style" }),
|
|
3567
|
+
themes.map((option) => /* @__PURE__ */ jsxs6("label", { children: [
|
|
3568
|
+
/* @__PURE__ */ jsx6("input", { type: "radio", name: styleName, checked: option.id === themeId, onChange: () => setThemeId(option.id) }),
|
|
3569
|
+
/* @__PURE__ */ jsx6("span", { children: /* @__PURE__ */ jsx6("strong", { children: option.label }) })
|
|
3570
|
+
] }, option.id))
|
|
3571
|
+
] })
|
|
3572
|
+
]
|
|
3573
|
+
}
|
|
3574
|
+
)
|
|
3575
|
+
] }),
|
|
3576
|
+
/* @__PURE__ */ jsx6("div", { className: "panel", children: /* @__PURE__ */ jsxs6("div", { className: "panel-inner", children: [
|
|
3577
|
+
/* @__PURE__ */ jsxs6("div", { className: "line-row", children: [
|
|
3578
|
+
/* @__PURE__ */ jsx6("p", { className: "line", "aria-live": "polite", children: line }),
|
|
3579
|
+
shown?.video && /* @__PURE__ */ jsxs6("button", { type: "button", className: "full-response", onClick: () => setSheetOpen(true), children: [
|
|
3580
|
+
"Full response ",
|
|
3581
|
+
/* @__PURE__ */ jsx6(ChevronUp, {})
|
|
3582
|
+
] })
|
|
3583
|
+
] }),
|
|
3584
|
+
(chat.error || listen.error) && /* @__PURE__ */ jsxs6("p", { className: "error", role: "status", children: [
|
|
3585
|
+
/* @__PURE__ */ jsx6(Warning, {}),
|
|
3586
|
+
/* @__PURE__ */ jsx6("span", { children: chat.error?.message ?? listen.error })
|
|
3587
|
+
] }),
|
|
3588
|
+
/* @__PURE__ */ jsxs6("form", { className: "composer", onSubmit: (event) => {
|
|
3589
|
+
event.preventDefault();
|
|
3590
|
+
ask(draft);
|
|
3591
|
+
}, children: [
|
|
3592
|
+
/* @__PURE__ */ jsx6(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
|
|
3593
|
+
/* @__PURE__ */ jsx6("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
|
|
3594
|
+
/* @__PURE__ */ jsx6(
|
|
3595
|
+
"textarea",
|
|
3596
|
+
{
|
|
3597
|
+
id: promptId,
|
|
3598
|
+
ref: inputRef,
|
|
3599
|
+
rows: 1,
|
|
3600
|
+
value: draft,
|
|
3601
|
+
placeholder: listen.listening ? "Listening\u2026" : listen.thinking ? "Working out what you said\u2026" : status === "narrating" ? "Talking \u2014 type to jump in\u2026" : chat.turns.length > 0 ? "Ask a follow-up\u2026" : "Ask anything\u2026",
|
|
3602
|
+
onChange: (event) => setDraft(event.target.value),
|
|
3603
|
+
onKeyDown: (event) => {
|
|
3604
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
3605
|
+
event.preventDefault();
|
|
3606
|
+
ask(draft);
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
),
|
|
3611
|
+
transport && /* @__PURE__ */ jsx6("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: transport.action, children: transport.icon }),
|
|
3612
|
+
listen.supported && /* @__PURE__ */ jsx6(
|
|
3613
|
+
"button",
|
|
3614
|
+
{
|
|
3615
|
+
type: "button",
|
|
3616
|
+
className: `ghost${listen.listening ? " listening" : ""}`,
|
|
3617
|
+
"aria-label": listen.listening ? "Stop listening" : "Ask by voice",
|
|
3618
|
+
"aria-pressed": listen.listening,
|
|
3619
|
+
disabled: listen.thinking,
|
|
3620
|
+
onClick: listen.toggle,
|
|
3621
|
+
children: /* @__PURE__ */ jsx6(Mic, {})
|
|
3622
|
+
}
|
|
3623
|
+
),
|
|
3624
|
+
/* @__PURE__ */ jsx6("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || status === "drawing", children: /* @__PURE__ */ jsx6(Send, {}) })
|
|
3625
|
+
] })
|
|
3626
|
+
] }) }),
|
|
3627
|
+
sheetOpen && shown && /* @__PURE__ */ jsxs6("div", { className: "sheet-layer", children: [
|
|
3628
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "scrim", tabIndex: -1, "aria-hidden": "true", onClick: () => setSheetOpen(false) }),
|
|
3629
|
+
/* @__PURE__ */ jsxs6("article", { ref: sheetRef, className: "sheet", role: "dialog", "aria-modal": "true", "aria-labelledby": responseTitleId, children: [
|
|
3630
|
+
/* @__PURE__ */ jsxs6("header", { children: [
|
|
3631
|
+
/* @__PURE__ */ jsx6("h1", { id: responseTitleId, children: shown.prompt }),
|
|
3632
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "round", "aria-label": "Close the full response", onClick: () => setSheetOpen(false), children: /* @__PURE__ */ jsx6(Close, {}) })
|
|
3633
|
+
] }),
|
|
3634
|
+
/* @__PURE__ */ jsx6("div", { className: "sheet-body", children: chat.transcript.map((entry, index) => /* @__PURE__ */ jsx6("p", { children: entry }, `${index}-${entry}`)) })
|
|
3635
|
+
] })
|
|
3636
|
+
] })
|
|
3637
|
+
]
|
|
3638
|
+
}
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
1836
3641
|
export {
|
|
3642
|
+
VideoChat,
|
|
1837
3643
|
VideoError,
|
|
1838
3644
|
VideoPlayer,
|
|
1839
|
-
|
|
3645
|
+
createVideoChatVoice,
|
|
3646
|
+
useNarration,
|
|
3647
|
+
useVideo,
|
|
3648
|
+
useVideoChat
|
|
1840
3649
|
};
|