ai-design-system 0.1.104 → 0.1.106
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/components/composites/PromptInput/PromptInput.tsx +23 -12
- package/components/composites/ScenePlayer/ScenePlayer.tsx +123 -37
- package/dist/index.cjs +188 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +22 -3
- package/dist/index.d.ts +20 -20
- package/dist/index.js +188 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@ import { SpeechInput, type SpeechInputProps } from "@/components/ai-elements/spe
|
|
|
35
35
|
import type { FormEvent } from "react";
|
|
36
36
|
import { Button } from "@/components/primitives/Button";
|
|
37
37
|
import { Icon } from "@/components/primitives/Icon";
|
|
38
|
+
import { InputGroupAddon } from "@/components/primitives/InputGroup";
|
|
38
39
|
import { cn } from "@/lib/utils";
|
|
39
40
|
|
|
40
41
|
export interface PromptInputContextProps {
|
|
@@ -93,6 +94,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
93
94
|
maxFiles,
|
|
94
95
|
maxFileSize,
|
|
95
96
|
onError,
|
|
97
|
+
className,
|
|
96
98
|
...props
|
|
97
99
|
}) => {
|
|
98
100
|
const isRowVariant = variant === "row" || variant === "inline";
|
|
@@ -140,30 +142,34 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
140
142
|
maxFiles={maxFiles}
|
|
141
143
|
maxFileSize={maxFileSize}
|
|
142
144
|
onError={onError}
|
|
145
|
+
className={cn(
|
|
146
|
+
"[&>[data-slot=input-group]]:rounded-2xl",
|
|
147
|
+
className?.includes("border") &&
|
|
148
|
+
"[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
|
|
149
|
+
className
|
|
150
|
+
)}
|
|
143
151
|
{...props}
|
|
144
152
|
>
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</PromptInputBody>
|
|
154
|
-
<div className="pr-1.5 shrink-0 flex items-center self-center">
|
|
153
|
+
{showAttachments && <AttachmentPreviews />}
|
|
154
|
+
<PromptInputTextarea
|
|
155
|
+
placeholder={placeholder}
|
|
156
|
+
disabled={disabled}
|
|
157
|
+
onChange={isControlled ? handleControlledChange : undefined}
|
|
158
|
+
className="min-h-9 py-2 px-3 text-sm resize-none border-0 shadow-none focus-visible:ring-0 bg-transparent flex-1 field-sizing-content"
|
|
159
|
+
/>
|
|
160
|
+
<InputGroupAddon align="inline-end" className="self-end pb-1 pr-1.5 shrink-0">
|
|
155
161
|
<PromptInputSubmit
|
|
156
162
|
disabled={disabled || (loading && !onStop)}
|
|
157
163
|
status={loading ? (onStop ? "streaming" : "submitted") : undefined}
|
|
158
164
|
onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
|
|
159
165
|
className={cn(
|
|
160
|
-
"
|
|
166
|
+
"rounded-lg bg-blue-600 hover:bg-blue-700 text-white transition-colors",
|
|
161
167
|
isStopping && "bg-transparent hover:bg-accent"
|
|
162
168
|
)}
|
|
163
169
|
>
|
|
164
170
|
{loading ? undefined : <Icon name="corner-down-left" size="sm" />}
|
|
165
171
|
</PromptInputSubmit>
|
|
166
|
-
</
|
|
172
|
+
</InputGroupAddon>
|
|
167
173
|
</AIPromptInput>
|
|
168
174
|
) : (
|
|
169
175
|
<AIPromptInput
|
|
@@ -173,6 +179,11 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
173
179
|
maxFiles={maxFiles}
|
|
174
180
|
maxFileSize={maxFileSize}
|
|
175
181
|
onError={onError}
|
|
182
|
+
className={cn(
|
|
183
|
+
className?.includes("border") &&
|
|
184
|
+
"[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
|
|
185
|
+
className
|
|
186
|
+
)}
|
|
176
187
|
{...props}
|
|
177
188
|
>
|
|
178
189
|
<PromptInputBody>
|
|
@@ -94,6 +94,26 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
94
94
|
return 0;
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// Dual-video buffer for gapless playback between scenes:
|
|
98
|
+
// One slot actively plays the current scene, while the other slot preloads the next scene in the background.
|
|
99
|
+
const [activeSlot, setActiveSlot] = React.useState<0 | 1>(0);
|
|
100
|
+
const [slot0Index, setSlot0Index] = React.useState<number>(() => {
|
|
101
|
+
if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
|
|
102
|
+
return initialSceneIndex;
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
});
|
|
106
|
+
const [slot1Index, setSlot1Index] = React.useState<number>(() => {
|
|
107
|
+
if (scenes.length > 1) {
|
|
108
|
+
const init = initialSceneIndex >= 0 && initialSceneIndex < scenes.length ? initialSceneIndex : 0;
|
|
109
|
+
return (init + 1) % scenes.length;
|
|
110
|
+
}
|
|
111
|
+
return 0;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const videoRef0 = React.useRef<HTMLVideoElement | null>(null);
|
|
115
|
+
const videoRef1 = React.useRef<HTMLVideoElement | null>(null);
|
|
116
|
+
|
|
97
117
|
const [isPlayingState, setIsPlayingState] = React.useState<boolean>(isPlayingProp ?? true);
|
|
98
118
|
const isPlaying = isPlayingProp !== undefined ? isPlayingProp : isPlayingState;
|
|
99
119
|
|
|
@@ -111,10 +131,46 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
111
131
|
}, [setIsPlaying]);
|
|
112
132
|
|
|
113
133
|
const [isMuted, setIsMuted] = React.useState<boolean>(initialMuted);
|
|
114
|
-
const videoRef = React.useRef<HTMLVideoElement | null>(null);
|
|
115
134
|
|
|
116
135
|
const activeScene = scenes[currentSceneIndex] || scenes[0];
|
|
117
136
|
|
|
137
|
+
const goToScene = React.useCallback(
|
|
138
|
+
(targetIdx: number) => {
|
|
139
|
+
if (!scenes.length) return;
|
|
140
|
+
const clampedIdx = (targetIdx + scenes.length) % scenes.length;
|
|
141
|
+
onSceneChange?.(clampedIdx, scenes[clampedIdx]);
|
|
142
|
+
setCurrentSceneIndex(clampedIdx);
|
|
143
|
+
|
|
144
|
+
if (scenes.length <= 1) {
|
|
145
|
+
setSlot0Index(clampedIdx);
|
|
146
|
+
const v0 = videoRef0.current;
|
|
147
|
+
if (v0) {
|
|
148
|
+
v0.currentTime = 0;
|
|
149
|
+
if (isPlaying) v0.play().catch(() => {});
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nextActiveSlot: 0 | 1 = activeSlot === 0 ? 1 : 0;
|
|
155
|
+
const targetVideo = nextActiveSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
156
|
+
if (targetVideo) {
|
|
157
|
+
targetVideo.currentTime = 0;
|
|
158
|
+
}
|
|
159
|
+
const sceneAfterTarget = (clampedIdx + 1) % scenes.length;
|
|
160
|
+
|
|
161
|
+
if (nextActiveSlot === 0) {
|
|
162
|
+
setSlot0Index(clampedIdx);
|
|
163
|
+
setSlot1Index(sceneAfterTarget);
|
|
164
|
+
} else {
|
|
165
|
+
setSlot1Index(clampedIdx);
|
|
166
|
+
setSlot0Index(sceneAfterTarget);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setActiveSlot(nextActiveSlot);
|
|
170
|
+
},
|
|
171
|
+
[scenes, activeSlot, isPlaying, onSceneChange]
|
|
172
|
+
);
|
|
173
|
+
|
|
118
174
|
const handlePromptSubmit = React.useCallback(
|
|
119
175
|
(message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
|
|
120
176
|
if (onSubmit) {
|
|
@@ -130,30 +186,20 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
130
186
|
|
|
131
187
|
const handlePrev = React.useCallback(() => {
|
|
132
188
|
if (!scenes.length) return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return nextIdx;
|
|
137
|
-
});
|
|
138
|
-
}, [scenes, onSceneChange]);
|
|
189
|
+
const prevIdx = currentSceneIndex > 0 ? currentSceneIndex - 1 : scenes.length - 1;
|
|
190
|
+
goToScene(prevIdx);
|
|
191
|
+
}, [scenes.length, currentSceneIndex, goToScene]);
|
|
139
192
|
|
|
140
193
|
const handleNext = React.useCallback(() => {
|
|
141
194
|
if (!scenes.length) return;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return nextIdx;
|
|
146
|
-
});
|
|
147
|
-
}, [scenes, onSceneChange]);
|
|
195
|
+
const nextIdx = currentSceneIndex < scenes.length - 1 ? currentSceneIndex + 1 : 0;
|
|
196
|
+
goToScene(nextIdx);
|
|
197
|
+
}, [scenes.length, currentSceneIndex, goToScene]);
|
|
148
198
|
|
|
149
199
|
const handleSceneEnded = React.useCallback(() => {
|
|
150
200
|
if (!scenes.length || !isPlaying) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
onSceneChange?.(nextIdx, scenes[nextIdx]);
|
|
154
|
-
return nextIdx;
|
|
155
|
-
});
|
|
156
|
-
}, [scenes, onSceneChange, isPlaying]);
|
|
201
|
+
handleNext();
|
|
202
|
+
}, [scenes.length, isPlaying, handleNext]);
|
|
157
203
|
|
|
158
204
|
const handleDownload = React.useCallback(async () => {
|
|
159
205
|
if (onDownload) {
|
|
@@ -187,28 +233,48 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
187
233
|
}
|
|
188
234
|
}, [onDownload, downloadUrl, videoUrl, activeScene?.scene_clip_url]);
|
|
189
235
|
|
|
190
|
-
// Playback & mute synchronization
|
|
236
|
+
// Playback & mute synchronization across dual slots
|
|
191
237
|
React.useEffect(() => {
|
|
192
|
-
const
|
|
193
|
-
|
|
238
|
+
const activeVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
239
|
+
const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
|
|
194
240
|
|
|
195
|
-
|
|
241
|
+
if (idleVideo) {
|
|
242
|
+
idleVideo.pause();
|
|
243
|
+
idleVideo.muted = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!activeVideo) return;
|
|
247
|
+
|
|
248
|
+
activeVideo.muted = isMuted;
|
|
196
249
|
|
|
197
250
|
if (isPlaying) {
|
|
198
|
-
|
|
251
|
+
if (activeVideo.ended) {
|
|
252
|
+
activeVideo.currentTime = 0;
|
|
253
|
+
}
|
|
254
|
+
const playPromise = activeVideo.play();
|
|
199
255
|
if (playPromise !== undefined) {
|
|
200
256
|
playPromise.catch(() => {
|
|
201
257
|
if (!isMuted) {
|
|
202
|
-
|
|
258
|
+
activeVideo.muted = true;
|
|
203
259
|
setIsMuted(true);
|
|
204
|
-
|
|
260
|
+
activeVideo.play().catch(() => {});
|
|
205
261
|
}
|
|
206
262
|
});
|
|
207
263
|
}
|
|
208
264
|
} else {
|
|
209
|
-
|
|
265
|
+
activeVideo.pause();
|
|
210
266
|
}
|
|
211
|
-
}, [
|
|
267
|
+
}, [activeSlot, isPlaying, isMuted, slot0Index, slot1Index]);
|
|
268
|
+
|
|
269
|
+
React.useEffect(() => {
|
|
270
|
+
if (!scenes || scenes.length === 0) return;
|
|
271
|
+
if (currentSceneIndex >= scenes.length) {
|
|
272
|
+
setCurrentSceneIndex(0);
|
|
273
|
+
setSlot0Index(0);
|
|
274
|
+
setSlot1Index(scenes.length > 1 ? 1 : 0);
|
|
275
|
+
setActiveSlot(0);
|
|
276
|
+
}
|
|
277
|
+
}, [scenes, currentSceneIndex]);
|
|
212
278
|
|
|
213
279
|
// Keyboard navigation & playback controls
|
|
214
280
|
React.useEffect(() => {
|
|
@@ -367,20 +433,40 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
367
433
|
|
|
368
434
|
{/* Main Scene Video Player Stage */}
|
|
369
435
|
<div className="flex-1 relative flex items-center justify-center bg-black overflow-hidden min-h-0">
|
|
436
|
+
{/* Slot 0 Video */}
|
|
370
437
|
<video
|
|
371
|
-
ref={
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
autoPlay
|
|
438
|
+
ref={videoRef0}
|
|
439
|
+
src={scenes[slot0Index]?.scene_clip_url}
|
|
440
|
+
preload="auto"
|
|
375
441
|
playsInline
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
className=
|
|
442
|
+
muted={activeSlot === 0 ? isMuted : true}
|
|
443
|
+
onEnded={activeSlot === 0 ? handleSceneEnded : undefined}
|
|
444
|
+
data-testid="scene-video-0"
|
|
445
|
+
className={cn(
|
|
446
|
+
"w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-150",
|
|
447
|
+
activeSlot === 0 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
|
|
448
|
+
)}
|
|
380
449
|
/>
|
|
381
450
|
|
|
451
|
+
{/* Slot 1 Video (preloads next scene in background for gapless playback) */}
|
|
452
|
+
{scenes.length > 1 && (
|
|
453
|
+
<video
|
|
454
|
+
ref={videoRef1}
|
|
455
|
+
src={scenes[slot1Index]?.scene_clip_url}
|
|
456
|
+
preload="auto"
|
|
457
|
+
playsInline
|
|
458
|
+
muted={activeSlot === 1 ? isMuted : true}
|
|
459
|
+
onEnded={activeSlot === 1 ? handleSceneEnded : undefined}
|
|
460
|
+
data-testid="scene-video-1"
|
|
461
|
+
className={cn(
|
|
462
|
+
"w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-150",
|
|
463
|
+
activeSlot === 1 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
|
|
464
|
+
)}
|
|
465
|
+
/>
|
|
466
|
+
)}
|
|
467
|
+
|
|
382
468
|
{/* Invisible Interactive Tap Zones for Stepping Scenes */}
|
|
383
|
-
<div className="absolute inset-x-0 top-[60px] bottom-[70px] flex z-
|
|
469
|
+
<div className="absolute inset-x-0 top-[60px] bottom-[70px] flex z-20">
|
|
384
470
|
<Button
|
|
385
471
|
variant="ghost"
|
|
386
472
|
data-testid="btn-prev-scene"
|