ai-design-system 0.1.101 → 0.1.103

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.
@@ -16,8 +16,11 @@ const meta = {
16
16
  args: {
17
17
  onBack: fn(),
18
18
  onClose: fn(),
19
+ onSubmit: fn(),
19
20
  onPublish: fn(),
20
21
  onSceneChange: fn(),
22
+ onPlayingChange: fn(),
23
+ onDownload: fn(),
21
24
  },
22
25
  } satisfies Meta<typeof ScenePlayer>;
23
26
 
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import type { FormEvent } from "react";
4
5
  import { Button } from "@/components/primitives/Button";
5
6
  import { Icon } from "@/components/primitives/Icon";
7
+ import { PromptInput } from "@/components/composites/PromptInput";
8
+ import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
6
9
  import { cn } from "@/lib/utils";
7
10
 
8
11
  export interface SceneItem {
@@ -18,16 +21,31 @@ export interface SceneItem {
18
21
  [key: string]: unknown;
19
22
  }
20
23
 
21
- export interface ScenePlayerProps extends React.HTMLAttributes<HTMLDivElement> {
24
+ export interface ScenePlayerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit"> {
22
25
  scenes: SceneItem[];
23
26
  title?: string;
24
27
  avatarInitials?: string;
25
28
  avatarColor?: string;
26
29
  initialSceneIndex?: number;
27
30
  isMuted?: boolean;
31
+ isPlaying?: boolean;
32
+ onPlayingChange?: (isPlaying: boolean) => void;
33
+ videoUrl?: string;
34
+ downloadUrl?: string;
35
+ onDownload?: () => void;
28
36
  onBack?: () => void;
29
37
  onClose?: () => void;
30
- onPublish?: (currentScene: SceneItem) => void;
38
+ placeholder?: string;
39
+ promptValue?: string;
40
+ onPromptValueChange?: (value: string) => void;
41
+ onSubmit?: (
42
+ message: PromptInputMessage,
43
+ event: FormEvent<HTMLFormElement>
44
+ ) => void | Promise<void>;
45
+ loading?: boolean;
46
+ onStop?: () => void;
47
+ enableSpeech?: boolean;
48
+ onPublish?: (currentScene: SceneItem, text?: string) => void;
31
49
  publishPlaceholder?: string;
32
50
  onSceneChange?: (sceneIndex: number, scene: SceneItem) => void;
33
51
  }
@@ -41,10 +59,22 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
41
59
  avatarColor = "bg-indigo-600",
42
60
  initialSceneIndex = 0,
43
61
  isMuted: initialMuted = false,
62
+ isPlaying: isPlayingProp,
63
+ onPlayingChange,
64
+ videoUrl,
65
+ downloadUrl,
66
+ onDownload,
44
67
  onBack,
45
68
  onClose,
69
+ placeholder,
70
+ promptValue,
71
+ onPromptValueChange,
72
+ onSubmit,
73
+ loading = false,
74
+ onStop,
75
+ enableSpeech = true,
46
76
  onPublish,
47
- publishPlaceholder = "Ready to publish...",
77
+ publishPlaceholder = "Ask a question or provide instructions...",
48
78
  onSceneChange,
49
79
  className,
50
80
  ...props
@@ -60,11 +90,40 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
60
90
  return 0;
61
91
  });
62
92
 
93
+ const [isPlayingState, setIsPlayingState] = React.useState<boolean>(isPlayingProp ?? true);
94
+ const isPlaying = isPlayingProp !== undefined ? isPlayingProp : isPlayingState;
95
+
96
+ const setIsPlaying = React.useCallback(
97
+ (value: boolean | ((prev: boolean) => boolean)) => {
98
+ const nextVal = typeof value === "function" ? value(isPlaying) : value;
99
+ setIsPlayingState(nextVal);
100
+ onPlayingChange?.(nextVal);
101
+ },
102
+ [isPlaying, onPlayingChange]
103
+ );
104
+
105
+ const togglePlay = React.useCallback(() => {
106
+ setIsPlaying((prev) => !prev);
107
+ }, [setIsPlaying]);
108
+
63
109
  const [isMuted, setIsMuted] = React.useState<boolean>(initialMuted);
64
110
  const videoRef = React.useRef<HTMLVideoElement | null>(null);
65
111
 
66
112
  const activeScene = scenes[currentSceneIndex] || scenes[0];
67
113
 
114
+ const handlePromptSubmit = React.useCallback(
115
+ (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
116
+ if (onSubmit) {
117
+ onSubmit(message, event);
118
+ return;
119
+ }
120
+ if (onPublish) {
121
+ onPublish(activeScene, message.text);
122
+ }
123
+ },
124
+ [onSubmit, onPublish, activeScene]
125
+ );
126
+
68
127
  const handlePrev = React.useCallback(() => {
69
128
  if (!scenes.length) return;
70
129
  setCurrentSceneIndex((prev) => {
@@ -84,41 +143,91 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
84
143
  }, [scenes, onSceneChange]);
85
144
 
86
145
  const handleSceneEnded = React.useCallback(() => {
87
- if (!scenes.length) return;
146
+ if (!scenes.length || !isPlaying) return;
88
147
  setCurrentSceneIndex((prev) => {
89
148
  const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
90
149
  onSceneChange?.(nextIdx, scenes[nextIdx]);
91
150
  return nextIdx;
92
151
  });
93
- }, [scenes, onSceneChange]);
152
+ }, [scenes, onSceneChange, isPlaying]);
94
153
 
95
- // Autoplay & mute handling
154
+ const handleDownload = React.useCallback(async () => {
155
+ if (onDownload) {
156
+ onDownload();
157
+ return;
158
+ }
159
+ const targetUrl = downloadUrl || videoUrl || activeScene?.scene_clip_url;
160
+ if (!targetUrl) return;
161
+
162
+ try {
163
+ const response = await fetch(targetUrl);
164
+ if (!response.ok) throw new Error("Fetch failed");
165
+ const blob = await response.blob();
166
+ const blobUrl = URL.createObjectURL(blob);
167
+ const link = document.createElement("a");
168
+ link.href = blobUrl;
169
+ link.download = targetUrl.split("/").pop()?.split("?")[0] || "video.mp4";
170
+ document.body.appendChild(link);
171
+ link.click();
172
+ document.body.removeChild(link);
173
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
174
+ } catch {
175
+ const link = document.createElement("a");
176
+ link.href = targetUrl;
177
+ link.download = targetUrl.split("/").pop()?.split("?")[0] || "video.mp4";
178
+ link.target = "_blank";
179
+ link.rel = "noopener noreferrer";
180
+ document.body.appendChild(link);
181
+ link.click();
182
+ document.body.removeChild(link);
183
+ }
184
+ }, [onDownload, downloadUrl, videoUrl, activeScene?.scene_clip_url]);
185
+
186
+ // Playback & mute synchronization
96
187
  React.useEffect(() => {
97
188
  const video = videoRef.current;
98
189
  if (!video) return;
99
190
 
100
191
  video.muted = isMuted;
101
- const playPromise = video.play();
102
- if (playPromise !== undefined) {
103
- playPromise.catch(() => {
104
- if (!isMuted) {
105
- video.muted = true;
106
- setIsMuted(true);
107
- video.play().catch(() => {});
108
- }
109
- });
192
+
193
+ if (isPlaying) {
194
+ const playPromise = video.play();
195
+ if (playPromise !== undefined) {
196
+ playPromise.catch(() => {
197
+ if (!isMuted) {
198
+ video.muted = true;
199
+ setIsMuted(true);
200
+ video.play().catch(() => {});
201
+ }
202
+ });
203
+ }
204
+ } else {
205
+ video.pause();
110
206
  }
111
- }, [activeScene?.scene_clip_url, isMuted]);
207
+ }, [activeScene?.scene_clip_url, isMuted, isPlaying]);
112
208
 
113
- // Keyboard navigation
209
+ // Keyboard navigation & playback controls
114
210
  React.useEffect(() => {
115
211
  const handleKeyDown = (e: KeyboardEvent) => {
212
+ const target = e.target as HTMLElement | null;
213
+ if (
214
+ target &&
215
+ (target.tagName === "INPUT" ||
216
+ target.tagName === "TEXTAREA" ||
217
+ target.isContentEditable)
218
+ ) {
219
+ return;
220
+ }
221
+
116
222
  if (e.key === "ArrowLeft") {
117
223
  e.preventDefault();
118
224
  handlePrev();
119
- } else if (e.key === "ArrowRight" || e.key === " ") {
225
+ } else if (e.key === "ArrowRight") {
120
226
  e.preventDefault();
121
227
  handleNext();
228
+ } else if (e.key === " " || e.key === "k" || e.key === "K") {
229
+ e.preventDefault();
230
+ togglePlay();
122
231
  } else if (e.key === "Escape") {
123
232
  e.preventDefault();
124
233
  handleBack?.();
@@ -130,7 +239,7 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
130
239
 
131
240
  window.addEventListener("keydown", handleKeyDown);
132
241
  return () => window.removeEventListener("keydown", handleKeyDown);
133
- }, [handlePrev, handleNext, handleBack]);
242
+ }, [handlePrev, handleNext, handleBack, togglePlay]);
134
243
 
135
244
  if (!scenes || scenes.length === 0) {
136
245
  return (
@@ -203,14 +312,6 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
203
312
  </Button>
204
313
  )}
205
314
 
206
- <div
207
- className={cn(
208
- "w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm text-white shrink-0 ring-1.5 ring-white/80 overflow-hidden",
209
- avatarColor
210
- )}
211
- >
212
- {avatarInitials}
213
- </div>
214
315
  <div className="flex flex-col min-w-0 justify-center">
215
316
  <span className="font-bold text-sm text-white truncate max-w-[200px]">
216
317
  {title}
@@ -222,6 +323,18 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
222
323
  </div>
223
324
 
224
325
  <div className="flex items-center gap-2 shrink-0">
326
+ {/* Play/Pause Toggle Button */}
327
+ <Button
328
+ variant="ghost"
329
+ size="icon"
330
+ data-testid="btn-toggle-play"
331
+ aria-label={isPlaying ? "Pause scene" : "Play scene"}
332
+ onClick={togglePlay}
333
+ className="h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer"
334
+ >
335
+ <Icon name={isPlaying ? "pause" : "play"} className="w-4 h-4" />
336
+ </Button>
337
+
225
338
  {/* Mute/Unmute Toggle Button */}
226
339
  <Button
227
340
  variant="ghost"
@@ -233,6 +346,18 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
233
346
  >
234
347
  <Icon name={isMuted ? "volume-x" : "volume-2"} className="w-4 h-4" />
235
348
  </Button>
349
+
350
+ {/* Download Video Button */}
351
+ <Button
352
+ variant="ghost"
353
+ size="icon"
354
+ data-testid="btn-download"
355
+ aria-label="Download video"
356
+ onClick={handleDownload}
357
+ className="h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer"
358
+ >
359
+ <Icon name="download" className="w-4 h-4" />
360
+ </Button>
236
361
  </div>
237
362
  </div>
238
363
 
@@ -269,21 +394,29 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
269
394
  </div>
270
395
  </div>
271
396
 
272
- {/* Bottom Action Bar */}
273
- <div className="flex items-center gap-3 px-4 py-3.5 bg-black z-20 shrink-0">
274
- <div className="flex-1 h-11 rounded-full bg-white/15 border border-white/25 px-4 flex items-center text-sm text-white/65 select-none truncate">
275
- {publishPlaceholder}
276
- </div>
277
- <Button
278
- variant="ghost"
279
- size="icon"
280
- data-testid="btn-publish-scene"
281
- aria-label="Publish scene"
282
- onClick={() => onPublish?.(activeScene)}
283
- className="w-11 h-11 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white shrink-0 p-0 shadow-md cursor-pointer transition-transform active:scale-95"
284
- >
285
- <Icon name="send" className="w-4 h-4 text-white" />
286
- </Button>
397
+ {/* Bottom PromptInput Composite from Design System */}
398
+ <div
399
+ className="p-3 bg-gradient-to-t from-black via-black/90 to-transparent pt-4 z-20 shrink-0"
400
+ onFocusCapture={() => {
401
+ setIsPlaying(false);
402
+ }}
403
+ onClickCapture={() => {
404
+ setIsPlaying(false);
405
+ }}
406
+ onTouchStartCapture={() => {
407
+ setIsPlaying(false);
408
+ }}
409
+ >
410
+ <PromptInput
411
+ placeholder={placeholder || publishPlaceholder}
412
+ value={promptValue}
413
+ onChange={onPromptValueChange}
414
+ onSubmit={handlePromptSubmit}
415
+ loading={loading}
416
+ onStop={onStop}
417
+ enableSpeech={enableSpeech}
418
+ className="border border-border bg-background shadow-md overflow-hidden rounded-2xl"
419
+ />
287
420
  </div>
288
421
  </div>
289
422
  );
package/dist/index.cjs CHANGED
@@ -1026,6 +1026,14 @@ var defaultIcons = {
1026
1026
  viewBox: "0 0 24 24",
1027
1027
  path: "M8 5v14l11-7z"
1028
1028
  },
1029
+ "pause": {
1030
+ name: "pause",
1031
+ viewBox: "0 0 24 24",
1032
+ elements: [
1033
+ { type: "rect", attrs: { x: "14", y: "4", width: "4", height: "16", rx: "1" } },
1034
+ { type: "rect", attrs: { x: "6", y: "4", width: "4", height: "16", rx: "1" } }
1035
+ ]
1036
+ },
1029
1037
  "file-text": {
1030
1038
  name: "file-text",
1031
1039
  viewBox: "0 0 24 24",
@@ -14575,10 +14583,22 @@ var ScenePlayer = React3__namespace.forwardRef(
14575
14583
  avatarColor = "bg-indigo-600",
14576
14584
  initialSceneIndex = 0,
14577
14585
  isMuted: initialMuted = false,
14586
+ isPlaying: isPlayingProp,
14587
+ onPlayingChange,
14588
+ videoUrl,
14589
+ downloadUrl,
14590
+ onDownload,
14578
14591
  onBack,
14579
14592
  onClose,
14593
+ placeholder,
14594
+ promptValue,
14595
+ onPromptValueChange,
14596
+ onSubmit,
14597
+ loading = false,
14598
+ onStop,
14599
+ enableSpeech = true,
14580
14600
  onPublish,
14581
- publishPlaceholder = "Ready to publish...",
14601
+ publishPlaceholder = "Ask a question or provide instructions...",
14582
14602
  onSceneChange,
14583
14603
  className
14584
14604
  } = _b, props = __objRest(_b, [
@@ -14588,8 +14608,20 @@ var ScenePlayer = React3__namespace.forwardRef(
14588
14608
  "avatarColor",
14589
14609
  "initialSceneIndex",
14590
14610
  "isMuted",
14611
+ "isPlaying",
14612
+ "onPlayingChange",
14613
+ "videoUrl",
14614
+ "downloadUrl",
14615
+ "onDownload",
14591
14616
  "onBack",
14592
14617
  "onClose",
14618
+ "placeholder",
14619
+ "promptValue",
14620
+ "onPromptValueChange",
14621
+ "onSubmit",
14622
+ "loading",
14623
+ "onStop",
14624
+ "enableSpeech",
14593
14625
  "onPublish",
14594
14626
  "publishPlaceholder",
14595
14627
  "onSceneChange",
@@ -14603,9 +14635,34 @@ var ScenePlayer = React3__namespace.forwardRef(
14603
14635
  }
14604
14636
  return 0;
14605
14637
  });
14638
+ const [isPlayingState, setIsPlayingState] = React3__namespace.useState(isPlayingProp != null ? isPlayingProp : true);
14639
+ const isPlaying = isPlayingProp !== void 0 ? isPlayingProp : isPlayingState;
14640
+ const setIsPlaying = React3__namespace.useCallback(
14641
+ (value) => {
14642
+ const nextVal = typeof value === "function" ? value(isPlaying) : value;
14643
+ setIsPlayingState(nextVal);
14644
+ onPlayingChange == null ? void 0 : onPlayingChange(nextVal);
14645
+ },
14646
+ [isPlaying, onPlayingChange]
14647
+ );
14648
+ const togglePlay = React3__namespace.useCallback(() => {
14649
+ setIsPlaying((prev) => !prev);
14650
+ }, [setIsPlaying]);
14606
14651
  const [isMuted, setIsMuted] = React3__namespace.useState(initialMuted);
14607
14652
  const videoRef = React3__namespace.useRef(null);
14608
14653
  const activeScene = scenes[currentSceneIndex] || scenes[0];
14654
+ const handlePromptSubmit = React3__namespace.useCallback(
14655
+ (message, event) => {
14656
+ if (onSubmit) {
14657
+ onSubmit(message, event);
14658
+ return;
14659
+ }
14660
+ if (onPublish) {
14661
+ onPublish(activeScene, message.text);
14662
+ }
14663
+ },
14664
+ [onSubmit, onPublish, activeScene]
14665
+ );
14609
14666
  const handlePrev = React3__namespace.useCallback(() => {
14610
14667
  if (!scenes.length) return;
14611
14668
  setCurrentSceneIndex((prev) => {
@@ -14623,37 +14680,79 @@ var ScenePlayer = React3__namespace.forwardRef(
14623
14680
  });
14624
14681
  }, [scenes, onSceneChange]);
14625
14682
  const handleSceneEnded = React3__namespace.useCallback(() => {
14626
- if (!scenes.length) return;
14683
+ if (!scenes.length || !isPlaying) return;
14627
14684
  setCurrentSceneIndex((prev) => {
14628
14685
  const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
14629
14686
  onSceneChange == null ? void 0 : onSceneChange(nextIdx, scenes[nextIdx]);
14630
14687
  return nextIdx;
14631
14688
  });
14632
- }, [scenes, onSceneChange]);
14689
+ }, [scenes, onSceneChange, isPlaying]);
14690
+ const handleDownload = React3__namespace.useCallback(async () => {
14691
+ var _a3, _b2;
14692
+ if (onDownload) {
14693
+ onDownload();
14694
+ return;
14695
+ }
14696
+ const targetUrl = downloadUrl || videoUrl || (activeScene == null ? void 0 : activeScene.scene_clip_url);
14697
+ if (!targetUrl) return;
14698
+ try {
14699
+ const response = await fetch(targetUrl);
14700
+ if (!response.ok) throw new Error("Fetch failed");
14701
+ const blob = await response.blob();
14702
+ const blobUrl = URL.createObjectURL(blob);
14703
+ const link = document.createElement("a");
14704
+ link.href = blobUrl;
14705
+ link.download = ((_a3 = targetUrl.split("/").pop()) == null ? void 0 : _a3.split("?")[0]) || "video.mp4";
14706
+ document.body.appendChild(link);
14707
+ link.click();
14708
+ document.body.removeChild(link);
14709
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 1e3);
14710
+ } catch (e) {
14711
+ const link = document.createElement("a");
14712
+ link.href = targetUrl;
14713
+ link.download = ((_b2 = targetUrl.split("/").pop()) == null ? void 0 : _b2.split("?")[0]) || "video.mp4";
14714
+ link.target = "_blank";
14715
+ link.rel = "noopener noreferrer";
14716
+ document.body.appendChild(link);
14717
+ link.click();
14718
+ document.body.removeChild(link);
14719
+ }
14720
+ }, [onDownload, downloadUrl, videoUrl, activeScene == null ? void 0 : activeScene.scene_clip_url]);
14633
14721
  React3__namespace.useEffect(() => {
14634
14722
  const video = videoRef.current;
14635
14723
  if (!video) return;
14636
14724
  video.muted = isMuted;
14637
- const playPromise = video.play();
14638
- if (playPromise !== void 0) {
14639
- playPromise.catch(() => {
14640
- if (!isMuted) {
14641
- video.muted = true;
14642
- setIsMuted(true);
14643
- video.play().catch(() => {
14644
- });
14645
- }
14646
- });
14725
+ if (isPlaying) {
14726
+ const playPromise = video.play();
14727
+ if (playPromise !== void 0) {
14728
+ playPromise.catch(() => {
14729
+ if (!isMuted) {
14730
+ video.muted = true;
14731
+ setIsMuted(true);
14732
+ video.play().catch(() => {
14733
+ });
14734
+ }
14735
+ });
14736
+ }
14737
+ } else {
14738
+ video.pause();
14647
14739
  }
14648
- }, [activeScene == null ? void 0 : activeScene.scene_clip_url, isMuted]);
14740
+ }, [activeScene == null ? void 0 : activeScene.scene_clip_url, isMuted, isPlaying]);
14649
14741
  React3__namespace.useEffect(() => {
14650
14742
  const handleKeyDown = (e) => {
14743
+ const target = e.target;
14744
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
14745
+ return;
14746
+ }
14651
14747
  if (e.key === "ArrowLeft") {
14652
14748
  e.preventDefault();
14653
14749
  handlePrev();
14654
- } else if (e.key === "ArrowRight" || e.key === " ") {
14750
+ } else if (e.key === "ArrowRight") {
14655
14751
  e.preventDefault();
14656
14752
  handleNext();
14753
+ } else if (e.key === " " || e.key === "k" || e.key === "K") {
14754
+ e.preventDefault();
14755
+ togglePlay();
14657
14756
  } else if (e.key === "Escape") {
14658
14757
  e.preventDefault();
14659
14758
  handleBack == null ? void 0 : handleBack();
@@ -14664,7 +14763,7 @@ var ScenePlayer = React3__namespace.forwardRef(
14664
14763
  };
14665
14764
  window.addEventListener("keydown", handleKeyDown);
14666
14765
  return () => window.removeEventListener("keydown", handleKeyDown);
14667
- }, [handlePrev, handleNext, handleBack]);
14766
+ }, [handlePrev, handleNext, handleBack, togglePlay]);
14668
14767
  if (!scenes || scenes.length === 0) {
14669
14768
  return /* @__PURE__ */ jsxRuntime.jsxs(
14670
14769
  "div",
@@ -14734,16 +14833,6 @@ var ScenePlayer = React3__namespace.forwardRef(
14734
14833
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "arrow-left", className: "w-5 h-5 text-white" })
14735
14834
  }
14736
14835
  ),
14737
- /* @__PURE__ */ jsxRuntime.jsx(
14738
- "div",
14739
- {
14740
- className: cn(
14741
- "w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm text-white shrink-0 ring-1.5 ring-white/80 overflow-hidden",
14742
- avatarColor
14743
- ),
14744
- children: avatarInitials
14745
- }
14746
- ),
14747
14836
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-w-0 justify-center", children: [
14748
14837
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold text-sm text-white truncate max-w-[200px]", children: title }),
14749
14838
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-white/75 truncate", children: [
@@ -14754,18 +14843,44 @@ var ScenePlayer = React3__namespace.forwardRef(
14754
14843
  ] })
14755
14844
  ] })
14756
14845
  ] }),
14757
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
14758
- Button2,
14759
- {
14760
- variant: "ghost",
14761
- size: "icon",
14762
- "data-testid": "btn-toggle-mute",
14763
- "aria-label": isMuted ? "Unmute audio" : "Mute audio",
14764
- onClick: () => setIsMuted((prev) => !prev),
14765
- className: "h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer",
14766
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: isMuted ? "volume-x" : "volume-2", className: "w-4 h-4" })
14767
- }
14768
- ) })
14846
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
14847
+ /* @__PURE__ */ jsxRuntime.jsx(
14848
+ Button2,
14849
+ {
14850
+ variant: "ghost",
14851
+ size: "icon",
14852
+ "data-testid": "btn-toggle-play",
14853
+ "aria-label": isPlaying ? "Pause scene" : "Play scene",
14854
+ onClick: togglePlay,
14855
+ className: "h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer",
14856
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: isPlaying ? "pause" : "play", className: "w-4 h-4" })
14857
+ }
14858
+ ),
14859
+ /* @__PURE__ */ jsxRuntime.jsx(
14860
+ Button2,
14861
+ {
14862
+ variant: "ghost",
14863
+ size: "icon",
14864
+ "data-testid": "btn-toggle-mute",
14865
+ "aria-label": isMuted ? "Unmute audio" : "Mute audio",
14866
+ onClick: () => setIsMuted((prev) => !prev),
14867
+ className: "h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer",
14868
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: isMuted ? "volume-x" : "volume-2", className: "w-4 h-4" })
14869
+ }
14870
+ ),
14871
+ /* @__PURE__ */ jsxRuntime.jsx(
14872
+ Button2,
14873
+ {
14874
+ variant: "ghost",
14875
+ size: "icon",
14876
+ "data-testid": "btn-download",
14877
+ "aria-label": "Download video",
14878
+ onClick: handleDownload,
14879
+ className: "h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer",
14880
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "download", className: "w-4 h-4" })
14881
+ }
14882
+ )
14883
+ ] })
14769
14884
  ] }),
14770
14885
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 relative flex items-center justify-center bg-black overflow-hidden min-h-0", children: [
14771
14886
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14805,21 +14920,34 @@ var ScenePlayer = React3__namespace.forwardRef(
14805
14920
  )
14806
14921
  ] })
14807
14922
  ] }),
14808
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3.5 bg-black z-20 shrink-0", children: [
14809
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 h-11 rounded-full bg-white/15 border border-white/25 px-4 flex items-center text-sm text-white/65 select-none truncate", children: publishPlaceholder }),
14810
- /* @__PURE__ */ jsxRuntime.jsx(
14811
- Button2,
14812
- {
14813
- variant: "ghost",
14814
- size: "icon",
14815
- "data-testid": "btn-publish-scene",
14816
- "aria-label": "Publish scene",
14817
- onClick: () => onPublish == null ? void 0 : onPublish(activeScene),
14818
- className: "w-11 h-11 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white shrink-0 p-0 shadow-md cursor-pointer transition-transform active:scale-95",
14819
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "send", className: "w-4 h-4 text-white" })
14820
- }
14821
- )
14822
- ] })
14923
+ /* @__PURE__ */ jsxRuntime.jsx(
14924
+ "div",
14925
+ {
14926
+ className: "p-3 bg-gradient-to-t from-black via-black/90 to-transparent pt-4 z-20 shrink-0",
14927
+ onFocusCapture: () => {
14928
+ setIsPlaying(false);
14929
+ },
14930
+ onClickCapture: () => {
14931
+ setIsPlaying(false);
14932
+ },
14933
+ onTouchStartCapture: () => {
14934
+ setIsPlaying(false);
14935
+ },
14936
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14937
+ PromptInput2,
14938
+ {
14939
+ placeholder: placeholder || publishPlaceholder,
14940
+ value: promptValue,
14941
+ onChange: onPromptValueChange,
14942
+ onSubmit: handlePromptSubmit,
14943
+ loading,
14944
+ onStop,
14945
+ enableSpeech,
14946
+ className: "border border-border bg-background shadow-md overflow-hidden rounded-2xl"
14947
+ }
14948
+ )
14949
+ }
14950
+ )
14823
14951
  ]
14824
14952
  })
14825
14953
  );