@vanillaskyai/video 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/PUBLIC-API.md +83 -79
- package/README.md +20 -15
- package/dist/assets/vanillasky-logo.svg +151 -0
- package/dist/{builtin-server-KB7FKF6A.js → builtin-server-CUYPIDE7.js} +2 -2
- package/dist/check-runtime.js +1 -1
- package/dist/{chunk-2PYO6VAC.js → chunk-2DOLF4QN.js} +1 -43
- package/dist/{chunk-GAIOHGNR.js → chunk-2OKB27TG.js} +42 -45
- package/dist/{chunk-K746NUIP.js → chunk-2USVB3CY.js} +80 -29
- package/dist/{chunk-RE4IMWJR.js → chunk-3VRMKFM7.js} +87 -14
- package/dist/{chunk-JNN3EYVP.js → chunk-4UNKZEAT.js} +46 -46
- package/dist/{chunk-2XT4MZ76.js → chunk-APQKY6P3.js} +36 -3
- package/dist/{chunk-LLPMVDJZ.js → chunk-L7QGGKJ2.js} +69 -38
- package/dist/{chunk-RGF452LL.js → chunk-OWBT45EN.js} +1 -1
- package/dist/{chunk-LKBZX7GV.js → chunk-PP3MCPN4.js} +4 -4
- package/dist/chunk-ZD2VTUYR.js +28 -0
- package/dist/cli.js +131 -24
- package/dist/{compose-video-PD6LKKRK.js → compose-video-2NFH4DZY.js} +3 -3
- package/dist/{events-B6qS1Lsb.d.ts → events-BQ6z264n.d.ts} +1 -1
- package/dist/index.d.ts +5 -67
- package/dist/index.js +2 -4
- package/dist/{kit-8g46H2RZ.d.ts → kit-tQnCPMOM.d.ts} +1 -1
- package/dist/react.d.ts +33 -53
- package/dist/react.js +982 -648
- package/dist/server.d.ts +11 -13
- package/dist/server.js +170 -81
- 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-ROTNGSD4.js → text-stream-54LXABJW.js} +1 -1
- package/dist/{types-2wHBqtg8.d.ts → types-CEh9eUFu.d.ts} +1 -1
- package/dist/{types-DrABlRa7.d.ts → types-DWFO6qca.d.ts} +1 -1
- package/docs/agent-integration.md +9 -3
- package/docs/architecture.md +8 -10
- package/docs/concepts.md +16 -40
- package/docs/custom-templates.md +27 -23
- package/docs/customization.md +23 -55
- package/docs/errors.md +58 -68
- package/docs/getting-started.md +21 -14
- package/docs/immersive-interface.md +87 -0
- package/docs/media-and-audio.md +2 -2
- package/docs/performance.md +69 -0
- package/docs/persistence.md +12 -27
- package/docs/production.md +2 -2
- package/docs/provider-integration.md +11 -5
- package/docs/reference/design-system.html +115 -0
- package/docs/reference/provider-adapters.md +37 -89
- package/docs/security.md +1 -1
- package/docs/streaming-protocol.md +2 -2
- package/examples/custom-template/README.md +6 -6
- package/package.json +6 -7
- package/registry/items/barChart.json +2 -2
- package/registry/items/bigNumber.json +2 -2
- package/registry/items/brandMessage.json +2 -2
- package/registry/items/cardList.json +2 -2
- package/registry/items/codeEditor.json +2 -2
- package/registry/items/ctaLogo.json +2 -2
- package/registry/items/ctaMedia.json +2 -2
- package/registry/items/incomingCall.json +2 -2
- package/registry/items/media.json +2 -2
- package/registry/items/milestone.json +2 -2
- package/registry/items/notification.json +2 -2
- package/registry/items/phoneMockup.json +2 -2
- package/registry/items/problemSolution.json +2 -2
- package/registry/items/progressRing.json +2 -2
- package/registry/items/promptInput.json +2 -2
- package/registry/items/reaction.json +2 -2
- package/registry/items/reviewStack.json +2 -2
- package/registry/items/steps.json +2 -2
- package/registry/items/terminal.json +2 -2
- package/registry/items/testimonial.json +2 -2
- package/registry/items/tripleStats.json +2 -2
- package/registry/items/tweet.json +2 -2
- package/registry/items/webMockup.json +2 -2
- package/starters/video-chat/.env.example +4 -2
- package/starters/video-chat/README.md +17 -5
- package/starters/video-chat/index.html +2 -1
- package/starters/video-chat/package.json +2 -4
- package/starters/video-chat/providers/speech.ts +20 -0
- package/starters/video-chat/providers/video.ts +66 -0
- package/starters/video-chat/providers.ts +3 -0
- package/starters/video-chat/server.ts +4 -80
- package/starters/video-chat/stock.ts +114 -99
- package/styles/video-chat.css +650 -246
- package/dist/chunk-SKRGRKHY.js +0 -142
- package/dist/state-DZcKuS32.d.ts +0 -3
- package/docs/branding-and-personalization.md +0 -87
- package/examples/custom-template/supplied-media.tsx +0 -92
package/dist/react.js
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createSceneTimeline,
|
|
3
2
|
getSceneDuration
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZD2VTUYR.js";
|
|
5
4
|
import {
|
|
6
|
-
VIDEO_STREAM_CONTENT_TYPE,
|
|
7
|
-
VIDEO_STREAM_HEADER,
|
|
8
5
|
decodeVideoSse,
|
|
9
|
-
sanitizeVideoChatMedia
|
|
10
|
-
|
|
6
|
+
sanitizeVideoChatMedia,
|
|
7
|
+
withDeadline
|
|
8
|
+
} from "./chunk-APQKY6P3.js";
|
|
11
9
|
import {
|
|
12
10
|
applyVideoEvent,
|
|
13
|
-
|
|
11
|
+
checksumVideo,
|
|
12
|
+
createVideoEventFactory,
|
|
14
13
|
createVideoState
|
|
15
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-2OKB27TG.js";
|
|
16
15
|
import {
|
|
17
16
|
safePublicDiagnostic
|
|
18
17
|
} from "./chunk-MMUXVA47.js";
|
|
19
18
|
import {
|
|
20
19
|
parseVideo
|
|
21
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2DOLF4QN.js";
|
|
22
21
|
import {
|
|
23
|
-
|
|
24
|
-
createVideoRequest
|
|
22
|
+
VIDEO_SCHEMA_VERSION
|
|
25
23
|
} from "./chunk-AOWSXU2K.js";
|
|
26
24
|
import {
|
|
27
25
|
getBuiltinTemplateMetadata
|
|
28
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-4UNKZEAT.js";
|
|
29
27
|
import "./chunk-3O7OMMMF.js";
|
|
30
28
|
import {
|
|
31
29
|
assertTemplateTransitionMetadata
|
|
@@ -33,7 +31,7 @@ import {
|
|
|
33
31
|
import {
|
|
34
32
|
VideoFrame,
|
|
35
33
|
getDimensions
|
|
36
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-2USVB3CY.js";
|
|
37
35
|
import "./chunk-224QNWRA.js";
|
|
38
36
|
import "./chunk-5CDAM24P.js";
|
|
39
37
|
import {
|
|
@@ -43,7 +41,9 @@ import {
|
|
|
43
41
|
getVideoDuration,
|
|
44
42
|
resolveVideoTimeline
|
|
45
43
|
} from "./chunk-SPVTJH3F.js";
|
|
46
|
-
import
|
|
44
|
+
import {
|
|
45
|
+
resolveVideoBrand
|
|
46
|
+
} from "./chunk-XGRA2MUV.js";
|
|
47
47
|
import "./chunk-WJ6FLKNC.js";
|
|
48
48
|
import "./chunk-73NTSFFI.js";
|
|
49
49
|
|
|
@@ -1018,6 +1018,15 @@ function usePlaybackClock({
|
|
|
1018
1018
|
}) {
|
|
1019
1019
|
useEffect(() => {
|
|
1020
1020
|
if (!isPlaying) return;
|
|
1021
|
+
let stalled = false;
|
|
1022
|
+
const reportStall = (next) => {
|
|
1023
|
+
if (stalled === next) return;
|
|
1024
|
+
stalled = next;
|
|
1025
|
+
try {
|
|
1026
|
+
void Promise.resolve(callbacksRef.current.onStallChange?.(next)).catch(() => void 0);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1021
1030
|
let frame = 0;
|
|
1022
1031
|
let previous = performance.now();
|
|
1023
1032
|
const tick = (now) => {
|
|
@@ -1067,11 +1076,15 @@ function usePlaybackClock({
|
|
|
1067
1076
|
}
|
|
1068
1077
|
}
|
|
1069
1078
|
const duration = current.config ? getVideoDuration(current.config) : 0;
|
|
1079
|
+
reportStall(!settled && Boolean(current.config?.scenes.length) && duration > 0 && timeRef.current >= duration);
|
|
1070
1080
|
if (!settled || looping || timeRef.current < duration) frame = requestAnimationFrame(tick);
|
|
1071
1081
|
else setIsPlaying(false);
|
|
1072
1082
|
};
|
|
1073
1083
|
frame = requestAnimationFrame(tick);
|
|
1074
|
-
return () =>
|
|
1084
|
+
return () => {
|
|
1085
|
+
cancelAnimationFrame(frame);
|
|
1086
|
+
reportStall(false);
|
|
1087
|
+
};
|
|
1075
1088
|
}, [isPlaying]);
|
|
1076
1089
|
}
|
|
1077
1090
|
|
|
@@ -1106,6 +1119,8 @@ function VideoPlayerRuntime({
|
|
|
1106
1119
|
onComplete,
|
|
1107
1120
|
onError,
|
|
1108
1121
|
onSceneChange,
|
|
1122
|
+
onFramePresented,
|
|
1123
|
+
onStallChange,
|
|
1109
1124
|
onStateChange
|
|
1110
1125
|
}) {
|
|
1111
1126
|
const nativeMediaVolume = Math.max(0, Math.min(
|
|
@@ -1140,14 +1155,25 @@ function VideoPlayerRuntime({
|
|
|
1140
1155
|
const timeRef = useRef(currentTime);
|
|
1141
1156
|
const audioRef = useRef(null);
|
|
1142
1157
|
const introStartedAtRef = useRef(autoStartGeneration ? performance.now() : null);
|
|
1143
|
-
const callbacksRef = useRef({ onComplete, onPlaybackEnd, onError, onSceneChange, onStateChange });
|
|
1158
|
+
const callbacksRef = useRef({ onComplete, onPlaybackEnd, onError, onSceneChange, onFramePresented, onStallChange, onStateChange });
|
|
1144
1159
|
const loopRef = useRef(loop);
|
|
1145
1160
|
const sceneIndexRef = useRef(-1);
|
|
1146
1161
|
const playbackEndedRef = useRef(false);
|
|
1147
1162
|
stateRef.current = state;
|
|
1148
1163
|
timeRef.current = currentTime;
|
|
1149
|
-
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, onStateChange };
|
|
1164
|
+
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, onFramePresented, onStallChange, onStateChange };
|
|
1150
1165
|
loopRef.current = loop;
|
|
1166
|
+
const reportFramePresented = useMemo(() => {
|
|
1167
|
+
let reported = false;
|
|
1168
|
+
return () => {
|
|
1169
|
+
if (reported) return;
|
|
1170
|
+
reported = true;
|
|
1171
|
+
try {
|
|
1172
|
+
void Promise.resolve(callbacksRef.current.onFramePresented?.()).catch(() => void 0);
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}, [stream, video]);
|
|
1151
1177
|
const primeSoundtrack = () => {
|
|
1152
1178
|
const audio = audioRef.current;
|
|
1153
1179
|
if (!audio || audio.dataset.audioOutput) return;
|
|
@@ -1499,6 +1525,7 @@ function VideoPlayerRuntime({
|
|
|
1499
1525
|
VideoFrame,
|
|
1500
1526
|
{
|
|
1501
1527
|
kit,
|
|
1528
|
+
onFramePresented: !showStartPoster && onFramePresented ? reportFramePresented : void 0,
|
|
1502
1529
|
config: displayConfig,
|
|
1503
1530
|
time: showStartPoster ? posterTime : currentTime,
|
|
1504
1531
|
width: dimensions.width,
|
|
@@ -1611,292 +1638,134 @@ var VideoError = class extends Error {
|
|
|
1611
1638
|
}
|
|
1612
1639
|
};
|
|
1613
1640
|
|
|
1614
|
-
// src/
|
|
1615
|
-
import { useCallback
|
|
1641
|
+
// src/video-chat/use-video-chat.ts
|
|
1642
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useReducer, useRef as useRef3 } from "react";
|
|
1616
1643
|
|
|
1617
|
-
// src/
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
let message = `Video response endpoint failed (${response.status})`;
|
|
1635
|
-
try {
|
|
1636
|
-
const body = await response.json();
|
|
1637
|
-
if (body && typeof body === "object" && "error" in body) {
|
|
1638
|
-
const error = body.error;
|
|
1639
|
-
if (error && typeof error === "object") {
|
|
1640
|
-
if ("code" in error && typeof error.code === "string" && HTTP_ERROR_CODES.has(error.code)) {
|
|
1641
|
-
code = error.code;
|
|
1642
|
-
}
|
|
1643
|
-
if ("message" in error && typeof error.message === "string") {
|
|
1644
|
-
message = safePublicDiagnostic(error.message, message);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
} catch {
|
|
1649
|
-
}
|
|
1650
|
-
return new VideoError(message, { code, status: response.status, requestId });
|
|
1651
|
-
}
|
|
1652
|
-
function normalizeStreamError(cause, requestId, state, aborted) {
|
|
1653
|
-
if (cause instanceof VideoError) return cause;
|
|
1654
|
-
const causeError = cause instanceof Error ? cause : void 0;
|
|
1655
|
-
const safeMessage = aborted ? causeError?.name === "TimeoutError" ? "Request timed out" : safePublicDiagnostic(causeError?.message, "Video generation was aborted") : causeError?.message.startsWith("Video response protocol") || causeError?.message.startsWith("Video response endpoint omitted protocol") ? safePublicDiagnostic(causeError.message, "Video response protocol failed") : "Video response stream failed";
|
|
1656
|
-
return new VideoError(safeMessage, {
|
|
1657
|
-
code: aborted ? "aborted" : "stream_failed",
|
|
1658
|
-
requestId: state.requestId ?? requestId,
|
|
1659
|
-
runId: state.runId,
|
|
1660
|
-
recoverable: false
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
function streamVideo(options) {
|
|
1664
|
-
const request = createVideoRequest(options.input, {
|
|
1665
|
-
requestId: options.requestId,
|
|
1666
|
-
capabilities: options.capabilities,
|
|
1667
|
-
...options.resume?.state.runId != null && options.resume.state.lastSequence >= 0 ? {
|
|
1668
|
-
resume: {
|
|
1669
|
-
runId: options.resume.state.runId,
|
|
1670
|
-
afterSequence: options.resume.state.lastSequence
|
|
1671
|
-
}
|
|
1672
|
-
} : {}
|
|
1673
|
-
});
|
|
1674
|
-
const controller = new AbortController();
|
|
1675
|
-
const forwardAbort = () => controller.abort(options.signal?.reason);
|
|
1676
|
-
if (options.signal?.aborted) forwardAbort();
|
|
1677
|
-
else options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1678
|
-
let resolveResult;
|
|
1679
|
-
let rejectResult;
|
|
1680
|
-
let settled = false;
|
|
1681
|
-
const result = new Promise((resolve, reject) => {
|
|
1682
|
-
resolveResult = resolve;
|
|
1683
|
-
rejectResult = reject;
|
|
1684
|
-
});
|
|
1685
|
-
const source = (async function* consume() {
|
|
1686
|
-
let state = options.resume?.state ?? createVideoState();
|
|
1687
|
-
try {
|
|
1688
|
-
const fetcher = options.fetcher ?? fetch;
|
|
1689
|
-
const headers = new Headers(options.headers);
|
|
1690
|
-
headers.set("Accept", VIDEO_STREAM_CONTENT_TYPE);
|
|
1691
|
-
headers.set("Content-Type", "application/json");
|
|
1692
|
-
if (request.resume) headers.set("Last-Event-ID", `${request.resume.runId}:${request.resume.afterSequence}`);
|
|
1693
|
-
const response = await fetcher(options.endpoint, {
|
|
1694
|
-
method: "POST",
|
|
1695
|
-
headers,
|
|
1696
|
-
body: JSON.stringify(request),
|
|
1697
|
-
signal: controller.signal,
|
|
1698
|
-
credentials: options.credentials
|
|
1699
|
-
});
|
|
1700
|
-
if (!response.ok) throw await responseError(response, request.requestId);
|
|
1701
|
-
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
1702
|
-
throw new Error("Video response endpoint did not return an SSE stream");
|
|
1703
|
-
}
|
|
1704
|
-
const protocolVersion = response.headers.get(VIDEO_STREAM_HEADER);
|
|
1705
|
-
if (protocolVersion == null) {
|
|
1706
|
-
throw new Error(`Video response endpoint omitted protocol version header ${VIDEO_STREAM_HEADER}`);
|
|
1707
|
-
}
|
|
1708
|
-
if (protocolVersion !== VIDEO_PROTOCOL_VERSION) {
|
|
1709
|
-
throw new Error(
|
|
1710
|
-
`Video response protocol mismatch: expected ${VIDEO_PROTOCOL_VERSION}, received ${protocolVersion}`
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
for await (const event of decodeVideoSse(response.body)) {
|
|
1714
|
-
state = applyVideoEvent(state, event);
|
|
1715
|
-
options.onEvent?.(event, state);
|
|
1716
|
-
if (state.status === "complete" && !settled) {
|
|
1717
|
-
settled = true;
|
|
1718
|
-
resolveResult(state);
|
|
1719
|
-
} else if (state.status === "error" && !settled) {
|
|
1720
|
-
settled = true;
|
|
1721
|
-
const error = state.errors.at(-1);
|
|
1722
|
-
rejectResult(new VideoError(error?.message ?? "Video response could not finish", {
|
|
1723
|
-
code: error?.code ?? "generation_failed",
|
|
1724
|
-
status: response.status,
|
|
1725
|
-
requestId: state.requestId ?? request.requestId,
|
|
1726
|
-
runId: state.runId,
|
|
1727
|
-
recoverable: error?.recoverable
|
|
1728
|
-
}));
|
|
1729
|
-
} else if (state.status === "aborted" && !settled) {
|
|
1730
|
-
settled = true;
|
|
1731
|
-
rejectResult(new VideoError(state.abortReason ?? "Video generation was aborted", {
|
|
1732
|
-
code: "aborted",
|
|
1733
|
-
status: response.status,
|
|
1734
|
-
requestId: state.requestId ?? request.requestId,
|
|
1735
|
-
runId: state.runId,
|
|
1736
|
-
recoverable: false
|
|
1737
|
-
}));
|
|
1738
|
-
}
|
|
1739
|
-
yield event;
|
|
1740
|
-
}
|
|
1741
|
-
if (!["complete", "error", "aborted"].includes(state.status)) {
|
|
1742
|
-
throw new Error("Video response stream ended without a terminal event");
|
|
1743
|
-
}
|
|
1744
|
-
if (!settled && state.status === "complete") {
|
|
1745
|
-
settled = true;
|
|
1746
|
-
resolveResult(state);
|
|
1644
|
+
// src/protocol/scene-timeline.ts
|
|
1645
|
+
function createQueue() {
|
|
1646
|
+
const buffered = [];
|
|
1647
|
+
const waiting = [];
|
|
1648
|
+
let closed = false;
|
|
1649
|
+
return {
|
|
1650
|
+
push(value) {
|
|
1651
|
+
const next = waiting.shift();
|
|
1652
|
+
if (next) next({ value, done: false });
|
|
1653
|
+
else buffered.push(value);
|
|
1654
|
+
},
|
|
1655
|
+
close() {
|
|
1656
|
+
closed = true;
|
|
1657
|
+
let next = waiting.shift();
|
|
1658
|
+
while (next) {
|
|
1659
|
+
next({ value: void 0, done: true });
|
|
1660
|
+
next = waiting.shift();
|
|
1747
1661
|
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1662
|
+
},
|
|
1663
|
+
iterable: {
|
|
1664
|
+
[Symbol.asyncIterator]() {
|
|
1665
|
+
return {
|
|
1666
|
+
next() {
|
|
1667
|
+
const buffer = buffered.shift();
|
|
1668
|
+
if (buffer !== void 0) return Promise.resolve({ value: buffer, done: false });
|
|
1669
|
+
if (closed) return Promise.resolve({ value: void 0, done: true });
|
|
1670
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1753
1673
|
}
|
|
1754
|
-
throw error;
|
|
1755
|
-
} finally {
|
|
1756
|
-
options.signal?.removeEventListener("abort", forwardAbort);
|
|
1757
|
-
}
|
|
1758
|
-
})();
|
|
1759
|
-
const stream = createReplayableStream(source, () => void 0);
|
|
1760
|
-
return {
|
|
1761
|
-
request,
|
|
1762
|
-
stream,
|
|
1763
|
-
result,
|
|
1764
|
-
abort(reason = "user cancelled") {
|
|
1765
|
-
controller.abort(new Error(reason));
|
|
1766
1674
|
}
|
|
1767
1675
|
};
|
|
1768
1676
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
requestId: current.request.requestId,
|
|
1804
|
-
recoverable: false
|
|
1805
|
-
}));
|
|
1806
|
-
setState((previous) => ({ ...previous, status: "aborted", abortReason: reason }));
|
|
1677
|
+
var runCounter = 0;
|
|
1678
|
+
function createSceneTimeline(options) {
|
|
1679
|
+
const queue = createQueue();
|
|
1680
|
+
const runId = options.runId ?? `composed-${runCounter += 1}`;
|
|
1681
|
+
const events = createVideoEventFactory({ runId });
|
|
1682
|
+
const orientation = options.orientation ?? "landscape";
|
|
1683
|
+
const snapshot = {
|
|
1684
|
+
schemaVersion: VIDEO_SCHEMA_VERSION,
|
|
1685
|
+
orientation,
|
|
1686
|
+
scenes: [],
|
|
1687
|
+
style: options.style
|
|
1688
|
+
};
|
|
1689
|
+
let position = 0;
|
|
1690
|
+
let finished = false;
|
|
1691
|
+
const held = [];
|
|
1692
|
+
let openForScenes = !options.awaitAudio;
|
|
1693
|
+
const emit = (event) => {
|
|
1694
|
+
if (finished) return;
|
|
1695
|
+
queue.push(event);
|
|
1696
|
+
};
|
|
1697
|
+
emit(events.create("response.start", {
|
|
1698
|
+
requestId: options.requestId ?? runId,
|
|
1699
|
+
format: { orientation },
|
|
1700
|
+
style: options.style
|
|
1701
|
+
}));
|
|
1702
|
+
if (options.audio) {
|
|
1703
|
+
snapshot.audio = options.audio;
|
|
1704
|
+
emit(events.create("audio.set", { audio: options.audio }));
|
|
1705
|
+
}
|
|
1706
|
+
const release = () => {
|
|
1707
|
+
const pending = held.splice(0, held.length);
|
|
1708
|
+
for (const scene of pending) {
|
|
1709
|
+
snapshot.scenes.push(scene);
|
|
1710
|
+
emit(events.create("scene.add", { scene, position: position++ }));
|
|
1807
1711
|
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
setError(void 0);
|
|
1817
|
-
const nextRun = streamVideo({
|
|
1818
|
-
endpoint: currentOptions.endpoint ?? "/api/video",
|
|
1819
|
-
input,
|
|
1820
|
-
requestId: (currentOptions.createRequestId ?? defaultRequestId)(),
|
|
1821
|
-
capabilities: currentOptions.templateIds ? { ...templatesRef.current.capabilities, templates: [...new Set(currentOptions.templateIds)] } : templatesRef.current.capabilities,
|
|
1822
|
-
headers: currentOptions.headers,
|
|
1823
|
-
credentials: currentOptions.credentials,
|
|
1824
|
-
fetcher: currentOptions.fetcher,
|
|
1825
|
-
onEvent: (_event, nextState) => {
|
|
1826
|
-
if (_event.type === "scene.add") {
|
|
1827
|
-
if (!currentOptions.templates?.getTemplate(_event.data.scene.templateId)) {
|
|
1828
|
-
preloadBuiltinTemplate(_event.data.scene.templateId);
|
|
1829
|
-
}
|
|
1830
|
-
warmSceneMedia(_event.data.scene.variables);
|
|
1831
|
-
}
|
|
1832
|
-
if (mounted.current && generation.current === currentGeneration) setState(nextState);
|
|
1712
|
+
};
|
|
1713
|
+
return {
|
|
1714
|
+
stream: queue.iterable,
|
|
1715
|
+
add(scene) {
|
|
1716
|
+
if (finished) return;
|
|
1717
|
+
if (!openForScenes) {
|
|
1718
|
+
held.push(scene);
|
|
1719
|
+
return;
|
|
1833
1720
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
if (
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const error2 = errorFrom(cause);
|
|
1843
|
-
setError(error2);
|
|
1844
|
-
setState((previous) => ({
|
|
1845
|
-
...previous,
|
|
1846
|
-
status: error2.code === "aborted" ? "aborted" : "error",
|
|
1847
|
-
...error2.code === "aborted" && previous.abortReason == null ? { abortReason: error2.message } : {}
|
|
1848
|
-
}));
|
|
1721
|
+
snapshot.scenes.push(scene);
|
|
1722
|
+
emit(events.create("scene.add", { scene, position: position++ }));
|
|
1723
|
+
},
|
|
1724
|
+
setAudio(audio) {
|
|
1725
|
+
if (finished || openForScenes) return;
|
|
1726
|
+
if (audio) {
|
|
1727
|
+
snapshot.audio = audio;
|
|
1728
|
+
emit(events.create("audio.set", { audio }));
|
|
1849
1729
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
}, []);
|
|
1867
|
-
return {
|
|
1868
|
-
generate,
|
|
1869
|
-
abort,
|
|
1870
|
-
video: state.config,
|
|
1871
|
-
status: state.status,
|
|
1872
|
-
error,
|
|
1873
|
-
warnings: state.warnings,
|
|
1874
|
-
playerProps: {
|
|
1875
|
-
...options.templates ? { templates: options.templates } : {},
|
|
1876
|
-
...run ? { stream: run.stream } : {}
|
|
1730
|
+
openForScenes = true;
|
|
1731
|
+
release();
|
|
1732
|
+
},
|
|
1733
|
+
complete() {
|
|
1734
|
+
if (finished) return;
|
|
1735
|
+
if (!openForScenes) {
|
|
1736
|
+
openForScenes = true;
|
|
1737
|
+
release();
|
|
1738
|
+
}
|
|
1739
|
+
emit(events.create("response.complete", {
|
|
1740
|
+
finishReason: "stop",
|
|
1741
|
+
snapshot,
|
|
1742
|
+
checksum: checksumVideo(snapshot)
|
|
1743
|
+
}));
|
|
1744
|
+
finished = true;
|
|
1745
|
+
queue.close();
|
|
1877
1746
|
}
|
|
1878
1747
|
};
|
|
1879
1748
|
}
|
|
1880
1749
|
|
|
1881
1750
|
// src/player/use-narration.ts
|
|
1882
|
-
import { useCallback
|
|
1751
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
|
|
1883
1752
|
function useNarration(options) {
|
|
1884
|
-
const [speaking, setSpeaking] =
|
|
1885
|
-
const optionsRef =
|
|
1753
|
+
const [speaking, setSpeaking] = useState2(false);
|
|
1754
|
+
const optionsRef = useRef2(options);
|
|
1886
1755
|
optionsRef.current = options;
|
|
1887
|
-
const currentRef =
|
|
1888
|
-
const spokenIndexRef =
|
|
1889
|
-
const stop =
|
|
1756
|
+
const currentRef = useRef2(void 0);
|
|
1757
|
+
const spokenIndexRef = useRef2(void 0);
|
|
1758
|
+
const stop = useCallback(() => {
|
|
1890
1759
|
currentRef.current?.abort();
|
|
1891
1760
|
currentRef.current = void 0;
|
|
1892
1761
|
setSpeaking(false);
|
|
1893
1762
|
}, []);
|
|
1894
|
-
const interrupt =
|
|
1763
|
+
const interrupt = useCallback(() => {
|
|
1895
1764
|
spokenIndexRef.current = void 0;
|
|
1896
1765
|
stop();
|
|
1897
1766
|
}, [stop]);
|
|
1898
|
-
|
|
1899
|
-
const onSceneChange =
|
|
1767
|
+
useEffect3(() => () => currentRef.current?.abort(), []);
|
|
1768
|
+
const onSceneChange = useCallback((scene, index) => {
|
|
1900
1769
|
const { voice, enabled = true } = optionsRef.current;
|
|
1901
1770
|
if (!enabled) return;
|
|
1902
1771
|
if (spokenIndexRef.current === index) return;
|
|
@@ -1909,7 +1778,18 @@ function useNarration(options) {
|
|
|
1909
1778
|
setSpeaking(true);
|
|
1910
1779
|
void (async () => {
|
|
1911
1780
|
try {
|
|
1912
|
-
|
|
1781
|
+
let started = false;
|
|
1782
|
+
await voice.speak(line, {
|
|
1783
|
+
signal: controller.signal,
|
|
1784
|
+
onStart: (source) => {
|
|
1785
|
+
if (started || controller.signal.aborted || currentRef.current !== controller || spokenIndexRef.current !== index || optionsRef.current.enabled === false) return;
|
|
1786
|
+
started = true;
|
|
1787
|
+
try {
|
|
1788
|
+
void Promise.resolve(optionsRef.current.onSpeechStart?.(source)).catch(() => void 0);
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1913
1793
|
} catch {
|
|
1914
1794
|
} finally {
|
|
1915
1795
|
if (currentRef.current === controller) {
|
|
@@ -1922,11 +1802,9 @@ function useNarration(options) {
|
|
|
1922
1802
|
return { onSceneChange, interrupt, speaking };
|
|
1923
1803
|
}
|
|
1924
1804
|
|
|
1925
|
-
// src/video-chat/use-video-chat.ts
|
|
1926
|
-
import { useCallback as useCallback3, useEffect as useEffect5, useReducer, useRef as useRef4 } from "react";
|
|
1927
|
-
|
|
1928
1805
|
// src/video-chat/voice.ts
|
|
1929
1806
|
var DEFAULT_MAX_CACHED_LINES = 60;
|
|
1807
|
+
var SPEECH_PREPARATION_TIMEOUT_MS = 3e3;
|
|
1930
1808
|
var FALLBACK_BITS_PER_SECOND = 128e3;
|
|
1931
1809
|
var sharedContext;
|
|
1932
1810
|
function actionEndpoint(endpoint, action) {
|
|
@@ -1959,6 +1837,13 @@ function createVideoChatVoice(options = {}) {
|
|
|
1959
1837
|
let silent = false;
|
|
1960
1838
|
let disposed = false;
|
|
1961
1839
|
let generatedSpeechUnavailable = false;
|
|
1840
|
+
let playbackFailure;
|
|
1841
|
+
const notifyFallback = () => {
|
|
1842
|
+
try {
|
|
1843
|
+
void Promise.resolve(options.onFallback?.()).catch(() => void 0);
|
|
1844
|
+
} catch {
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1962
1847
|
const forgetOldest = () => {
|
|
1963
1848
|
while (lines.size > maximum) {
|
|
1964
1849
|
const oldest = lines.keys().next();
|
|
@@ -1991,38 +1876,34 @@ function createVideoChatVoice(options = {}) {
|
|
|
1991
1876
|
if (generatedSpeechUnavailable) {
|
|
1992
1877
|
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
1993
1878
|
} else {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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");
|
|
1879
|
+
prepared = await withDeadline(async (preparationSignal) => {
|
|
1880
|
+
const response = await fetcher(actionEndpoint(endpoint, "speech"), {
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers,
|
|
1883
|
+
credentials: options.credentials,
|
|
1884
|
+
signal: preparationSignal,
|
|
1885
|
+
body: JSON.stringify({ text: normalized })
|
|
1886
|
+
});
|
|
1887
|
+
preparationSignal.throwIfAborted();
|
|
1888
|
+
if (response.status === 204 || response.status === 404) {
|
|
1889
|
+
generatedSpeechUnavailable = true;
|
|
1890
|
+
return { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2013
1891
|
}
|
|
1892
|
+
if (!response.ok) throw new Error("Speech is unavailable");
|
|
1893
|
+
const bytes = await response.arrayBuffer();
|
|
1894
|
+
preparationSignal.throwIfAborted();
|
|
1895
|
+
const seconds = await measureSeconds(bytes);
|
|
1896
|
+
preparationSignal.throwIfAborted();
|
|
2014
1897
|
createdSrc = URL.createObjectURL(new Blob([bytes], {
|
|
2015
1898
|
type: response.headers.get("content-type") || "audio/mpeg"
|
|
2016
1899
|
}));
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
throw controller.signal.reason ?? new DOMException("Speech preparation cancelled", "AbortError");
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
1900
|
+
return { source: "generated", src: createdSrc, seconds };
|
|
1901
|
+
}, SPEECH_PREPARATION_TIMEOUT_MS, controller.signal);
|
|
2022
1902
|
}
|
|
2023
1903
|
} catch (cause) {
|
|
2024
1904
|
if (createdSrc) URL.revokeObjectURL(createdSrc);
|
|
2025
1905
|
if (controller.signal.aborted) throw controller.signal.reason ?? cause;
|
|
1906
|
+
notifyFallback();
|
|
2026
1907
|
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2027
1908
|
}
|
|
2028
1909
|
if (disposed || controller.signal.aborted) {
|
|
@@ -2044,6 +1925,19 @@ function createVideoChatVoice(options = {}) {
|
|
|
2044
1925
|
signal?.removeEventListener("abort", forwardAbort);
|
|
2045
1926
|
}
|
|
2046
1927
|
};
|
|
1928
|
+
const speechStops = /* @__PURE__ */ new Set();
|
|
1929
|
+
const watchSpeech = (seconds, expire) => {
|
|
1930
|
+
let remaining = Math.max(1, seconds) * 2e3 + 5e3;
|
|
1931
|
+
const timer = setInterval(() => {
|
|
1932
|
+
if (held) return;
|
|
1933
|
+
remaining -= 250;
|
|
1934
|
+
if (remaining <= 0) {
|
|
1935
|
+
clearInterval(timer);
|
|
1936
|
+
expire();
|
|
1937
|
+
}
|
|
1938
|
+
}, 250);
|
|
1939
|
+
return () => clearInterval(timer);
|
|
1940
|
+
};
|
|
2047
1941
|
const stopBrowser = () => {
|
|
2048
1942
|
globalThis.speechSynthesis?.cancel();
|
|
2049
1943
|
browserFinish?.();
|
|
@@ -2060,7 +1954,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
2060
1954
|
},
|
|
2061
1955
|
resume() {
|
|
2062
1956
|
held = false;
|
|
2063
|
-
if (sounding && !silent) void sounding.play().catch(() =>
|
|
1957
|
+
if (sounding && !silent) void sounding.play().catch(() => playbackFailure?.());
|
|
2064
1958
|
if (!silent) globalThis.speechSynthesis?.resume();
|
|
2065
1959
|
},
|
|
2066
1960
|
setMuted(muted) {
|
|
@@ -2068,7 +1962,16 @@ function createVideoChatVoice(options = {}) {
|
|
|
2068
1962
|
if (sounding) sounding.muted = muted;
|
|
2069
1963
|
if (muted) stopBrowser();
|
|
2070
1964
|
},
|
|
2071
|
-
async speak(text, { signal }) {
|
|
1965
|
+
async speak(text, { signal, onStart }) {
|
|
1966
|
+
let started = false;
|
|
1967
|
+
const notifyStart = (source) => {
|
|
1968
|
+
if (started || disposed || signal.aborted || silent || held) return;
|
|
1969
|
+
started = true;
|
|
1970
|
+
try {
|
|
1971
|
+
void Promise.resolve(onStart?.(source)).catch(() => void 0);
|
|
1972
|
+
} catch {
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
2072
1975
|
const line = await load(text, signal);
|
|
2073
1976
|
if (disposed || signal.aborted || silent) return;
|
|
2074
1977
|
if (line.source === "browser") {
|
|
@@ -2081,40 +1984,91 @@ function createVideoChatVoice(options = {}) {
|
|
|
2081
1984
|
const finish = () => {
|
|
2082
1985
|
if (finished) return;
|
|
2083
1986
|
finished = true;
|
|
1987
|
+
clearWatchdog();
|
|
1988
|
+
signal.removeEventListener("abort", stop);
|
|
1989
|
+
speechStops.delete(stop);
|
|
1990
|
+
utterance.onstart = null;
|
|
1991
|
+
utterance.onend = null;
|
|
1992
|
+
utterance.onerror = null;
|
|
2084
1993
|
if (browserFinish === finish) browserFinish = void 0;
|
|
2085
1994
|
resolve();
|
|
2086
1995
|
};
|
|
1996
|
+
const stop = () => {
|
|
1997
|
+
synthesis.cancel();
|
|
1998
|
+
finish();
|
|
1999
|
+
};
|
|
2000
|
+
const clearWatchdog = watchSpeech(estimatedBrowserSeconds(text), stop);
|
|
2001
|
+
speechStops.add(stop);
|
|
2087
2002
|
browserFinish = finish;
|
|
2003
|
+
utterance.onstart = () => {
|
|
2004
|
+
if (!finished) notifyStart("browser");
|
|
2005
|
+
};
|
|
2088
2006
|
utterance.onend = finish;
|
|
2089
2007
|
utterance.onerror = finish;
|
|
2090
|
-
signal.addEventListener("abort",
|
|
2091
|
-
|
|
2008
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
2009
|
+
try {
|
|
2010
|
+
synthesis.speak(utterance);
|
|
2011
|
+
if (held) synthesis.pause();
|
|
2012
|
+
} catch {
|
|
2092
2013
|
finish();
|
|
2093
|
-
}
|
|
2094
|
-
synthesis.speak(utterance);
|
|
2095
|
-
if (held) synthesis.pause();
|
|
2014
|
+
}
|
|
2096
2015
|
});
|
|
2097
2016
|
return;
|
|
2098
2017
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2018
|
+
let playbackFailed = false;
|
|
2019
|
+
try {
|
|
2020
|
+
const element = new Audio(line.src);
|
|
2021
|
+
element.muted = silent;
|
|
2022
|
+
sounding = element;
|
|
2023
|
+
await new Promise((resolve) => {
|
|
2024
|
+
let finished = false;
|
|
2025
|
+
const finish = () => {
|
|
2026
|
+
if (finished) return;
|
|
2027
|
+
finished = true;
|
|
2028
|
+
clearWatchdog();
|
|
2029
|
+
signal.removeEventListener("abort", stop);
|
|
2030
|
+
speechStops.delete(stop);
|
|
2031
|
+
element.onplaying = null;
|
|
2032
|
+
element.onended = null;
|
|
2033
|
+
element.onerror = null;
|
|
2034
|
+
if (sounding === element) {
|
|
2035
|
+
sounding = void 0;
|
|
2036
|
+
playbackFailure = void 0;
|
|
2037
|
+
}
|
|
2038
|
+
resolve();
|
|
2039
|
+
};
|
|
2040
|
+
const stop = () => {
|
|
2041
|
+
element.pause();
|
|
2042
|
+
finish();
|
|
2043
|
+
};
|
|
2044
|
+
const fail = () => {
|
|
2045
|
+
playbackFailed = true;
|
|
2046
|
+
stop();
|
|
2047
|
+
};
|
|
2048
|
+
const clearWatchdog = watchSpeech(Math.max(line.seconds, estimatedBrowserSeconds(text)), fail);
|
|
2049
|
+
speechStops.add(stop);
|
|
2050
|
+
playbackFailure = fail;
|
|
2051
|
+
element.onplaying = () => {
|
|
2052
|
+
if (!finished) notifyStart("generated");
|
|
2053
|
+
};
|
|
2054
|
+
element.onended = finish;
|
|
2055
|
+
element.onerror = fail;
|
|
2056
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
2057
|
+
try {
|
|
2058
|
+
if (!held) void element.play().catch(fail);
|
|
2059
|
+
} catch {
|
|
2060
|
+
fail();
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
} catch {
|
|
2064
|
+
playbackFailed = true;
|
|
2065
|
+
}
|
|
2066
|
+
if (playbackFailed && !disposed && !signal.aborted && !silent) {
|
|
2067
|
+
notifyFallback();
|
|
2068
|
+
URL.revokeObjectURL(line.src);
|
|
2069
|
+
lines.set(text.trim(), { source: "browser", seconds: estimatedBrowserSeconds(text) });
|
|
2070
|
+
await this.speak(text, { signal, onStart: notifyStart });
|
|
2071
|
+
}
|
|
2118
2072
|
},
|
|
2119
2073
|
dispose() {
|
|
2120
2074
|
disposed = true;
|
|
@@ -2122,6 +2076,8 @@ function createVideoChatVoice(options = {}) {
|
|
|
2122
2076
|
controller.abort(new DOMException("Video chat voice was disposed", "AbortError"));
|
|
2123
2077
|
}
|
|
2124
2078
|
pendingLoads.clear();
|
|
2079
|
+
for (const stop of speechStops) stop();
|
|
2080
|
+
speechStops.clear();
|
|
2125
2081
|
sounding?.pause();
|
|
2126
2082
|
sounding = void 0;
|
|
2127
2083
|
stopBrowser();
|
|
@@ -2135,6 +2091,12 @@ function createVideoChatVoice(options = {}) {
|
|
|
2135
2091
|
|
|
2136
2092
|
// src/video-chat/use-video-chat.ts
|
|
2137
2093
|
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
2094
|
+
function observe(callback) {
|
|
2095
|
+
try {
|
|
2096
|
+
void Promise.resolve(callback?.()).catch(() => void 0);
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2138
2100
|
function initialState(muted) {
|
|
2139
2101
|
return {
|
|
2140
2102
|
turns: [],
|
|
@@ -2160,6 +2122,7 @@ function reducer(state, action) {
|
|
|
2160
2122
|
return {
|
|
2161
2123
|
...state,
|
|
2162
2124
|
turns: [...state.turns, action.turn],
|
|
2125
|
+
playerKey: state.playerKey + 1,
|
|
2163
2126
|
shownTurnId: action.turn.id,
|
|
2164
2127
|
status: "composing",
|
|
2165
2128
|
resumeStatus: "composing",
|
|
@@ -2219,6 +2182,8 @@ function reducer(state, action) {
|
|
|
2219
2182
|
suggestions: action.suggestions
|
|
2220
2183
|
}))
|
|
2221
2184
|
};
|
|
2185
|
+
case "suggestions":
|
|
2186
|
+
return { ...state, turns: replaceTurn(state.turns, action.id, (turn) => ({ ...turn, suggestions: action.suggestions })) };
|
|
2222
2187
|
case "scene":
|
|
2223
2188
|
if (state.playerKey !== action.key) return state;
|
|
2224
2189
|
return {
|
|
@@ -2229,6 +2194,11 @@ function reducer(state, action) {
|
|
|
2229
2194
|
case "playback-end":
|
|
2230
2195
|
if (state.playerKey !== action.key) return state;
|
|
2231
2196
|
return { ...state, status: "ended", resumeStatus: "ended", playbackEnded: true, openingSpeaking: false };
|
|
2197
|
+
case "warning":
|
|
2198
|
+
return { ...state, turns: replaceTurn(state.turns, action.id, (turn) => ({
|
|
2199
|
+
...turn,
|
|
2200
|
+
warnings: [.../* @__PURE__ */ new Set([...turn.warnings ?? [], action.message])]
|
|
2201
|
+
})) };
|
|
2232
2202
|
case "error":
|
|
2233
2203
|
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
2234
2204
|
return {
|
|
@@ -2285,6 +2255,17 @@ function reducer(state, action) {
|
|
|
2285
2255
|
error: void 0
|
|
2286
2256
|
};
|
|
2287
2257
|
}
|
|
2258
|
+
case "restore": {
|
|
2259
|
+
const restored = {
|
|
2260
|
+
...initialState(state.muted),
|
|
2261
|
+
capabilities: state.capabilities,
|
|
2262
|
+
welcome: state.welcome,
|
|
2263
|
+
turns: action.turns,
|
|
2264
|
+
playerKey: state.playerKey
|
|
2265
|
+
};
|
|
2266
|
+
const latest = action.turns.at(-1);
|
|
2267
|
+
return latest ? reducer(restored, { type: "select", id: latest.id }) : restored;
|
|
2268
|
+
}
|
|
2288
2269
|
case "reset":
|
|
2289
2270
|
return { ...initialState(state.muted), capabilities: state.capabilities, welcome: state.welcome };
|
|
2290
2271
|
}
|
|
@@ -2311,28 +2292,35 @@ function conversationFor(turns) {
|
|
|
2311
2292
|
response: [...transcriptFor(turn).join(" ")].slice(0, 8e3).join("")
|
|
2312
2293
|
}));
|
|
2313
2294
|
}
|
|
2314
|
-
function
|
|
2295
|
+
async function untilAborted(work, signal) {
|
|
2296
|
+
let abort = () => void 0;
|
|
2297
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
2298
|
+
abort = () => reject(signal.reason ?? new DOMException("Cancelled", "AbortError"));
|
|
2299
|
+
if (signal.aborted) abort();
|
|
2300
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
2301
|
+
});
|
|
2302
|
+
try {
|
|
2303
|
+
return await Promise.race([work, cancelled]);
|
|
2304
|
+
} finally {
|
|
2305
|
+
signal.removeEventListener("abort", abort);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
function errorFrom(cause) {
|
|
2315
2309
|
if (cause instanceof VideoError) return cause;
|
|
2316
2310
|
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
2317
2311
|
return new VideoError("Video chat was cancelled", { code: "aborted", recoverable: false });
|
|
2318
2312
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
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 {
|
|
2313
|
+
if (cause instanceof DOMException && cause.name === "TimeoutError") {
|
|
2314
|
+
return new VideoError("Video chat timed out", { code: "timeout", recoverable: false });
|
|
2334
2315
|
}
|
|
2335
|
-
return new VideoError(
|
|
2316
|
+
return new VideoError("Video chat could not produce a playable response", { code: "video_chat_failed", recoverable: false });
|
|
2317
|
+
}
|
|
2318
|
+
async function responseError(response) {
|
|
2319
|
+
return new VideoError("Video chat could not produce a playable response", {
|
|
2320
|
+
code: "http_error",
|
|
2321
|
+
status: response.status,
|
|
2322
|
+
recoverable: false
|
|
2323
|
+
});
|
|
2336
2324
|
}
|
|
2337
2325
|
function pacedScene(scene, spokenSeconds, templates) {
|
|
2338
2326
|
const held = spokenSeconds && spokenSeconds > 0 ? spokenSeconds + 0.8 : getSceneDuration(scene, templates?.getTemplateMetadata(scene.templateId) ?? getBuiltinTemplateMetadata(scene.templateId));
|
|
@@ -2361,35 +2349,79 @@ function prepareVisualScene(scene, mode) {
|
|
|
2361
2349
|
return overMedia ? { ...shown, textArchetype: "subtle" } : shown;
|
|
2362
2350
|
}
|
|
2363
2351
|
function useVideoChat(options = {}) {
|
|
2364
|
-
|
|
2352
|
+
return useVideoChatSession(options).chat;
|
|
2353
|
+
}
|
|
2354
|
+
function useVideoChatSession(options = {}) {
|
|
2355
|
+
const optionsRef = useRef3(options);
|
|
2365
2356
|
optionsRef.current = options;
|
|
2366
|
-
const
|
|
2357
|
+
const voiceWarningRef = useRef3(() => void 0);
|
|
2358
|
+
const ownedVoiceRef = useRef3(void 0);
|
|
2367
2359
|
if (!options.voice && !ownedVoiceRef.current) {
|
|
2368
2360
|
ownedVoiceRef.current = createVideoChatVoice({
|
|
2369
2361
|
endpoint: options.endpoint,
|
|
2370
2362
|
headers: options.headers,
|
|
2371
2363
|
credentials: options.credentials,
|
|
2372
|
-
fetcher: options.fetcher
|
|
2364
|
+
fetcher: options.fetcher,
|
|
2365
|
+
onFallback: () => voiceWarningRef.current()
|
|
2373
2366
|
});
|
|
2374
2367
|
}
|
|
2375
2368
|
const voice = options.voice ?? ownedVoiceRef.current;
|
|
2376
|
-
const voiceRef =
|
|
2369
|
+
const voiceRef = useRef3(voice);
|
|
2377
2370
|
voiceRef.current = voice;
|
|
2378
|
-
const
|
|
2379
|
-
const
|
|
2371
|
+
const unavailableVoiceLines = useRef3(/* @__PURE__ */ new Set());
|
|
2372
|
+
const speechStartRef = useRef3(() => void 0);
|
|
2373
|
+
const narration = useNarration({ onSpeechStart: (source) => speechStartRef.current(source), voice: {
|
|
2374
|
+
speak: (text, options2) => unavailableVoiceLines.current.has(text) ? void 0 : voice.speak(text, options2)
|
|
2375
|
+
} });
|
|
2376
|
+
const narrationRef = useRef3(narration);
|
|
2380
2377
|
narrationRef.current = narration;
|
|
2381
2378
|
const [state, dispatch] = useReducer(reducer, options.initialMuted ?? false, initialState);
|
|
2382
|
-
const stateRef =
|
|
2379
|
+
const stateRef = useRef3(state);
|
|
2383
2380
|
stateRef.current = state;
|
|
2384
|
-
const runRef =
|
|
2385
|
-
const inFlightRef =
|
|
2386
|
-
const
|
|
2387
|
-
const
|
|
2388
|
-
const
|
|
2389
|
-
const
|
|
2390
|
-
const
|
|
2391
|
-
const
|
|
2392
|
-
const
|
|
2381
|
+
const runRef = useRef3(0);
|
|
2382
|
+
const inFlightRef = useRef3(void 0);
|
|
2383
|
+
const suggestionsRef = useRef3(void 0);
|
|
2384
|
+
const openingRef = useRef3(void 0);
|
|
2385
|
+
const timelineRef = useRef3(void 0);
|
|
2386
|
+
const heldRef = useRef3(false);
|
|
2387
|
+
const flushRef = useRef3(void 0);
|
|
2388
|
+
const mountedRef = useRef3(true);
|
|
2389
|
+
const firstFrameRef = useRef3(void 0);
|
|
2390
|
+
const reportMetric = useCallback2((metric) => {
|
|
2391
|
+
observe(() => optionsRef.current.onPlaybackMetric?.(metric));
|
|
2392
|
+
}, []);
|
|
2393
|
+
const finishStall = useCallback2(() => {
|
|
2394
|
+
const timing = firstFrameRef.current;
|
|
2395
|
+
if (!timing?.active || timing.stallStartedAt == null) return;
|
|
2396
|
+
const now = monotonicNow();
|
|
2397
|
+
const started = timing.stallStartedAt;
|
|
2398
|
+
timing.stallStartedAt = void 0;
|
|
2399
|
+
reportMetric({
|
|
2400
|
+
type: "stall",
|
|
2401
|
+
turnId: timing.turnId,
|
|
2402
|
+
mode: timing.mode,
|
|
2403
|
+
elapsedMs: Math.max(0, Math.round(now - timing.startedAt)),
|
|
2404
|
+
durationMs: Math.max(0, Math.round(now - started)),
|
|
2405
|
+
reason: "scene-generation"
|
|
2406
|
+
});
|
|
2407
|
+
}, [reportMetric]);
|
|
2408
|
+
const endTiming = useCallback2(() => {
|
|
2409
|
+
finishStall();
|
|
2410
|
+
if (firstFrameRef.current) firstFrameRef.current.active = false;
|
|
2411
|
+
}, [finishStall]);
|
|
2412
|
+
speechStartRef.current = (source) => {
|
|
2413
|
+
const timing = firstFrameRef.current;
|
|
2414
|
+
if (!mountedRef.current || !timing?.active || timing.speechReported || heldRef.current || stateRef.current.muted) return;
|
|
2415
|
+
timing.speechReported = true;
|
|
2416
|
+
reportMetric({
|
|
2417
|
+
type: "first-speech",
|
|
2418
|
+
turnId: timing.turnId,
|
|
2419
|
+
mode: timing.mode,
|
|
2420
|
+
elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt)),
|
|
2421
|
+
source: optionsRef.current.voice ? "custom" : source === "generated" ? "generated" : "browser"
|
|
2422
|
+
});
|
|
2423
|
+
};
|
|
2424
|
+
const request = useCallback2(async (action, init = {}, signal) => {
|
|
2393
2425
|
const current = optionsRef.current;
|
|
2394
2426
|
const headers = new Headers(current.headers);
|
|
2395
2427
|
new Headers(init.headers).forEach((header, name) => headers.set(name, header));
|
|
@@ -2403,27 +2435,29 @@ function useVideoChat(options = {}) {
|
|
|
2403
2435
|
signal
|
|
2404
2436
|
});
|
|
2405
2437
|
}, []);
|
|
2406
|
-
|
|
2438
|
+
useEffect4(() => {
|
|
2407
2439
|
mountedRef.current = true;
|
|
2408
2440
|
const controller = new AbortController();
|
|
2409
2441
|
void request("capabilities", {}, controller.signal).then(async (response) => {
|
|
2410
|
-
if (!response.ok) throw await
|
|
2442
|
+
if (!response.ok) throw await responseError(response);
|
|
2411
2443
|
return response.json();
|
|
2412
2444
|
}).then((value) => {
|
|
2413
2445
|
if (mountedRef.current) dispatch({ type: "capabilities", value });
|
|
2414
2446
|
}).catch(() => void 0);
|
|
2415
2447
|
void request("welcome", {}, controller.signal).then(async (response) => {
|
|
2416
|
-
if (!response.ok) throw await
|
|
2448
|
+
if (!response.ok) throw await responseError(response);
|
|
2417
2449
|
return response.json();
|
|
2418
2450
|
}).then((value) => {
|
|
2419
2451
|
if (mountedRef.current) dispatch({ type: "welcome", value });
|
|
2420
2452
|
}).catch(() => void 0);
|
|
2421
2453
|
return () => {
|
|
2454
|
+
endTiming();
|
|
2422
2455
|
mountedRef.current = false;
|
|
2423
2456
|
controller.abort();
|
|
2424
2457
|
runRef.current += 1;
|
|
2425
2458
|
inFlightRef.current?.abort(new DOMException("Component unmounted", "AbortError"));
|
|
2426
2459
|
openingRef.current?.abort(new DOMException("Component unmounted", "AbortError"));
|
|
2460
|
+
suggestionsRef.current?.abort();
|
|
2427
2461
|
timelineRef.current?.complete();
|
|
2428
2462
|
timelineRef.current = void 0;
|
|
2429
2463
|
narrationRef.current.interrupt();
|
|
@@ -2431,12 +2465,14 @@ function useVideoChat(options = {}) {
|
|
|
2431
2465
|
if (!mountedRef.current) ownedVoiceRef.current?.dispose?.();
|
|
2432
2466
|
});
|
|
2433
2467
|
};
|
|
2434
|
-
}, [request]);
|
|
2435
|
-
|
|
2436
|
-
const cancel =
|
|
2468
|
+
}, [request, endTiming]);
|
|
2469
|
+
useEffect4(() => voice.setMuted(state.muted), [state.muted, voice]);
|
|
2470
|
+
const cancel = useCallback2((reason = "Video chat was cancelled") => {
|
|
2471
|
+
endTiming();
|
|
2437
2472
|
runRef.current += 1;
|
|
2438
2473
|
inFlightRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
2439
2474
|
openingRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
2475
|
+
suggestionsRef.current?.abort();
|
|
2440
2476
|
inFlightRef.current = void 0;
|
|
2441
2477
|
openingRef.current = void 0;
|
|
2442
2478
|
timelineRef.current?.complete();
|
|
@@ -2444,16 +2480,19 @@ function useVideoChat(options = {}) {
|
|
|
2444
2480
|
flushRef.current = void 0;
|
|
2445
2481
|
narrationRef.current.interrupt();
|
|
2446
2482
|
dispatch({ type: "cancelled" });
|
|
2447
|
-
}, []);
|
|
2448
|
-
const ask =
|
|
2483
|
+
}, [endTiming]);
|
|
2484
|
+
const ask = useCallback2(async (value, askOptions = {}) => {
|
|
2449
2485
|
const prompt = value.trim();
|
|
2450
2486
|
if (!prompt) return void 0;
|
|
2487
|
+
endTiming();
|
|
2451
2488
|
inFlightRef.current?.abort(new DOMException("Replaced by a new prompt", "AbortError"));
|
|
2452
2489
|
openingRef.current?.abort(new DOMException("Replaced by a new prompt", "AbortError"));
|
|
2490
|
+
suggestionsRef.current?.abort();
|
|
2453
2491
|
timelineRef.current?.complete();
|
|
2454
2492
|
timelineRef.current = void 0;
|
|
2455
2493
|
narrationRef.current.interrupt();
|
|
2456
2494
|
voiceRef.current.resume();
|
|
2495
|
+
unavailableVoiceLines.current.clear();
|
|
2457
2496
|
heldRef.current = false;
|
|
2458
2497
|
const controller = new AbortController();
|
|
2459
2498
|
const openingController = new AbortController();
|
|
@@ -2488,7 +2527,7 @@ function useVideoChat(options = {}) {
|
|
|
2488
2527
|
suggestions: [],
|
|
2489
2528
|
...openingMedia ? { openingMedia } : {}
|
|
2490
2529
|
};
|
|
2491
|
-
firstFrameRef.current = { turnId: id, mode, startedAt: monotonicNow(), reported: false };
|
|
2530
|
+
firstFrameRef.current = { turnId: id, mode, startedAt: monotonicNow(), reported: false, speechReported: false, active: true };
|
|
2492
2531
|
dispatch({ type: "start", turn });
|
|
2493
2532
|
let timeline;
|
|
2494
2533
|
let style;
|
|
@@ -2500,9 +2539,14 @@ function useVideoChat(options = {}) {
|
|
|
2500
2539
|
let terminal = false;
|
|
2501
2540
|
let attempt = 0;
|
|
2502
2541
|
let ready = [];
|
|
2542
|
+
let received = [];
|
|
2503
2543
|
let appended = 0;
|
|
2504
|
-
const isCurrent = () => mountedRef.current && runRef.current === run && !controller.signal.aborted;
|
|
2544
|
+
const isCurrent = () => mountedRef.current && runRef.current === run && !controller.signal.aborted && !terminal;
|
|
2505
2545
|
const isOpeningCurrent = () => isCurrent() && !terminal && openingRef.current === openingController && !openingController.signal.aborted;
|
|
2546
|
+
const warn = (message) => {
|
|
2547
|
+
if (isCurrent()) dispatch({ type: "warning", id, message });
|
|
2548
|
+
};
|
|
2549
|
+
voiceWarningRef.current = () => warn("Using browser voice for this response.");
|
|
2506
2550
|
const flush = () => {
|
|
2507
2551
|
if (!isCurrent()) return;
|
|
2508
2552
|
let available = appended;
|
|
@@ -2536,11 +2580,11 @@ function useVideoChat(options = {}) {
|
|
|
2536
2580
|
}
|
|
2537
2581
|
};
|
|
2538
2582
|
flushRef.current = flush;
|
|
2539
|
-
const resolveOpeningMedia = async (keyword) => {
|
|
2583
|
+
const resolveOpeningMedia = async (keyword, fallbackKeyword) => {
|
|
2540
2584
|
try {
|
|
2541
2585
|
const response = await request("opening-media", {
|
|
2542
2586
|
method: "POST",
|
|
2543
|
-
body: JSON.stringify({ keyword, orientation })
|
|
2587
|
+
body: JSON.stringify({ keyword, ...fallbackKeyword ? { fallbackKeyword } : {}, orientation })
|
|
2544
2588
|
}, openingController.signal);
|
|
2545
2589
|
if (!response.ok || !isOpeningCurrent() || timeline) return;
|
|
2546
2590
|
const payload = await response.json();
|
|
@@ -2549,13 +2593,25 @@ function useVideoChat(options = {}) {
|
|
|
2549
2593
|
} catch {
|
|
2550
2594
|
}
|
|
2551
2595
|
};
|
|
2596
|
+
const prepareSpeech = async (text, signal) => {
|
|
2597
|
+
try {
|
|
2598
|
+
return currentOptions.voice ? await withDeadline((child) => voiceRef.current.prepare(text, { signal: child }), 3e3, signal) : await voiceRef.current.prepare(text, { signal });
|
|
2599
|
+
} catch (cause) {
|
|
2600
|
+
if (currentOptions.voice && !signal.aborted && isCurrent()) unavailableVoiceLines.current.add(text);
|
|
2601
|
+
throw cause;
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2552
2604
|
const speakOpening = async (text) => {
|
|
2553
2605
|
try {
|
|
2554
|
-
await voiceRef.current.prepare(text, { signal: openingController.signal });
|
|
2555
2606
|
if (!isOpeningCurrent()) return;
|
|
2556
2607
|
dispatch({ type: "opening-start", id, line: text });
|
|
2557
|
-
await
|
|
2608
|
+
await prepareSpeech(text, openingController.signal);
|
|
2609
|
+
if (!isOpeningCurrent()) return;
|
|
2610
|
+
await voiceRef.current.speak(text, { signal: openingController.signal, onStart: (source) => {
|
|
2611
|
+
if (isOpeningCurrent()) speechStartRef.current(source);
|
|
2612
|
+
} });
|
|
2558
2613
|
} catch {
|
|
2614
|
+
if (isOpeningCurrent()) warn("Some narration is unavailable; the response will continue.");
|
|
2559
2615
|
} finally {
|
|
2560
2616
|
openingActive = false;
|
|
2561
2617
|
if (isOpeningCurrent()) dispatch({ type: "opening-end", id });
|
|
@@ -2581,7 +2637,7 @@ function useVideoChat(options = {}) {
|
|
|
2581
2637
|
...currentOptions.style ? { style: currentOptions.style } : {}
|
|
2582
2638
|
})
|
|
2583
2639
|
}, controller.signal);
|
|
2584
|
-
if (!response.ok) throw await
|
|
2640
|
+
if (!response.ok) throw await responseError(response);
|
|
2585
2641
|
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
2586
2642
|
throw new VideoError("Video chat endpoint did not return a video stream", { code: "invalid_response" });
|
|
2587
2643
|
}
|
|
@@ -2590,83 +2646,100 @@ function useVideoChat(options = {}) {
|
|
|
2590
2646
|
const pending = [];
|
|
2591
2647
|
let narrating = Promise.resolve();
|
|
2592
2648
|
let terminalError;
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
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);
|
|
2649
|
+
try {
|
|
2650
|
+
for await (const event of decodeVideoSse(response.body)) {
|
|
2651
|
+
if (!isCurrent() || currentAttempt !== attempt) return { video: { schemaVersion: "0.1", orientation, scenes: [], style }, lines: [] };
|
|
2652
|
+
if (event.type === "response.start") style = event.data.style;
|
|
2653
|
+
if (event.type === "response.warning" || event.type === "response.error" && !event.data.terminal) {
|
|
2654
|
+
warn("Some parts were simplified so the response could continue.");
|
|
2607
2655
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2656
|
+
if (event.type === "data.video-chat-opening") {
|
|
2657
|
+
const payload = event.data && typeof event.data === "object" && !Array.isArray(event.data) ? event.data : {};
|
|
2658
|
+
const line = typeof payload.line === "string" ? payload.line.trim().slice(0, 300) : "";
|
|
2659
|
+
const keyword = typeof payload.keyword === "string" ? payload.keyword.trim().slice(0, 80) : "";
|
|
2660
|
+
const fallbackKeyword = typeof payload.fallbackKeyword === "string" ? payload.fallbackKeyword.trim().slice(0, 80) : void 0;
|
|
2661
|
+
if (!openingMedia && keyword) void resolveOpeningMedia(keyword, fallbackKeyword);
|
|
2662
|
+
if (!openingRequested && line) {
|
|
2663
|
+
spokenHook = line;
|
|
2664
|
+
lines.push(line);
|
|
2665
|
+
openingRequested = true;
|
|
2666
|
+
openingActive = true;
|
|
2667
|
+
void speakOpening(line);
|
|
2668
|
+
}
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
if (event.type === "response.error" && event.data.terminal) {
|
|
2672
|
+
terminalError = new VideoError("Video chat could not finish this response", {
|
|
2673
|
+
code: event.data.error.code,
|
|
2674
|
+
requestId: event.data.snapshot ? void 0 : id,
|
|
2675
|
+
recoverable: event.data.error.recoverable
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
if (event.type === "response.abort") {
|
|
2679
|
+
terminalError = new VideoError("Video chat was interrupted", { code: "aborted", recoverable: false });
|
|
2680
|
+
}
|
|
2681
|
+
if (event.type !== "scene.add") continue;
|
|
2682
|
+
const position = event.data.position;
|
|
2683
|
+
const plannedScene = event.data.scene;
|
|
2684
|
+
planned[position] = plannedScene;
|
|
2685
|
+
received[position] = prepareVisualScene(plannedScene, mode);
|
|
2686
|
+
if (!currentOptions.templates?.getTemplate(plannedScene.templateId)) {
|
|
2687
|
+
preloadBuiltinTemplate(plannedScene.templateId);
|
|
2688
|
+
}
|
|
2689
|
+
warmSceneMedia(plannedScene.variables);
|
|
2690
|
+
const narrated = narrating.then(async () => {
|
|
2691
|
+
const supplied = plannedScene.narration?.trim();
|
|
2692
|
+
if (supplied) return supplied;
|
|
2693
|
+
return withDeadline(async (signal) => {
|
|
2694
|
+
const narrationResponse = await request("narration", {
|
|
2695
|
+
method: "POST",
|
|
2696
|
+
body: JSON.stringify({ prompt, scene: plannedScene, earlier: [...lines] })
|
|
2697
|
+
}, signal);
|
|
2698
|
+
if (!narrationResponse.ok) throw new Error("Narration unavailable");
|
|
2699
|
+
const payload = await narrationResponse.json();
|
|
2700
|
+
const line = typeof payload.line === "string" ? payload.line.trim() : "";
|
|
2701
|
+
if (!line) throw new Error("Narration unavailable");
|
|
2702
|
+
return line;
|
|
2703
|
+
}, 3e3, controller.signal);
|
|
2704
|
+
}).catch((cause) => {
|
|
2705
|
+
if (controller.signal.aborted) throw cause;
|
|
2706
|
+
warn("Some narration was simplified so the response could continue.");
|
|
2707
|
+
return Object.values(plannedScene.variables).filter((value2) => typeof value2 === "string" && !/^https?:/i.test(value2)).join(" ").slice(0, 500);
|
|
2708
|
+
}).then((line) => {
|
|
2709
|
+
if (line) lines.push(line);
|
|
2710
|
+
return line;
|
|
2615
2711
|
});
|
|
2712
|
+
narrating = narrated.catch(() => "");
|
|
2713
|
+
pending.push(narrated.then(async (line) => {
|
|
2714
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2715
|
+
const visual = prepareVisualScene(plannedScene, mode);
|
|
2716
|
+
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
2717
|
+
const spoken = line ? await prepareSpeech(line, controller.signal).catch((cause) => {
|
|
2718
|
+
if (controller.signal.aborted) throw cause;
|
|
2719
|
+
warn("Some narration is unavailable; the response will continue.");
|
|
2720
|
+
return void 0;
|
|
2721
|
+
}) : void 0;
|
|
2722
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2723
|
+
ready[position] = pacedScene(withNarration, spoken?.seconds, currentOptions.templates);
|
|
2724
|
+
flush();
|
|
2725
|
+
}).catch(() => {
|
|
2726
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2727
|
+
ready[position] = { ...received[position], timing: { fixedDuration: 5 } };
|
|
2728
|
+
warn("Some parts were simplified so the response could continue.");
|
|
2729
|
+
flush();
|
|
2730
|
+
}));
|
|
2616
2731
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
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
|
-
}));
|
|
2732
|
+
} catch (cause) {
|
|
2733
|
+
if (controller.signal.aborted) throw cause;
|
|
2734
|
+
terminalError = errorFrom(cause);
|
|
2652
2735
|
}
|
|
2653
2736
|
await Promise.all(pending);
|
|
2654
|
-
if (terminalError)
|
|
2737
|
+
if (terminalError && ready.some(Boolean) && style) {
|
|
2738
|
+
warn("The response was interrupted; completed scenes are still available.");
|
|
2739
|
+
} else if (terminalError) throw terminalError;
|
|
2655
2740
|
if (planned.length === 0 || ready.filter(Boolean).length === 0 || !style) {
|
|
2656
2741
|
throw new VideoError("The video response contained no playable scenes", { code: "empty_response" });
|
|
2657
2742
|
}
|
|
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
2743
|
return {
|
|
2671
2744
|
video: {
|
|
2672
2745
|
schemaVersion: "0.1",
|
|
@@ -2674,25 +2747,43 @@ function useVideoChat(options = {}) {
|
|
|
2674
2747
|
scenes: ready.filter((entry) => entry != null),
|
|
2675
2748
|
style
|
|
2676
2749
|
},
|
|
2677
|
-
|
|
2750
|
+
lines
|
|
2678
2751
|
};
|
|
2679
2752
|
};
|
|
2680
2753
|
try {
|
|
2681
2754
|
let response;
|
|
2682
2755
|
try {
|
|
2683
|
-
response = await runAttempt(attempt);
|
|
2756
|
+
response = await untilAborted(runAttempt(attempt), controller.signal);
|
|
2684
2757
|
} catch (cause) {
|
|
2685
|
-
if (controller.signal.aborted || timeline) throw cause;
|
|
2758
|
+
if (controller.signal.aborted || timeline || spokenHook) throw cause;
|
|
2686
2759
|
attempt += 1;
|
|
2687
2760
|
ready = [];
|
|
2761
|
+
received = [];
|
|
2688
2762
|
appended = 0;
|
|
2689
2763
|
style = void 0;
|
|
2690
|
-
response = await runAttempt(attempt);
|
|
2764
|
+
response = await untilAborted(runAttempt(attempt), controller.signal);
|
|
2691
2765
|
}
|
|
2692
2766
|
if (!isCurrent()) return void 0;
|
|
2693
2767
|
planDone = true;
|
|
2694
2768
|
flush();
|
|
2695
|
-
dispatch({ type: "complete", id, video: response.video, suggestions:
|
|
2769
|
+
dispatch({ type: "complete", id, video: response.video, suggestions: [] });
|
|
2770
|
+
const suggestionsController = new AbortController();
|
|
2771
|
+
suggestionsRef.current = suggestionsController;
|
|
2772
|
+
void withDeadline(async (signal) => {
|
|
2773
|
+
const result = await request("suggestions", {
|
|
2774
|
+
method: "POST",
|
|
2775
|
+
body: JSON.stringify({ prompt, lines: response.lines })
|
|
2776
|
+
}, signal);
|
|
2777
|
+
if (!result.ok) return [];
|
|
2778
|
+
const payload = await result.json();
|
|
2779
|
+
return Array.isArray(payload.suggestions) ? payload.suggestions : [];
|
|
2780
|
+
}, 7e3, suggestionsController.signal).then((suggestions2) => {
|
|
2781
|
+
if (mountedRef.current && runRef.current === run && !suggestionsController.signal.aborted) {
|
|
2782
|
+
dispatch({ type: "suggestions", id, suggestions: suggestions2 });
|
|
2783
|
+
}
|
|
2784
|
+
}).catch(() => void 0).finally(() => {
|
|
2785
|
+
if (suggestionsRef.current === suggestionsController) suggestionsRef.current = void 0;
|
|
2786
|
+
});
|
|
2696
2787
|
return response.video;
|
|
2697
2788
|
} catch (cause) {
|
|
2698
2789
|
if (runRef.current !== run) return void 0;
|
|
@@ -2700,31 +2791,52 @@ function useVideoChat(options = {}) {
|
|
|
2700
2791
|
return void 0;
|
|
2701
2792
|
}
|
|
2702
2793
|
terminal = true;
|
|
2794
|
+
const recovered = received.flatMap((scene, index) => scene ? [ready[index] ?? { ...scene, timing: { fixedDuration: 5 } }] : []);
|
|
2795
|
+
if (recovered.length === 0 && spokenHook) {
|
|
2796
|
+
recovered.push({ id: `${id}-opening`, templateId: "media", variables: { texts: spokenHook, mediaType: "gradient" }, narration: spokenHook, timing: { fixedDuration: Math.max(4, spokenHook.split(/\s+/u).length / 2.5) } });
|
|
2797
|
+
}
|
|
2798
|
+
if (recovered.length > 0) {
|
|
2799
|
+
style ??= { brand: resolveVideoBrand(currentOptions.brand), density: "normal", motion: "normal", defaultBackgroundEffect: "static", defaultTextArchetype: "subtle", defaultTransition: "crossfade" };
|
|
2800
|
+
openingController.abort(new DOMException("Continuing completed response", "AbortError"));
|
|
2801
|
+
if (!timeline) {
|
|
2802
|
+
timeline = createSceneTimeline({ style, orientation });
|
|
2803
|
+
dispatch({ type: "player", id, stream: timeline.stream });
|
|
2804
|
+
}
|
|
2805
|
+
for (const scene of recovered.slice(appended)) timeline.add(scene);
|
|
2806
|
+
timeline.complete();
|
|
2807
|
+
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2808
|
+
const video = { schemaVersion: "0.1", orientation, style, scenes: recovered };
|
|
2809
|
+
dispatch({ type: "warning", id, message: "The response was interrupted; completed scenes are still available." });
|
|
2810
|
+
dispatch({ type: "complete", id, video, suggestions: [] });
|
|
2811
|
+
return video;
|
|
2812
|
+
}
|
|
2703
2813
|
openingController.abort(new DOMException("Response failed", "AbortError"));
|
|
2704
2814
|
timeline?.complete();
|
|
2705
2815
|
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2706
|
-
const error =
|
|
2816
|
+
const error = errorFrom(cause);
|
|
2707
2817
|
dispatch({ type: "error", id, error });
|
|
2708
2818
|
return void 0;
|
|
2709
2819
|
} finally {
|
|
2710
2820
|
clearTimeout(timeout);
|
|
2711
2821
|
if (runRef.current === run) inFlightRef.current = void 0;
|
|
2712
2822
|
}
|
|
2713
|
-
}, [request]);
|
|
2714
|
-
const pause =
|
|
2823
|
+
}, [request, endTiming]);
|
|
2824
|
+
const pause = useCallback2(() => {
|
|
2825
|
+
finishStall();
|
|
2715
2826
|
heldRef.current = true;
|
|
2716
2827
|
voiceRef.current.pause();
|
|
2717
2828
|
dispatch({ type: "pause" });
|
|
2718
|
-
}, []);
|
|
2719
|
-
const resume =
|
|
2829
|
+
}, [finishStall]);
|
|
2830
|
+
const resume = useCallback2(() => {
|
|
2720
2831
|
heldRef.current = false;
|
|
2721
2832
|
voiceRef.current.resume();
|
|
2722
2833
|
dispatch({ type: "resume" });
|
|
2723
2834
|
flushRef.current?.();
|
|
2724
2835
|
}, []);
|
|
2725
|
-
const replay =
|
|
2836
|
+
const replay = useCallback2(() => {
|
|
2726
2837
|
const turn = stateRef.current.turns.find((entry) => entry.id === stateRef.current.shownTurnId);
|
|
2727
2838
|
if (!turn?.completed || !turn.video) return;
|
|
2839
|
+
endTiming();
|
|
2728
2840
|
if (inFlightRef.current) {
|
|
2729
2841
|
runRef.current += 1;
|
|
2730
2842
|
inFlightRef.current.abort(new DOMException("Replaying a saved response", "AbortError"));
|
|
@@ -2738,10 +2850,11 @@ function useVideoChat(options = {}) {
|
|
|
2738
2850
|
heldRef.current = false;
|
|
2739
2851
|
voiceRef.current.resume();
|
|
2740
2852
|
dispatch({ type: "replay" });
|
|
2741
|
-
}, []);
|
|
2742
|
-
const selectTurn =
|
|
2853
|
+
}, [endTiming]);
|
|
2854
|
+
const selectTurn = useCallback2((id) => {
|
|
2743
2855
|
const turn = stateRef.current.turns.find((entry) => entry.id === id);
|
|
2744
2856
|
if (!turn?.completed || !turn.video) return;
|
|
2857
|
+
endTiming();
|
|
2745
2858
|
if (inFlightRef.current) {
|
|
2746
2859
|
runRef.current += 1;
|
|
2747
2860
|
inFlightRef.current.abort(new DOMException("Viewing a saved response", "AbortError"));
|
|
@@ -2755,14 +2868,20 @@ function useVideoChat(options = {}) {
|
|
|
2755
2868
|
heldRef.current = false;
|
|
2756
2869
|
voiceRef.current.resume();
|
|
2757
2870
|
dispatch({ type: "select", id });
|
|
2758
|
-
}, []);
|
|
2759
|
-
const reset =
|
|
2871
|
+
}, [endTiming]);
|
|
2872
|
+
const reset = useCallback2(() => {
|
|
2760
2873
|
cancel("Session reset");
|
|
2761
2874
|
heldRef.current = false;
|
|
2762
2875
|
voiceRef.current.resume();
|
|
2763
2876
|
dispatch({ type: "reset" });
|
|
2764
2877
|
}, [cancel]);
|
|
2765
|
-
const
|
|
2878
|
+
const restoreSession = useCallback2((turns) => {
|
|
2879
|
+
cancel("Restoring session");
|
|
2880
|
+
heldRef.current = false;
|
|
2881
|
+
voiceRef.current.resume();
|
|
2882
|
+
dispatch({ type: "restore", turns: turns.filter((turn) => turn.completed && turn.video).slice(-100) });
|
|
2883
|
+
}, [cancel]);
|
|
2884
|
+
const setMuted = useCallback2((muted) => dispatch({ type: "mute", value: muted }), []);
|
|
2766
2885
|
const currentTurn = state.turns.at(-1);
|
|
2767
2886
|
const shownTurn = state.turns.find((turn) => turn.id === state.shownTurnId) ?? currentTurn;
|
|
2768
2887
|
const availableModes = state.capabilities?.modes ?? ["templates"];
|
|
@@ -2777,30 +2896,38 @@ function useVideoChat(options = {}) {
|
|
|
2777
2896
|
paused: state.status === "paused",
|
|
2778
2897
|
controls: false,
|
|
2779
2898
|
orientation: shownTurn?.fixedOrientation ? shownTurn.orientation : "auto",
|
|
2899
|
+
onFramePresented: () => {
|
|
2900
|
+
const timing = firstFrameRef.current;
|
|
2901
|
+
if (stateRef.current.playerKey !== playbackKey || state.playback?.kind !== "stream" || !timing?.active || timing.reported) return;
|
|
2902
|
+
timing.reported = true;
|
|
2903
|
+
const elapsedMs = Math.max(0, Math.round(monotonicNow() - timing.startedAt));
|
|
2904
|
+
observe(() => optionsRef.current.onFirstFrame?.({ turnId: timing.turnId, mode: timing.mode, timeToFirstFrameMs: elapsedMs }));
|
|
2905
|
+
reportMetric({ type: "first-frame", turnId: timing.turnId, mode: timing.mode, elapsedMs });
|
|
2906
|
+
},
|
|
2907
|
+
onStallChange: (stalled) => {
|
|
2908
|
+
if (stateRef.current.playerKey !== playbackKey || state.playback?.kind !== "stream") return;
|
|
2909
|
+
const timing = firstFrameRef.current;
|
|
2910
|
+
if (!timing?.active || !timing.reported) return;
|
|
2911
|
+
if (!stalled) finishStall();
|
|
2912
|
+
else if (!heldRef.current && timing.stallStartedAt == null) timing.stallStartedAt = monotonicNow();
|
|
2913
|
+
},
|
|
2780
2914
|
onSceneChange: (scene, index) => {
|
|
2781
2915
|
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
2916
|
dispatch({ type: "scene", key: playbackKey, scene, index });
|
|
2795
2917
|
narrationRef.current.onSceneChange(scene, index);
|
|
2796
2918
|
},
|
|
2797
|
-
onPlaybackEnd: () =>
|
|
2919
|
+
onPlaybackEnd: () => {
|
|
2920
|
+
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2921
|
+
endTiming();
|
|
2922
|
+
dispatch({ type: "playback-end", key: playbackKey });
|
|
2923
|
+
},
|
|
2798
2924
|
onError: (cause) => {
|
|
2925
|
+
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2799
2926
|
const id = stateRef.current.turns.at(-1)?.id;
|
|
2800
|
-
if (id) dispatch({ type: "error", id, error:
|
|
2927
|
+
if (id) dispatch({ type: "error", id, error: errorFrom(cause) });
|
|
2801
2928
|
}
|
|
2802
2929
|
} : void 0;
|
|
2803
|
-
|
|
2930
|
+
const chat = {
|
|
2804
2931
|
ask,
|
|
2805
2932
|
cancel,
|
|
2806
2933
|
pause,
|
|
@@ -2815,6 +2942,7 @@ function useVideoChat(options = {}) {
|
|
|
2815
2942
|
welcome: state.welcome,
|
|
2816
2943
|
availableModes,
|
|
2817
2944
|
status: state.status,
|
|
2945
|
+
warnings: shownTurn?.warnings ?? [],
|
|
2818
2946
|
error: state.error,
|
|
2819
2947
|
suggestions,
|
|
2820
2948
|
caption: state.caption,
|
|
@@ -2826,15 +2954,16 @@ function useVideoChat(options = {}) {
|
|
|
2826
2954
|
playerKey: state.playerKey,
|
|
2827
2955
|
playerProps
|
|
2828
2956
|
};
|
|
2957
|
+
return { chat, restoreSession };
|
|
2829
2958
|
}
|
|
2830
2959
|
|
|
2831
2960
|
// src/video-chat/video-chat.tsx
|
|
2832
|
-
import { useCallback as useCallback6, useEffect as useEffect9, useId, useMemo as
|
|
2961
|
+
import { useCallback as useCallback6, useEffect as useEffect9, useId, useLayoutEffect, useMemo as useMemo2, useRef as useRef7, useState as useState6 } from "react";
|
|
2833
2962
|
|
|
2834
2963
|
// src/video-chat/modes.ts
|
|
2835
2964
|
var visualModes = [
|
|
2836
|
-
{ id: "templates", label: "Templates only", note: "
|
|
2837
|
-
{ id: "full", label: "Full AI video", note: "
|
|
2965
|
+
{ id: "templates", label: "Templates only", note: "Animated text, diagrams and charts" },
|
|
2966
|
+
{ id: "full", label: "Full AI video", note: "AI-generated footage \xB7 billed per clip" }
|
|
2838
2967
|
];
|
|
2839
2968
|
var defaultMode = visualModes[0];
|
|
2840
2969
|
var modeById = (id) => visualModes.find((mode) => mode.id === id) ?? defaultMode;
|
|
@@ -2939,9 +3068,9 @@ var Warning = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
|
2939
3068
|
] });
|
|
2940
3069
|
|
|
2941
3070
|
// src/video-chat/use-dismiss.ts
|
|
2942
|
-
import { useEffect as
|
|
3071
|
+
import { useEffect as useEffect5 } from "react";
|
|
2943
3072
|
function useDismiss(open, close, surfaces) {
|
|
2944
|
-
|
|
3073
|
+
useEffect5(() => {
|
|
2945
3074
|
if (!open) return;
|
|
2946
3075
|
const onKeyDown = (event) => {
|
|
2947
3076
|
if (event.key !== "Escape") return;
|
|
@@ -2963,7 +3092,7 @@ function useDismiss(open, close, surfaces) {
|
|
|
2963
3092
|
}, [open, close, surfaces]);
|
|
2964
3093
|
}
|
|
2965
3094
|
function useFocusTrap(active, surface) {
|
|
2966
|
-
|
|
3095
|
+
useEffect5(() => {
|
|
2967
3096
|
if (!active) return;
|
|
2968
3097
|
const returnTo = document.activeElement;
|
|
2969
3098
|
const focusable = () => Array.from(
|
|
@@ -2996,11 +3125,11 @@ function useFocusTrap(active, surface) {
|
|
|
2996
3125
|
}
|
|
2997
3126
|
|
|
2998
3127
|
// src/video-chat/suggestion-cards.tsx
|
|
2999
|
-
import { useCallback as
|
|
3128
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef4, useState as useState3 } from "react";
|
|
3000
3129
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3001
3130
|
function Frame({ media, poster, playing }) {
|
|
3002
|
-
const video =
|
|
3003
|
-
|
|
3131
|
+
const video = useRef4(null);
|
|
3132
|
+
useEffect6(() => {
|
|
3004
3133
|
const element = video.current;
|
|
3005
3134
|
if (!element || playing === void 0) return;
|
|
3006
3135
|
if (playing) void element.play().catch(() => void 0);
|
|
@@ -3025,20 +3154,20 @@ function Frame({ media, poster, playing }) {
|
|
|
3025
3154
|
);
|
|
3026
3155
|
}
|
|
3027
3156
|
function SuggestionCards({ suggestions, label, onAsk }) {
|
|
3028
|
-
const railRef =
|
|
3029
|
-
const [at, setAt] =
|
|
3030
|
-
const [taken, setTaken] =
|
|
3031
|
-
const take =
|
|
3157
|
+
const railRef = useRef4(null);
|
|
3158
|
+
const [at, setAt] = useState3(0);
|
|
3159
|
+
const [taken, setTaken] = useState3(false);
|
|
3160
|
+
const take = useCallback3((index) => {
|
|
3032
3161
|
setTaken(true);
|
|
3033
3162
|
setAt(index);
|
|
3034
3163
|
}, []);
|
|
3035
|
-
|
|
3164
|
+
useEffect6(() => {
|
|
3036
3165
|
if (taken || suggestions.length < 2) return;
|
|
3037
3166
|
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
3038
3167
|
const tour = window.setInterval(() => setAt((index) => (index + 1) % suggestions.length), 5e3);
|
|
3039
3168
|
return () => window.clearInterval(tour);
|
|
3040
3169
|
}, [taken, suggestions.length]);
|
|
3041
|
-
|
|
3170
|
+
useEffect6(() => {
|
|
3042
3171
|
const rail = railRef.current;
|
|
3043
3172
|
const card = rail?.children[at];
|
|
3044
3173
|
if (!rail || !card) return;
|
|
@@ -3095,7 +3224,7 @@ function Welcome({ data, onAsk, title }) {
|
|
|
3095
3224
|
}
|
|
3096
3225
|
|
|
3097
3226
|
// src/video-chat/use-voice-input.ts
|
|
3098
|
-
import { useCallback as
|
|
3227
|
+
import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState4 } from "react";
|
|
3099
3228
|
function recognitionConstructor() {
|
|
3100
3229
|
if (typeof window === "undefined") return void 0;
|
|
3101
3230
|
const holder = window;
|
|
@@ -3165,19 +3294,19 @@ async function recordAndTranscribe(signal, ready, captured, options) {
|
|
|
3165
3294
|
return clip ? transcribeRecording(clip, signal, options) : "";
|
|
3166
3295
|
}
|
|
3167
3296
|
function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {}) {
|
|
3168
|
-
const [supported, setSupported] =
|
|
3169
|
-
const [listening, setListening] =
|
|
3170
|
-
const [error, setError] =
|
|
3171
|
-
const [thinking, setThinking] =
|
|
3172
|
-
const recognitionRef =
|
|
3173
|
-
const useRecorderRef =
|
|
3174
|
-
const operationRef =
|
|
3175
|
-
const handlerRef =
|
|
3297
|
+
const [supported, setSupported] = useState4(false);
|
|
3298
|
+
const [listening, setListening] = useState4(false);
|
|
3299
|
+
const [error, setError] = useState4();
|
|
3300
|
+
const [thinking, setThinking] = useState4(false);
|
|
3301
|
+
const recognitionRef = useRef5(void 0);
|
|
3302
|
+
const useRecorderRef = useRef5(false);
|
|
3303
|
+
const operationRef = useRef5(void 0);
|
|
3304
|
+
const handlerRef = useRef5(onTranscript);
|
|
3176
3305
|
handlerRef.current = onTranscript;
|
|
3177
|
-
|
|
3306
|
+
useEffect7(() => {
|
|
3178
3307
|
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3179
3308
|
}, [transcriptionAvailable]);
|
|
3180
|
-
const stop =
|
|
3309
|
+
const stop = useCallback4(() => {
|
|
3181
3310
|
recognitionRef.current?.abort();
|
|
3182
3311
|
recognitionRef.current = void 0;
|
|
3183
3312
|
const operation = operationRef.current;
|
|
@@ -3187,7 +3316,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3187
3316
|
setListening(false);
|
|
3188
3317
|
setThinking(false);
|
|
3189
3318
|
}, []);
|
|
3190
|
-
|
|
3319
|
+
useEffect7(() => () => {
|
|
3191
3320
|
recognitionRef.current?.abort();
|
|
3192
3321
|
recognitionRef.current = void 0;
|
|
3193
3322
|
const operation = operationRef.current;
|
|
@@ -3195,7 +3324,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3195
3324
|
operation?.controller.abort();
|
|
3196
3325
|
operation?.finish?.();
|
|
3197
3326
|
}, []);
|
|
3198
|
-
const finish =
|
|
3327
|
+
const finish = useCallback4(() => {
|
|
3199
3328
|
const recognition = recognitionRef.current;
|
|
3200
3329
|
if (recognition) {
|
|
3201
3330
|
recognition.stop();
|
|
@@ -3209,7 +3338,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3209
3338
|
}
|
|
3210
3339
|
stop();
|
|
3211
3340
|
}, [stop]);
|
|
3212
|
-
const record =
|
|
3341
|
+
const record = useCallback4(async () => {
|
|
3213
3342
|
if (!transcriptionAvailable || !recorderAvailable()) {
|
|
3214
3343
|
setError("Server transcription is not configured.");
|
|
3215
3344
|
return;
|
|
@@ -3247,7 +3376,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3247
3376
|
}
|
|
3248
3377
|
}
|
|
3249
3378
|
}, [transcriptionAvailable, options.endpoint, options.headers, options.credentials, options.fetcher]);
|
|
3250
|
-
const toggle =
|
|
3379
|
+
const toggle = useCallback4(() => {
|
|
3251
3380
|
if (listening) {
|
|
3252
3381
|
finish();
|
|
3253
3382
|
return;
|
|
@@ -3298,14 +3427,77 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3298
3427
|
return { supported, listening, thinking, error, toggle, stop };
|
|
3299
3428
|
}
|
|
3300
3429
|
|
|
3430
|
+
// src/video-chat/use-immersive-controls.ts
|
|
3431
|
+
import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef6, useState as useState5 } from "react";
|
|
3432
|
+
function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
3433
|
+
const [visible, setVisible] = useState5(true);
|
|
3434
|
+
const hovered = useRef6(false);
|
|
3435
|
+
const focused = useRef6(false);
|
|
3436
|
+
const previousCaptions = useRef6(false);
|
|
3437
|
+
const timer = useRef6(null);
|
|
3438
|
+
const clearTimer = useCallback5(() => {
|
|
3439
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
3440
|
+
timer.current = null;
|
|
3441
|
+
}, []);
|
|
3442
|
+
const reveal = useCallback5(() => {
|
|
3443
|
+
clearTimer();
|
|
3444
|
+
setVisible(true);
|
|
3445
|
+
if (playing && !pinned && !hovered.current && !focused.current) {
|
|
3446
|
+
timer.current = setTimeout(() => {
|
|
3447
|
+
timer.current = null;
|
|
3448
|
+
setVisible(false);
|
|
3449
|
+
}, 2e3);
|
|
3450
|
+
}
|
|
3451
|
+
}, [clearTimer, playing, pinned]);
|
|
3452
|
+
useEffect8(() => {
|
|
3453
|
+
const firstCaption = hasCaptions && !previousCaptions.current;
|
|
3454
|
+
previousCaptions.current = hasCaptions;
|
|
3455
|
+
if (firstCaption && playing && !pinned && !focused.current) {
|
|
3456
|
+
hovered.current = false;
|
|
3457
|
+
clearTimer();
|
|
3458
|
+
setVisible(false);
|
|
3459
|
+
} else {
|
|
3460
|
+
reveal();
|
|
3461
|
+
}
|
|
3462
|
+
return clearTimer;
|
|
3463
|
+
}, [hasCaptions, playing, pinned, reveal, clearTimer]);
|
|
3464
|
+
const onPointerEnter = useCallback5((event) => {
|
|
3465
|
+
if (event?.pointerType === "touch") return;
|
|
3466
|
+
hovered.current = true;
|
|
3467
|
+
reveal();
|
|
3468
|
+
}, [reveal]);
|
|
3469
|
+
const onPointerLeave = useCallback5(() => {
|
|
3470
|
+
if (!hovered.current) return;
|
|
3471
|
+
hovered.current = false;
|
|
3472
|
+
reveal();
|
|
3473
|
+
}, [reveal]);
|
|
3474
|
+
const onFocusCapture = useCallback5((event) => {
|
|
3475
|
+
focused.current = event.target.matches(":focus-visible");
|
|
3476
|
+
reveal();
|
|
3477
|
+
}, [reveal]);
|
|
3478
|
+
const onBlurCapture = useCallback5((event) => {
|
|
3479
|
+
if (focused.current && !event.currentTarget.contains(event.relatedTarget)) {
|
|
3480
|
+
focused.current = false;
|
|
3481
|
+
reveal();
|
|
3482
|
+
}
|
|
3483
|
+
}, [reveal]);
|
|
3484
|
+
return { visible, reveal, onPointerEnter, onPointerLeave, onFocusCapture, onBlurCapture };
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// src/video-chat/logo.tsx
|
|
3488
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
3489
|
+
function Logo() {
|
|
3490
|
+
return /* @__PURE__ */ jsx6("img", { className: "brand-logo", src: new URL("./assets/vanillasky-logo.svg", import.meta.url).href, alt: "VanillaSky", width: 144, height: 36 });
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3301
3493
|
// src/video-chat/video-chat.tsx
|
|
3302
|
-
import { Fragment as Fragment4, jsx as
|
|
3494
|
+
import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3303
3495
|
var DESKTOP_WIDTH = 900;
|
|
3304
3496
|
function useViewportOrientation() {
|
|
3305
|
-
const [portrait, setPortrait] = useState6(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`).matches : false);
|
|
3497
|
+
const [portrait, setPortrait] = useState6(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`).matches : false);
|
|
3306
3498
|
useEffect9(() => {
|
|
3307
3499
|
if (typeof window.matchMedia !== "function") return;
|
|
3308
|
-
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`);
|
|
3500
|
+
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`);
|
|
3309
3501
|
const update = () => setPortrait(query.matches);
|
|
3310
3502
|
update();
|
|
3311
3503
|
query.addEventListener("change", update);
|
|
@@ -3330,22 +3522,28 @@ function useFilmingStep(active, key) {
|
|
|
3330
3522
|
return STEPS[Math.min(step, STEPS.length - 1)];
|
|
3331
3523
|
}
|
|
3332
3524
|
function Waveform({ active, listening }) {
|
|
3333
|
-
return /* @__PURE__ */
|
|
3525
|
+
return /* @__PURE__ */ jsx7("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx7("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
|
|
3334
3526
|
}
|
|
3335
3527
|
function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
3336
3528
|
const [draft, setDraft] = useState6("");
|
|
3529
|
+
const [savedSessions, setSavedSessions] = useState6([]);
|
|
3337
3530
|
const [themeId, setThemeId] = useState6(defaultTheme.id);
|
|
3338
3531
|
const [modeId, setModeId] = useState6(options.mode ?? defaultMode.id);
|
|
3339
3532
|
const [historyOpen, setHistoryOpen] = useState6(false);
|
|
3340
3533
|
const [settingsOpen, setSettingsOpen] = useState6(false);
|
|
3341
|
-
const [
|
|
3534
|
+
const [captionsOn, setCaptionsOn] = useState6(true);
|
|
3535
|
+
const [captionsExpanded, setCaptionsExpanded] = useState6(false);
|
|
3536
|
+
const [alwaysShowControls, setAlwaysShowControls] = useState6(false);
|
|
3537
|
+
const [editing, setEditing] = useState6(false);
|
|
3538
|
+
const resumeAfterInput = useRef7(false);
|
|
3539
|
+
const panelRef = useRef7(null);
|
|
3540
|
+
const composerRef = useRef7(null);
|
|
3342
3541
|
const viewportOrientation = useViewportOrientation();
|
|
3343
|
-
const [themeChoice, setThemeChoice] = useState6("system");
|
|
3344
3542
|
const theme = themeById(themeId);
|
|
3345
3543
|
const sessionOrientation = options.orientation ?? viewportOrientation;
|
|
3346
3544
|
const { brand: configuredBrand, style: configuredStyle, ...sessionOptions } = options;
|
|
3347
3545
|
const presentationBrand = configuredBrand ?? theme.brand;
|
|
3348
|
-
const chat =
|
|
3546
|
+
const { chat, restoreSession } = useVideoChatSession({
|
|
3349
3547
|
...sessionOptions,
|
|
3350
3548
|
mode: modeId,
|
|
3351
3549
|
orientation: sessionOrientation,
|
|
@@ -3356,8 +3554,6 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3356
3554
|
const historyId = `${instanceId}-history`;
|
|
3357
3555
|
const settingsId = `${instanceId}-settings`;
|
|
3358
3556
|
const promptId = `${instanceId}-prompt`;
|
|
3359
|
-
const responseTitleId = `${instanceId}-response-title`;
|
|
3360
|
-
const appearanceName = `${instanceId}-appearance`;
|
|
3361
3557
|
const visualsName = `${instanceId}-visuals`;
|
|
3362
3558
|
const styleName = `${instanceId}-style`;
|
|
3363
3559
|
const inputRef = useRef7(null);
|
|
@@ -3365,23 +3561,20 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3365
3561
|
const historyButtonRef = useRef7(null);
|
|
3366
3562
|
const settingsRef = useRef7(null);
|
|
3367
3563
|
const settingsButtonRef = useRef7(null);
|
|
3368
|
-
const sheetRef = useRef7(null);
|
|
3369
3564
|
const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
|
|
3370
3565
|
endpoint: options.endpoint,
|
|
3371
3566
|
headers: options.headers,
|
|
3372
3567
|
credentials: options.credentials,
|
|
3373
3568
|
fetcher: options.fetcher
|
|
3374
3569
|
});
|
|
3375
|
-
const historySurfaces =
|
|
3376
|
-
const settingsSurfaces =
|
|
3377
|
-
const noSurfaces = useMemo3(() => [], []);
|
|
3570
|
+
const historySurfaces = useMemo2(() => [historyRef, historyButtonRef], []);
|
|
3571
|
+
const settingsSurfaces = useMemo2(() => [settingsRef, settingsButtonRef], []);
|
|
3378
3572
|
const closeHistory = useCallback6(() => setHistoryOpen(false), []);
|
|
3379
3573
|
const closeSettings = useCallback6(() => setSettingsOpen(false), []);
|
|
3380
|
-
const closeSheet = useCallback6(() => setSheetOpen(false), []);
|
|
3381
3574
|
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
3382
3575
|
useDismiss(settingsOpen, closeSettings, settingsSurfaces);
|
|
3383
|
-
|
|
3384
|
-
useFocusTrap(
|
|
3576
|
+
useFocusTrap(settingsOpen, settingsRef);
|
|
3577
|
+
useFocusTrap(historyOpen, historyRef);
|
|
3385
3578
|
const ask = useCallback6((value) => {
|
|
3386
3579
|
const prompt = (typeof value === "string" ? value : value.prompt).trim();
|
|
3387
3580
|
if (!prompt) return;
|
|
@@ -3389,7 +3582,10 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3389
3582
|
listen.stop();
|
|
3390
3583
|
setHistoryOpen(false);
|
|
3391
3584
|
setSettingsOpen(false);
|
|
3392
|
-
|
|
3585
|
+
setCaptionsExpanded(false);
|
|
3586
|
+
setEditing(false);
|
|
3587
|
+
resumeAfterInput.current = false;
|
|
3588
|
+
inputRef.current?.blur();
|
|
3393
3589
|
void chat.ask(prompt, typeof value === "string" ? void 0 : {
|
|
3394
3590
|
openingMedia: value.media,
|
|
3395
3591
|
opening: value.opening
|
|
@@ -3400,14 +3596,22 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3400
3596
|
inputRef.current?.focus();
|
|
3401
3597
|
return;
|
|
3402
3598
|
}
|
|
3599
|
+
listen.stop();
|
|
3600
|
+
setDraft("");
|
|
3601
|
+
const completed = chat.turns.filter((turn) => turn.completed && turn.video);
|
|
3602
|
+
if (completed.length) setSavedSessions((sessions) => [{ id: completed[0].id, turns: completed }, ...sessions].slice(0, 10));
|
|
3403
3603
|
chat.reset();
|
|
3404
3604
|
setHistoryOpen(false);
|
|
3405
3605
|
setSettingsOpen(false);
|
|
3406
|
-
|
|
3407
|
-
|
|
3606
|
+
setCaptionsExpanded(false);
|
|
3607
|
+
setEditing(false);
|
|
3608
|
+
resumeAfterInput.current = false;
|
|
3609
|
+
inputRef.current?.blur();
|
|
3610
|
+
}, [chat, listen]);
|
|
3408
3611
|
const current = chat.currentTurn;
|
|
3409
3612
|
const shown = chat.shownTurn;
|
|
3410
3613
|
const showing = chat.playerProps != null;
|
|
3614
|
+
const ambientPoster = shown?.openingMedia?.type === "image" ? shown.openingMedia.url : shown?.openingMedia?.posterUrl;
|
|
3411
3615
|
const waitingForPicture = chat.turns.length > 0 && !showing && (chat.status === "composing" || chat.status === "playing" || chat.status === "paused");
|
|
3412
3616
|
const filmingStep = useFilmingStep(waitingForPicture, current?.id);
|
|
3413
3617
|
const status = chat.turns.length === 0 ? "idle" : chat.status === "composing" ? "drawing" : chat.status === "playing" ? "narrating" : chat.status === "error" || chat.status === "cancelled" ? "ended" : chat.status;
|
|
@@ -3416,47 +3620,99 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3416
3620
|
const availableModes = visualModes.filter((option) => chat.availableModes.includes(option.id));
|
|
3417
3621
|
const selectedMode = modeById(modeId);
|
|
3418
3622
|
const line = chat.caption ?? "";
|
|
3419
|
-
const
|
|
3623
|
+
const fullTranscript = shown?.video ? [shown.opening, ...shown.video.scenes.map((scene) => scene.narration)].filter((entry) => Boolean(entry)) : chat.transcript;
|
|
3624
|
+
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx7(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx7(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx7(Replay, {}) } : void 0;
|
|
3625
|
+
const cancelInput = useCallback6(() => {
|
|
3626
|
+
listen.stop();
|
|
3627
|
+
setDraft("");
|
|
3628
|
+
setEditing(false);
|
|
3629
|
+
inputRef.current?.blur();
|
|
3630
|
+
if (resumeAfterInput.current) chat.resume();
|
|
3631
|
+
resumeAfterInput.current = false;
|
|
3632
|
+
}, [listen, chat]);
|
|
3633
|
+
const beginInput = (speak = false) => {
|
|
3634
|
+
if (!editing) {
|
|
3635
|
+
resumeAfterInput.current = chat.status === "playing" || chat.status === "composing";
|
|
3636
|
+
if (resumeAfterInput.current) chat.pause();
|
|
3637
|
+
}
|
|
3638
|
+
setEditing(true);
|
|
3639
|
+
setSettingsOpen(false);
|
|
3640
|
+
setHistoryOpen(false);
|
|
3641
|
+
if (speak) listen.toggle();
|
|
3642
|
+
};
|
|
3643
|
+
const controls = useImmersiveControls(
|
|
3644
|
+
status === "narrating" || Boolean(line) && status !== "paused",
|
|
3645
|
+
alwaysShowControls || captionsExpanded || editing || historyOpen || settingsOpen || listen.listening || listen.thinking || Boolean(chat.error || listen.error),
|
|
3646
|
+
captionsOn && Boolean(line)
|
|
3647
|
+
);
|
|
3648
|
+
const captionControls = useImmersiveControls(Boolean(line), captionsExpanded, false);
|
|
3649
|
+
const controlEvents = {
|
|
3650
|
+
onPointerEnter: controls.onPointerEnter,
|
|
3651
|
+
onPointerLeave: controls.onPointerLeave,
|
|
3652
|
+
onFocusCapture: controls.onFocusCapture,
|
|
3653
|
+
onBlurCapture: controls.onBlurCapture
|
|
3654
|
+
};
|
|
3655
|
+
useLayoutEffect(() => {
|
|
3656
|
+
const composer = composerRef.current;
|
|
3657
|
+
if (!composer) return;
|
|
3658
|
+
const measure = () => panelRef.current?.style.setProperty("--composer-height", `${composer.getBoundingClientRect().height}px`);
|
|
3659
|
+
measure();
|
|
3660
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
3661
|
+
const observer = new ResizeObserver(measure);
|
|
3662
|
+
observer.observe(composer);
|
|
3663
|
+
return () => observer.disconnect();
|
|
3664
|
+
}, []);
|
|
3420
3665
|
return /* @__PURE__ */ jsxs6(
|
|
3421
3666
|
"div",
|
|
3422
3667
|
{
|
|
3423
3668
|
className: `vanillasky-video-chat${className ? ` ${className}` : ""}`,
|
|
3424
3669
|
"data-orientation": stageOrientation,
|
|
3425
|
-
"data-
|
|
3670
|
+
"data-controls-visible": controls.visible,
|
|
3671
|
+
tabIndex: 0,
|
|
3672
|
+
"aria-label": "Video conversation",
|
|
3673
|
+
onPointerMove: controls.reveal,
|
|
3674
|
+
onPointerDown: controls.reveal,
|
|
3675
|
+
onKeyDownCapture: controls.reveal,
|
|
3426
3676
|
children: [
|
|
3427
|
-
/* @__PURE__ */ jsxs6("header", { className: "chrome", children: [
|
|
3677
|
+
/* @__PURE__ */ jsxs6("header", { className: "chrome", ...controlEvents, children: [
|
|
3678
|
+
/* @__PURE__ */ jsx7("div", { className: "session-brand", children: /* @__PURE__ */ jsx7(Logo, {}) }),
|
|
3428
3679
|
/* @__PURE__ */ jsxs6("div", { className: "group", children: [
|
|
3429
|
-
/* @__PURE__ */
|
|
3680
|
+
/* @__PURE__ */ jsxs6(
|
|
3430
3681
|
"button",
|
|
3431
3682
|
{
|
|
3432
3683
|
type: "button",
|
|
3433
3684
|
className: "round",
|
|
3434
|
-
"aria-label":
|
|
3685
|
+
"aria-label": "New session",
|
|
3435
3686
|
onClick: newSession,
|
|
3436
|
-
children:
|
|
3687
|
+
children: [
|
|
3688
|
+
/* @__PURE__ */ jsx7(Plus, {}),
|
|
3689
|
+
/* @__PURE__ */ jsx7("span", { className: "nav-label", children: "New session" })
|
|
3690
|
+
]
|
|
3437
3691
|
}
|
|
3438
3692
|
),
|
|
3439
|
-
|
|
3693
|
+
/* @__PURE__ */ jsxs6(
|
|
3440
3694
|
"button",
|
|
3441
3695
|
{
|
|
3442
3696
|
ref: historyButtonRef,
|
|
3443
3697
|
type: "button",
|
|
3444
3698
|
className: "pill",
|
|
3445
3699
|
"aria-expanded": historyOpen,
|
|
3700
|
+
"aria-label": "History",
|
|
3701
|
+
"aria-haspopup": "dialog",
|
|
3446
3702
|
"aria-controls": historyId,
|
|
3447
3703
|
onClick: () => {
|
|
3448
3704
|
setHistoryOpen((open) => !open);
|
|
3449
3705
|
setSettingsOpen(false);
|
|
3450
3706
|
},
|
|
3451
3707
|
children: [
|
|
3452
|
-
|
|
3453
|
-
"
|
|
3708
|
+
/* @__PURE__ */ jsx7(Replay, {}),
|
|
3709
|
+
/* @__PURE__ */ jsx7("span", { className: "nav-label", children: "History" })
|
|
3454
3710
|
]
|
|
3455
3711
|
}
|
|
3456
3712
|
)
|
|
3457
3713
|
] }),
|
|
3458
3714
|
/* @__PURE__ */ jsxs6("div", { className: "group", children: [
|
|
3459
|
-
/* @__PURE__ */
|
|
3715
|
+
/* @__PURE__ */ jsx7(
|
|
3460
3716
|
"button",
|
|
3461
3717
|
{
|
|
3462
3718
|
ref: settingsButtonRef,
|
|
@@ -3470,10 +3726,10 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3470
3726
|
setSettingsOpen((open) => !open);
|
|
3471
3727
|
setHistoryOpen(false);
|
|
3472
3728
|
},
|
|
3473
|
-
children: /* @__PURE__ */
|
|
3729
|
+
children: /* @__PURE__ */ jsx7(Gear, {})
|
|
3474
3730
|
}
|
|
3475
3731
|
),
|
|
3476
|
-
/* @__PURE__ */
|
|
3732
|
+
/* @__PURE__ */ jsx7(
|
|
3477
3733
|
"button",
|
|
3478
3734
|
{
|
|
3479
3735
|
type: "button",
|
|
@@ -3481,60 +3737,102 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3481
3737
|
"aria-label": chat.muted ? "Turn the voice on" : "Turn the voice off",
|
|
3482
3738
|
"aria-pressed": chat.muted,
|
|
3483
3739
|
onClick: () => chat.setMuted(!chat.muted),
|
|
3484
|
-
children: chat.muted ? /* @__PURE__ */
|
|
3740
|
+
children: chat.muted ? /* @__PURE__ */ jsx7(Muted, {}) : /* @__PURE__ */ jsx7(Sound, {})
|
|
3485
3741
|
}
|
|
3486
3742
|
)
|
|
3487
3743
|
] })
|
|
3488
3744
|
] }),
|
|
3489
3745
|
/* @__PURE__ */ jsxs6("div", { className: "stage-area", children: [
|
|
3490
3746
|
/* @__PURE__ */ jsxs6("div", { className: "stage", style: { background: themeBackground(presentationBrand) }, children: [
|
|
3747
|
+
showing && /* @__PURE__ */ jsx7("div", { className: "ambient-media", "aria-hidden": "true", children: ambientPoster && /* @__PURE__ */ jsx7("img", { className: "frame-media", src: ambientPoster, alt: "" }) }),
|
|
3491
3748
|
!showing && /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
3492
|
-
/* @__PURE__ */
|
|
3749
|
+
/* @__PURE__ */ jsx7("div", { className: "ground", "aria-hidden": "true" }),
|
|
3493
3750
|
shown?.openingMedia && /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
3494
|
-
/* @__PURE__ */
|
|
3495
|
-
/* @__PURE__ */
|
|
3751
|
+
/* @__PURE__ */ jsx7(Frame, { media: shown.openingMedia, poster: true }),
|
|
3752
|
+
/* @__PURE__ */ jsx7("div", { className: "opening-wash", "aria-hidden": "true" })
|
|
3496
3753
|
] }),
|
|
3497
|
-
chat.turns.length === 0 && /* @__PURE__ */
|
|
3754
|
+
chat.turns.length === 0 && /* @__PURE__ */ jsx7(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
|
|
3498
3755
|
shown?.prompt && /* @__PURE__ */ jsxs6("div", { className: "asked", children: [
|
|
3499
|
-
/* @__PURE__ */
|
|
3500
|
-
waitingForPicture && /* @__PURE__ */
|
|
3756
|
+
/* @__PURE__ */ jsx7("p", { className: "asked-prompt", children: shown.prompt }),
|
|
3757
|
+
waitingForPicture && /* @__PURE__ */ jsx7("p", { className: "asked-step", "aria-live": "polite", children: filmingStep })
|
|
3501
3758
|
] })
|
|
3502
3759
|
] }),
|
|
3503
|
-
chat.playerProps && /* @__PURE__ */
|
|
3760
|
+
chat.playerProps && /* @__PURE__ */ jsx7("div", { className: "player-fit", style: { width: stageOrientation === "portrait" ? "min(100cqw, 56.25cqh)" : "min(100cqw, 177.7778cqh)" }, children: /* @__PURE__ */ jsx7(
|
|
3504
3761
|
VideoPlayer,
|
|
3505
3762
|
{
|
|
3506
3763
|
...chat.playerProps,
|
|
3507
3764
|
templates: options.templates,
|
|
3765
|
+
orientation: stageOrientation,
|
|
3508
3766
|
responsiveBreakpoint: DESKTOP_WIDTH,
|
|
3509
3767
|
ariaLabel: chat.playerProps.video ? "Replay" : "The response"
|
|
3510
3768
|
},
|
|
3511
3769
|
chat.playerKey
|
|
3512
|
-
),
|
|
3770
|
+
) }),
|
|
3513
3771
|
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "ending", children: [
|
|
3514
|
-
/* @__PURE__ */
|
|
3772
|
+
/* @__PURE__ */ jsx7("div", { className: "ending-wash", "aria-hidden": "true" }),
|
|
3515
3773
|
/* @__PURE__ */ jsxs6("div", { className: "ending-body", children: [
|
|
3516
|
-
/* @__PURE__ */
|
|
3517
|
-
/* @__PURE__ */
|
|
3774
|
+
/* @__PURE__ */ jsx7("p", { className: "ending-label", children: "Ask next" }),
|
|
3775
|
+
/* @__PURE__ */ jsx7(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
|
|
3518
3776
|
] })
|
|
3519
3777
|
] })
|
|
3520
3778
|
] }),
|
|
3521
|
-
historyOpen && /* @__PURE__ */
|
|
3522
|
-
"
|
|
3523
|
-
|
|
3524
|
-
type: "button",
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3779
|
+
historyOpen && /* @__PURE__ */ jsxs6("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "History", children: [
|
|
3780
|
+
/* @__PURE__ */ jsxs6("div", { className: "popover-heading", children: [
|
|
3781
|
+
/* @__PURE__ */ jsx7("h2", { children: "History" }),
|
|
3782
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "round", "aria-label": "Close history", onClick: closeHistory, children: /* @__PURE__ */ jsx7(Close, {}) })
|
|
3783
|
+
] }),
|
|
3784
|
+
/* @__PURE__ */ jsx7("h3", { className: "section-label", children: "Current session" }),
|
|
3785
|
+
chat.turns.length === 0 && /* @__PURE__ */ jsx7("p", { className: "history-empty", children: "Your questions will appear here." }),
|
|
3786
|
+
chat.turns.map((turn, index) => /* @__PURE__ */ jsxs6(
|
|
3787
|
+
"button",
|
|
3788
|
+
{
|
|
3789
|
+
type: "button",
|
|
3790
|
+
className: `history-row${turn.id === shown?.id ? " current" : ""}`,
|
|
3791
|
+
"aria-current": turn.id === shown?.id ? true : void 0,
|
|
3792
|
+
disabled: !turn.completed || !turn.video,
|
|
3793
|
+
onClick: () => {
|
|
3794
|
+
listen.stop();
|
|
3795
|
+
setDraft("");
|
|
3796
|
+
setEditing(false);
|
|
3797
|
+
resumeAfterInput.current = false;
|
|
3798
|
+
chat.selectTurn(turn.id);
|
|
3799
|
+
setHistoryOpen(false);
|
|
3800
|
+
},
|
|
3801
|
+
children: [
|
|
3802
|
+
/* @__PURE__ */ jsx7("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
|
|
3803
|
+
/* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
|
|
3804
|
+
turn.prompt,
|
|
3805
|
+
/* @__PURE__ */ jsx7("small", { children: turn.id === shown?.id ? "Now showing" : turn.completed ? "Play answer" : "Unfinished answer" })
|
|
3806
|
+
] })
|
|
3807
|
+
]
|
|
3530
3808
|
},
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3809
|
+
turn.id
|
|
3810
|
+
)),
|
|
3811
|
+
savedSessions.length > 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
3812
|
+
/* @__PURE__ */ jsx7("h3", { className: "section-label", children: "Earlier sessions" }),
|
|
3813
|
+
savedSessions.map((session) => /* @__PURE__ */ jsxs6("button", { type: "button", className: "history-row", onClick: () => {
|
|
3814
|
+
listen.stop();
|
|
3815
|
+
setDraft("");
|
|
3816
|
+
setEditing(false);
|
|
3817
|
+
resumeAfterInput.current = false;
|
|
3818
|
+
const completed = chat.turns.filter((turn) => turn.completed && turn.video);
|
|
3819
|
+
setSavedSessions((sessions) => [...completed.length ? [{ id: completed[0].id, turns: completed }] : [], ...sessions.filter((entry) => entry.id !== session.id)].slice(0, 10));
|
|
3820
|
+
restoreSession(session.turns);
|
|
3821
|
+
setHistoryOpen(false);
|
|
3822
|
+
setCaptionsExpanded(false);
|
|
3823
|
+
}, children: [
|
|
3824
|
+
/* @__PURE__ */ jsx7(Replay, {}),
|
|
3825
|
+
/* @__PURE__ */ jsxs6("span", { className: "prompt", children: [
|
|
3826
|
+
session.turns[0]?.prompt,
|
|
3827
|
+
/* @__PURE__ */ jsxs6("small", { children: [
|
|
3828
|
+
session.turns.length,
|
|
3829
|
+
" ",
|
|
3830
|
+
session.turns.length === 1 ? "answer" : "answers"
|
|
3831
|
+
] })
|
|
3832
|
+
] })
|
|
3833
|
+
] }, session.id))
|
|
3834
|
+
] })
|
|
3835
|
+
] }),
|
|
3538
3836
|
settingsOpen && /* @__PURE__ */ jsxs6(
|
|
3539
3837
|
"div",
|
|
3540
3838
|
{
|
|
@@ -3543,97 +3841,135 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3543
3841
|
className: "sheet-popover settings",
|
|
3544
3842
|
role: "dialog",
|
|
3545
3843
|
"aria-label": "Settings",
|
|
3844
|
+
"aria-modal": "true",
|
|
3546
3845
|
children: [
|
|
3547
|
-
/* @__PURE__ */ jsxs6("
|
|
3548
|
-
/* @__PURE__ */
|
|
3549
|
-
|
|
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))
|
|
3846
|
+
/* @__PURE__ */ jsxs6("div", { className: "popover-heading", children: [
|
|
3847
|
+
/* @__PURE__ */ jsx7("h2", { children: "Settings" }),
|
|
3848
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "round", "aria-label": "Close settings", onClick: closeSettings, children: /* @__PURE__ */ jsx7(Close, {}) })
|
|
3553
3849
|
] }),
|
|
3554
|
-
/* @__PURE__ */
|
|
3555
|
-
|
|
3556
|
-
/* @__PURE__ */
|
|
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) }),
|
|
3850
|
+
/* @__PURE__ */ jsxs6("fieldset", { className: "playback-options", children: [
|
|
3851
|
+
/* @__PURE__ */ jsx7("legend", { children: "Watching" }),
|
|
3852
|
+
/* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
|
|
3559
3853
|
/* @__PURE__ */ jsxs6("span", { children: [
|
|
3560
|
-
/* @__PURE__ */
|
|
3561
|
-
|
|
3854
|
+
/* @__PURE__ */ jsx7("strong", { children: "Subtitles" }),
|
|
3855
|
+
/* @__PURE__ */ jsx7("small", { children: "Read along with the answer" })
|
|
3856
|
+
] }),
|
|
3857
|
+
/* @__PURE__ */ jsx7("input", { type: "checkbox", role: "switch", checked: captionsOn, onChange: (event) => {
|
|
3858
|
+
setCaptionsOn(event.target.checked);
|
|
3859
|
+
setCaptionsExpanded(false);
|
|
3860
|
+
} })
|
|
3861
|
+
] }),
|
|
3862
|
+
/* @__PURE__ */ jsxs6("label", { className: "switch-row", children: [
|
|
3863
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
3864
|
+
/* @__PURE__ */ jsx7("strong", { children: "Keep controls visible" }),
|
|
3865
|
+
/* @__PURE__ */ jsx7("small", { children: "Keep the input bar on screen" })
|
|
3866
|
+
] }),
|
|
3867
|
+
/* @__PURE__ */ jsx7("input", { type: "checkbox", role: "switch", checked: alwaysShowControls, onChange: (event) => setAlwaysShowControls(event.target.checked) })
|
|
3868
|
+
] })
|
|
3869
|
+
] }),
|
|
3870
|
+
/* @__PURE__ */ jsxs6("fieldset", { className: "visual-options", children: [
|
|
3871
|
+
/* @__PURE__ */ jsx7("legend", { children: "Video creation" }),
|
|
3872
|
+
availableModes.map((option) => /* @__PURE__ */ jsxs6("label", { className: "choice-row", children: [
|
|
3873
|
+
/* @__PURE__ */ jsx7("input", { type: "radio", name: visualsName, checked: option.id === selectedMode.id, onChange: () => setModeId(option.id) }),
|
|
3874
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
3875
|
+
/* @__PURE__ */ jsx7("strong", { children: option.label }),
|
|
3876
|
+
/* @__PURE__ */ jsx7("small", { children: option.note })
|
|
3562
3877
|
] })
|
|
3563
3878
|
] }, option.id))
|
|
3564
3879
|
] }),
|
|
3565
|
-
/* @__PURE__ */ jsxs6("fieldset", { children: [
|
|
3566
|
-
/* @__PURE__ */
|
|
3880
|
+
/* @__PURE__ */ jsxs6("fieldset", { className: "style-options", children: [
|
|
3881
|
+
/* @__PURE__ */ jsx7("legend", { children: "Video style" }),
|
|
3567
3882
|
themes.map((option) => /* @__PURE__ */ jsxs6("label", { children: [
|
|
3568
|
-
/* @__PURE__ */
|
|
3569
|
-
/* @__PURE__ */
|
|
3883
|
+
/* @__PURE__ */ jsx7("input", { type: "radio", name: styleName, checked: option.id === themeId, onChange: () => setThemeId(option.id) }),
|
|
3884
|
+
/* @__PURE__ */ jsx7("span", { children: /* @__PURE__ */ jsx7("strong", { children: option.label }) })
|
|
3570
3885
|
] }, option.id))
|
|
3571
|
-
] })
|
|
3886
|
+
] }),
|
|
3887
|
+
/* @__PURE__ */ jsx7("p", { className: "settings-note", children: "Creation and style changes apply to your next question." })
|
|
3572
3888
|
]
|
|
3573
3889
|
}
|
|
3574
3890
|
)
|
|
3575
3891
|
] }),
|
|
3576
|
-
/* @__PURE__ */
|
|
3577
|
-
/* @__PURE__ */
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3892
|
+
/* @__PURE__ */ jsx7("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs6("div", { className: "panel-inner", children: [
|
|
3893
|
+
/* @__PURE__ */ jsx7("div", { className: "caption-slot", "data-captions": captionsOn && Boolean(line), "aria-hidden": !captionsOn || !line, children: /* @__PURE__ */ jsx7("div", { className: "caption-clip", children: /* @__PURE__ */ jsxs6(
|
|
3894
|
+
"div",
|
|
3895
|
+
{
|
|
3896
|
+
className: "line-row",
|
|
3897
|
+
"data-expanded": captionsExpanded,
|
|
3898
|
+
"data-actions-visible": captionControls.visible,
|
|
3899
|
+
onPointerMove: captionControls.onPointerEnter,
|
|
3900
|
+
onPointerLeave: captionControls.onPointerLeave,
|
|
3901
|
+
onPointerDown: captionControls.reveal,
|
|
3902
|
+
onFocusCapture: captionControls.onFocusCapture,
|
|
3903
|
+
onBlurCapture: captionControls.onBlurCapture,
|
|
3904
|
+
children: [
|
|
3905
|
+
captionsOn && line && /* @__PURE__ */ jsxs6("div", { className: "caption-actions", children: [
|
|
3906
|
+
/* @__PURE__ */ jsxs6("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
|
|
3907
|
+
captionsExpanded ? "Collapse" : "Expand",
|
|
3908
|
+
/* @__PURE__ */ jsx7(ChevronUp, {})
|
|
3909
|
+
] }),
|
|
3910
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "caption-action", "aria-label": "Hide subtitles", onClick: () => {
|
|
3911
|
+
setCaptionsOn(false);
|
|
3912
|
+
setCaptionsExpanded(false);
|
|
3913
|
+
}, children: /* @__PURE__ */ jsx7(Close, {}) })
|
|
3914
|
+
] }),
|
|
3915
|
+
captionsExpanded ? /* @__PURE__ */ jsx7("div", { className: "expanded-captions", role: "region", tabIndex: 0, "aria-label": "Expanded subtitles", children: fullTranscript.map((entry, index) => /* @__PURE__ */ jsx7("p", { children: entry }, index)) }) : /* @__PURE__ */ jsx7("p", { className: "line", "aria-live": "polite", children: line })
|
|
3916
|
+
]
|
|
3917
|
+
}
|
|
3918
|
+
) }) }),
|
|
3584
3919
|
(chat.error || listen.error) && /* @__PURE__ */ jsxs6("p", { className: "error", role: "status", children: [
|
|
3585
|
-
/* @__PURE__ */
|
|
3586
|
-
/* @__PURE__ */
|
|
3920
|
+
/* @__PURE__ */ jsx7(Warning, {}),
|
|
3921
|
+
/* @__PURE__ */ jsx7("span", { children: chat.error?.message ?? listen.error })
|
|
3587
3922
|
] }),
|
|
3588
|
-
/* @__PURE__ */ jsxs6("
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3923
|
+
/* @__PURE__ */ jsxs6("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
|
|
3924
|
+
(listen.listening || listen.thinking) && /* @__PURE__ */ jsx7("div", { className: "composer-meta", children: /* @__PURE__ */ jsx7("span", { role: "status", children: listen.listening ? "Listening\u2026 Tap the mic to finish, then review and send." : "Turning your words into a draft\u2026" }) }),
|
|
3925
|
+
/* @__PURE__ */ jsxs6("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
|
|
3926
|
+
event.preventDefault();
|
|
3927
|
+
ask(draft);
|
|
3928
|
+
}, children: [
|
|
3929
|
+
transport && /* @__PURE__ */ jsx7("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: () => {
|
|
3930
|
+
listen.stop();
|
|
3931
|
+
setEditing(false);
|
|
3932
|
+
resumeAfterInput.current = false;
|
|
3933
|
+
inputRef.current?.blur();
|
|
3934
|
+
transport.action();
|
|
3935
|
+
}, children: transport.icon }),
|
|
3936
|
+
/* @__PURE__ */ jsx7(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
|
|
3937
|
+
/* @__PURE__ */ jsx7("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
|
|
3938
|
+
/* @__PURE__ */ jsx7(
|
|
3939
|
+
"textarea",
|
|
3940
|
+
{
|
|
3941
|
+
id: promptId,
|
|
3942
|
+
ref: inputRef,
|
|
3943
|
+
rows: 1,
|
|
3944
|
+
value: draft,
|
|
3945
|
+
placeholder: listen.listening ? "Listening\u2026" : chat.turns.length ? "Ask a follow-up\u2026" : "Ask anything\u2026",
|
|
3946
|
+
onFocus: () => {
|
|
3947
|
+
if (!editing) beginInput();
|
|
3948
|
+
},
|
|
3949
|
+
onChange: (event) => {
|
|
3950
|
+
setDraft(event.target.value);
|
|
3951
|
+
event.target.style.height = "auto";
|
|
3952
|
+
event.target.style.height = `${Math.min(event.target.scrollHeight, 120)}px`;
|
|
3953
|
+
},
|
|
3954
|
+
onKeyDown: (event) => {
|
|
3955
|
+
if (event.key === "Escape") {
|
|
3956
|
+
event.stopPropagation();
|
|
3957
|
+
cancelInput();
|
|
3958
|
+
}
|
|
3959
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
3960
|
+
event.preventDefault();
|
|
3961
|
+
ask(draft);
|
|
3962
|
+
}
|
|
3607
3963
|
}
|
|
3608
3964
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
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
|
-
] })
|
|
3965
|
+
),
|
|
3966
|
+
listen.supported && /* @__PURE__ */ jsx7("button", { type: "button", className: `ghost${listen.listening ? " listening" : ""}`, "aria-label": listen.listening ? "Stop listening" : "Ask by voice", "aria-pressed": listen.listening, disabled: listen.thinking, onClick: () => beginInput(true), children: /* @__PURE__ */ jsx7(Mic, {}) }),
|
|
3967
|
+
/* @__PURE__ */ jsx7("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || listen.thinking, children: /* @__PURE__ */ jsx7(Send, {}) }),
|
|
3968
|
+
editing && /* @__PURE__ */ jsx7("button", { type: "button", className: "ghost", "aria-label": "Cancel question", onClick: cancelInput, children: /* @__PURE__ */ jsx7(Close, {}) })
|
|
3969
|
+
] })
|
|
3970
|
+
] }),
|
|
3971
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx7("p", { className: "dock-hint", children: "Speak or type. See where it takes you." })
|
|
3972
|
+
] }) })
|
|
3637
3973
|
]
|
|
3638
3974
|
}
|
|
3639
3975
|
);
|
|
@@ -3643,7 +3979,5 @@ export {
|
|
|
3643
3979
|
VideoError,
|
|
3644
3980
|
VideoPlayer,
|
|
3645
3981
|
createVideoChatVoice,
|
|
3646
|
-
useNarration,
|
|
3647
|
-
useVideo,
|
|
3648
3982
|
useVideoChat
|
|
3649
3983
|
};
|