ai-design-system 0.1.102 → 0.1.104
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.stories.tsx +23 -0
- package/components/composites/PromptInput/PromptInput.tsx +49 -4
- package/components/composites/PromptInput/index.ts +1 -1
- package/components/composites/ScenePlayer/ScenePlayer.stories.tsx +2 -0
- package/components/composites/ScenePlayer/ScenePlayer.tsx +136 -27
- package/components/composites/index.ts +1 -0
- package/components/features/ChatPanel/ChatPanel.tsx +17 -1
- package/components/index.ts +1 -0
- package/dist/index.cjs +215 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +9 -6
- package/dist/index.d.ts +114 -91
- package/dist/index.js +215 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -360,5 +360,28 @@ export const WithSpeechInput: Story = {
|
|
|
360
360
|
},
|
|
361
361
|
};
|
|
362
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Row / Inline Variant
|
|
365
|
+
* Single row where the input box and send button are side-by-side, without tools icons.
|
|
366
|
+
*/
|
|
367
|
+
export const RowVariant: Story = {
|
|
368
|
+
args: {
|
|
369
|
+
variant: "row",
|
|
370
|
+
placeholder: "Ask a question or provide instructions...",
|
|
371
|
+
onSubmit: (message) => {
|
|
372
|
+
console.log("Submitted in row mode:", message);
|
|
373
|
+
alert(`Submitted: ${message.text}`);
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
parameters: {
|
|
377
|
+
docs: {
|
|
378
|
+
description: {
|
|
379
|
+
story:
|
|
380
|
+
"Compact single-row PromptInput variant where input box and send button are in the same line without toolbar tool icons (attachments, speech mic, etc.).",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
363
386
|
|
|
364
387
|
|
|
@@ -44,11 +44,14 @@ export interface PromptInputContextProps {
|
|
|
44
44
|
modelId?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export type PromptInputVariant = "default" | "row" | "inline";
|
|
48
|
+
|
|
47
49
|
export interface PromptInputBlockProps
|
|
48
50
|
extends Omit<
|
|
49
51
|
AIPromptInputProps,
|
|
50
52
|
"globalDrop" | "syncHiddenInput" | "onSubmit" | "onChange"
|
|
51
53
|
> {
|
|
54
|
+
variant?: PromptInputVariant;
|
|
52
55
|
disabled?: boolean;
|
|
53
56
|
placeholder?: string;
|
|
54
57
|
value?: string;
|
|
@@ -62,6 +65,7 @@ export interface PromptInputBlockProps
|
|
|
62
65
|
onStop?: () => void;
|
|
63
66
|
context?: PromptInputContextProps | ReactNode;
|
|
64
67
|
tools?: ReactNode;
|
|
68
|
+
enableAttachments?: boolean;
|
|
65
69
|
enableSpeech?: boolean;
|
|
66
70
|
speechProps?: SpeechInputProps;
|
|
67
71
|
attachIcon?: "paperclip" | "plus";
|
|
@@ -69,6 +73,7 @@ export interface PromptInputBlockProps
|
|
|
69
73
|
|
|
70
74
|
export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
71
75
|
({
|
|
76
|
+
variant = "default",
|
|
72
77
|
disabled = false,
|
|
73
78
|
placeholder,
|
|
74
79
|
value,
|
|
@@ -79,7 +84,8 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
79
84
|
onStop,
|
|
80
85
|
context,
|
|
81
86
|
tools,
|
|
82
|
-
|
|
87
|
+
enableAttachments,
|
|
88
|
+
enableSpeech,
|
|
83
89
|
speechProps,
|
|
84
90
|
attachIcon = "paperclip",
|
|
85
91
|
accept = "image/*,audio/*",
|
|
@@ -89,6 +95,10 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
89
95
|
onError,
|
|
90
96
|
...props
|
|
91
97
|
}) => {
|
|
98
|
+
const isRowVariant = variant === "row" || variant === "inline";
|
|
99
|
+
const showAttachments = enableAttachments ?? !isRowVariant;
|
|
100
|
+
const showSpeech = enableSpeech ?? !isRowVariant;
|
|
101
|
+
|
|
92
102
|
const existingController = useOptionalPromptInputController();
|
|
93
103
|
|
|
94
104
|
const handleSubmit = React.useCallback(
|
|
@@ -122,7 +132,40 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
122
132
|
|
|
123
133
|
const isStopping = loading && Boolean(onStop);
|
|
124
134
|
|
|
125
|
-
const promptInputContent = (
|
|
135
|
+
const promptInputContent = isRowVariant ? (
|
|
136
|
+
<AIPromptInput
|
|
137
|
+
onSubmit={handleSubmit}
|
|
138
|
+
accept={accept}
|
|
139
|
+
multiple={multiple}
|
|
140
|
+
maxFiles={maxFiles}
|
|
141
|
+
maxFileSize={maxFileSize}
|
|
142
|
+
onError={onError}
|
|
143
|
+
{...props}
|
|
144
|
+
>
|
|
145
|
+
<PromptInputBody className="contents">
|
|
146
|
+
{showAttachments && <AttachmentPreviews />}
|
|
147
|
+
<PromptInputTextarea
|
|
148
|
+
placeholder={placeholder}
|
|
149
|
+
disabled={disabled}
|
|
150
|
+
onChange={isControlled ? handleControlledChange : undefined}
|
|
151
|
+
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"
|
|
152
|
+
/>
|
|
153
|
+
</PromptInputBody>
|
|
154
|
+
<div className="pr-1.5 shrink-0 flex items-center self-center">
|
|
155
|
+
<PromptInputSubmit
|
|
156
|
+
disabled={disabled || (loading && !onStop)}
|
|
157
|
+
status={loading ? (onStop ? "streaming" : "submitted") : undefined}
|
|
158
|
+
onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
|
|
159
|
+
className={cn(
|
|
160
|
+
"h-8 w-8 rounded-lg bg-blue-600 hover:bg-blue-700 text-white transition-colors shrink-0 p-0 flex items-center justify-center cursor-pointer",
|
|
161
|
+
isStopping && "bg-transparent hover:bg-accent"
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{loading ? undefined : <Icon name="corner-down-left" size="sm" />}
|
|
165
|
+
</PromptInputSubmit>
|
|
166
|
+
</div>
|
|
167
|
+
</AIPromptInput>
|
|
168
|
+
) : (
|
|
126
169
|
<AIPromptInput
|
|
127
170
|
onSubmit={handleSubmit}
|
|
128
171
|
accept={accept}
|
|
@@ -142,8 +185,10 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
142
185
|
</PromptInputBody>
|
|
143
186
|
<PromptInputFooter>
|
|
144
187
|
<PromptInputTools>
|
|
145
|
-
|
|
146
|
-
|
|
188
|
+
{showAttachments && (
|
|
189
|
+
<AttachButton disabled={disabled || loading} icon={attachIcon} />
|
|
190
|
+
)}
|
|
191
|
+
{showSpeech && (
|
|
147
192
|
<SpeechInput
|
|
148
193
|
disabled={disabled || loading}
|
|
149
194
|
{...speechProps}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PromptInput } from "./PromptInput";
|
|
2
|
-
export type { PromptInputBlockProps, PromptInputContextProps } from "./PromptInput";
|
|
2
|
+
export type { PromptInputBlockProps, PromptInputContextProps, PromptInputVariant } from "./PromptInput";
|
|
3
3
|
|
|
4
4
|
export { SpeechInput } from "@/components/ai-elements/speech-input";
|
|
5
5
|
export type { SpeechInputProps } from "@/components/ai-elements/speech-input";
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import type { FormEvent } from "react";
|
|
5
5
|
import { Button } from "@/components/primitives/Button";
|
|
6
6
|
import { Icon } from "@/components/primitives/Icon";
|
|
7
|
-
import { PromptInput } from "@/components/composites/PromptInput";
|
|
7
|
+
import { PromptInput, type PromptInputVariant } from "@/components/composites/PromptInput";
|
|
8
8
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
|
|
@@ -28,6 +28,11 @@ export interface ScenePlayerProps extends Omit<React.HTMLAttributes<HTMLDivEleme
|
|
|
28
28
|
avatarColor?: string;
|
|
29
29
|
initialSceneIndex?: number;
|
|
30
30
|
isMuted?: boolean;
|
|
31
|
+
isPlaying?: boolean;
|
|
32
|
+
onPlayingChange?: (isPlaying: boolean) => void;
|
|
33
|
+
videoUrl?: string;
|
|
34
|
+
downloadUrl?: string;
|
|
35
|
+
onDownload?: () => void;
|
|
31
36
|
onBack?: () => void;
|
|
32
37
|
onClose?: () => void;
|
|
33
38
|
placeholder?: string;
|
|
@@ -40,6 +45,8 @@ export interface ScenePlayerProps extends Omit<React.HTMLAttributes<HTMLDivEleme
|
|
|
40
45
|
loading?: boolean;
|
|
41
46
|
onStop?: () => void;
|
|
42
47
|
enableSpeech?: boolean;
|
|
48
|
+
enableAttachments?: boolean;
|
|
49
|
+
promptInputVariant?: PromptInputVariant;
|
|
43
50
|
onPublish?: (currentScene: SceneItem, text?: string) => void;
|
|
44
51
|
publishPlaceholder?: string;
|
|
45
52
|
onSceneChange?: (sceneIndex: number, scene: SceneItem) => void;
|
|
@@ -54,6 +61,11 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
54
61
|
avatarColor = "bg-indigo-600",
|
|
55
62
|
initialSceneIndex = 0,
|
|
56
63
|
isMuted: initialMuted = false,
|
|
64
|
+
isPlaying: isPlayingProp,
|
|
65
|
+
onPlayingChange,
|
|
66
|
+
videoUrl,
|
|
67
|
+
downloadUrl,
|
|
68
|
+
onDownload,
|
|
57
69
|
onBack,
|
|
58
70
|
onClose,
|
|
59
71
|
placeholder,
|
|
@@ -62,7 +74,9 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
62
74
|
onSubmit,
|
|
63
75
|
loading = false,
|
|
64
76
|
onStop,
|
|
65
|
-
enableSpeech =
|
|
77
|
+
enableSpeech = false,
|
|
78
|
+
enableAttachments = false,
|
|
79
|
+
promptInputVariant = "row",
|
|
66
80
|
onPublish,
|
|
67
81
|
publishPlaceholder = "Ask a question or provide instructions...",
|
|
68
82
|
onSceneChange,
|
|
@@ -80,6 +94,22 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
80
94
|
return 0;
|
|
81
95
|
});
|
|
82
96
|
|
|
97
|
+
const [isPlayingState, setIsPlayingState] = React.useState<boolean>(isPlayingProp ?? true);
|
|
98
|
+
const isPlaying = isPlayingProp !== undefined ? isPlayingProp : isPlayingState;
|
|
99
|
+
|
|
100
|
+
const setIsPlaying = React.useCallback(
|
|
101
|
+
(value: boolean | ((prev: boolean) => boolean)) => {
|
|
102
|
+
const nextVal = typeof value === "function" ? value(isPlaying) : value;
|
|
103
|
+
setIsPlayingState(nextVal);
|
|
104
|
+
onPlayingChange?.(nextVal);
|
|
105
|
+
},
|
|
106
|
+
[isPlaying, onPlayingChange]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const togglePlay = React.useCallback(() => {
|
|
110
|
+
setIsPlaying((prev) => !prev);
|
|
111
|
+
}, [setIsPlaying]);
|
|
112
|
+
|
|
83
113
|
const [isMuted, setIsMuted] = React.useState<boolean>(initialMuted);
|
|
84
114
|
const videoRef = React.useRef<HTMLVideoElement | null>(null);
|
|
85
115
|
|
|
@@ -117,41 +147,91 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
117
147
|
}, [scenes, onSceneChange]);
|
|
118
148
|
|
|
119
149
|
const handleSceneEnded = React.useCallback(() => {
|
|
120
|
-
if (!scenes.length) return;
|
|
150
|
+
if (!scenes.length || !isPlaying) return;
|
|
121
151
|
setCurrentSceneIndex((prev) => {
|
|
122
152
|
const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
|
|
123
153
|
onSceneChange?.(nextIdx, scenes[nextIdx]);
|
|
124
154
|
return nextIdx;
|
|
125
155
|
});
|
|
126
|
-
}, [scenes, onSceneChange]);
|
|
156
|
+
}, [scenes, onSceneChange, isPlaying]);
|
|
157
|
+
|
|
158
|
+
const handleDownload = React.useCallback(async () => {
|
|
159
|
+
if (onDownload) {
|
|
160
|
+
onDownload();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const targetUrl = downloadUrl || videoUrl || activeScene?.scene_clip_url;
|
|
164
|
+
if (!targetUrl) return;
|
|
127
165
|
|
|
128
|
-
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(targetUrl);
|
|
168
|
+
if (!response.ok) throw new Error("Fetch failed");
|
|
169
|
+
const blob = await response.blob();
|
|
170
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
171
|
+
const link = document.createElement("a");
|
|
172
|
+
link.href = blobUrl;
|
|
173
|
+
link.download = targetUrl.split("/").pop()?.split("?")[0] || "video.mp4";
|
|
174
|
+
document.body.appendChild(link);
|
|
175
|
+
link.click();
|
|
176
|
+
document.body.removeChild(link);
|
|
177
|
+
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
178
|
+
} catch {
|
|
179
|
+
const link = document.createElement("a");
|
|
180
|
+
link.href = targetUrl;
|
|
181
|
+
link.download = targetUrl.split("/").pop()?.split("?")[0] || "video.mp4";
|
|
182
|
+
link.target = "_blank";
|
|
183
|
+
link.rel = "noopener noreferrer";
|
|
184
|
+
document.body.appendChild(link);
|
|
185
|
+
link.click();
|
|
186
|
+
document.body.removeChild(link);
|
|
187
|
+
}
|
|
188
|
+
}, [onDownload, downloadUrl, videoUrl, activeScene?.scene_clip_url]);
|
|
189
|
+
|
|
190
|
+
// Playback & mute synchronization
|
|
129
191
|
React.useEffect(() => {
|
|
130
192
|
const video = videoRef.current;
|
|
131
193
|
if (!video) return;
|
|
132
194
|
|
|
133
195
|
video.muted = isMuted;
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
playPromise.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
196
|
+
|
|
197
|
+
if (isPlaying) {
|
|
198
|
+
const playPromise = video.play();
|
|
199
|
+
if (playPromise !== undefined) {
|
|
200
|
+
playPromise.catch(() => {
|
|
201
|
+
if (!isMuted) {
|
|
202
|
+
video.muted = true;
|
|
203
|
+
setIsMuted(true);
|
|
204
|
+
video.play().catch(() => {});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
video.pause();
|
|
143
210
|
}
|
|
144
|
-
}, [activeScene?.scene_clip_url, isMuted]);
|
|
211
|
+
}, [activeScene?.scene_clip_url, isMuted, isPlaying]);
|
|
145
212
|
|
|
146
|
-
// Keyboard navigation
|
|
213
|
+
// Keyboard navigation & playback controls
|
|
147
214
|
React.useEffect(() => {
|
|
148
215
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
216
|
+
const target = e.target as HTMLElement | null;
|
|
217
|
+
if (
|
|
218
|
+
target &&
|
|
219
|
+
(target.tagName === "INPUT" ||
|
|
220
|
+
target.tagName === "TEXTAREA" ||
|
|
221
|
+
target.isContentEditable)
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
149
226
|
if (e.key === "ArrowLeft") {
|
|
150
227
|
e.preventDefault();
|
|
151
228
|
handlePrev();
|
|
152
|
-
} else if (e.key === "ArrowRight"
|
|
229
|
+
} else if (e.key === "ArrowRight") {
|
|
153
230
|
e.preventDefault();
|
|
154
231
|
handleNext();
|
|
232
|
+
} else if (e.key === " " || e.key === "k" || e.key === "K") {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
togglePlay();
|
|
155
235
|
} else if (e.key === "Escape") {
|
|
156
236
|
e.preventDefault();
|
|
157
237
|
handleBack?.();
|
|
@@ -163,7 +243,7 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
163
243
|
|
|
164
244
|
window.addEventListener("keydown", handleKeyDown);
|
|
165
245
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
166
|
-
}, [handlePrev, handleNext, handleBack]);
|
|
246
|
+
}, [handlePrev, handleNext, handleBack, togglePlay]);
|
|
167
247
|
|
|
168
248
|
if (!scenes || scenes.length === 0) {
|
|
169
249
|
return (
|
|
@@ -236,14 +316,6 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
236
316
|
</Button>
|
|
237
317
|
)}
|
|
238
318
|
|
|
239
|
-
<div
|
|
240
|
-
className={cn(
|
|
241
|
-
"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",
|
|
242
|
-
avatarColor
|
|
243
|
-
)}
|
|
244
|
-
>
|
|
245
|
-
{avatarInitials}
|
|
246
|
-
</div>
|
|
247
319
|
<div className="flex flex-col min-w-0 justify-center">
|
|
248
320
|
<span className="font-bold text-sm text-white truncate max-w-[200px]">
|
|
249
321
|
{title}
|
|
@@ -255,6 +327,18 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
255
327
|
</div>
|
|
256
328
|
|
|
257
329
|
<div className="flex items-center gap-2 shrink-0">
|
|
330
|
+
{/* Play/Pause Toggle Button */}
|
|
331
|
+
<Button
|
|
332
|
+
variant="ghost"
|
|
333
|
+
size="icon"
|
|
334
|
+
data-testid="btn-toggle-play"
|
|
335
|
+
aria-label={isPlaying ? "Pause scene" : "Play scene"}
|
|
336
|
+
onClick={togglePlay}
|
|
337
|
+
className="h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer"
|
|
338
|
+
>
|
|
339
|
+
<Icon name={isPlaying ? "pause" : "play"} className="w-4 h-4" />
|
|
340
|
+
</Button>
|
|
341
|
+
|
|
258
342
|
{/* Mute/Unmute Toggle Button */}
|
|
259
343
|
<Button
|
|
260
344
|
variant="ghost"
|
|
@@ -266,6 +350,18 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
266
350
|
>
|
|
267
351
|
<Icon name={isMuted ? "volume-x" : "volume-2"} className="w-4 h-4" />
|
|
268
352
|
</Button>
|
|
353
|
+
|
|
354
|
+
{/* Download Video Button */}
|
|
355
|
+
<Button
|
|
356
|
+
variant="ghost"
|
|
357
|
+
size="icon"
|
|
358
|
+
data-testid="btn-download"
|
|
359
|
+
aria-label="Download video"
|
|
360
|
+
onClick={handleDownload}
|
|
361
|
+
className="h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer"
|
|
362
|
+
>
|
|
363
|
+
<Icon name="download" className="w-4 h-4" />
|
|
364
|
+
</Button>
|
|
269
365
|
</div>
|
|
270
366
|
</div>
|
|
271
367
|
|
|
@@ -303,8 +399,20 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
303
399
|
</div>
|
|
304
400
|
|
|
305
401
|
{/* Bottom PromptInput Composite from Design System */}
|
|
306
|
-
<div
|
|
402
|
+
<div
|
|
403
|
+
className="p-3 bg-gradient-to-t from-black via-black/90 to-transparent pt-4 z-20 shrink-0"
|
|
404
|
+
onFocusCapture={() => {
|
|
405
|
+
setIsPlaying(false);
|
|
406
|
+
}}
|
|
407
|
+
onClickCapture={() => {
|
|
408
|
+
setIsPlaying(false);
|
|
409
|
+
}}
|
|
410
|
+
onTouchStartCapture={() => {
|
|
411
|
+
setIsPlaying(false);
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
307
414
|
<PromptInput
|
|
415
|
+
variant={promptInputVariant}
|
|
308
416
|
placeholder={placeholder || publishPlaceholder}
|
|
309
417
|
value={promptValue}
|
|
310
418
|
onChange={onPromptValueChange}
|
|
@@ -312,6 +420,7 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
312
420
|
loading={loading}
|
|
313
421
|
onStop={onStop}
|
|
314
422
|
enableSpeech={enableSpeech}
|
|
423
|
+
enableAttachments={enableAttachments}
|
|
315
424
|
className="border border-border bg-background shadow-md overflow-hidden rounded-2xl"
|
|
316
425
|
/>
|
|
317
426
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { AIConversation } from "@/components/blocks/AIConversation";
|
|
5
5
|
import { FileChangeQueue } from "@/components/blocks/FileChangeQueue";
|
|
6
|
-
import { PromptInput, type PromptInputContextProps } from "@/components/composites/PromptInput";
|
|
6
|
+
import { PromptInput, type PromptInputContextProps, type PromptInputVariant } from "@/components/composites/PromptInput";
|
|
7
7
|
import { TaskQueue, type TaskItem } from "@/components/composites/TaskQueue";
|
|
8
8
|
import { ApprovalCard } from "@/components/composites/ApprovalCard";
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
@@ -194,6 +194,18 @@ export interface ChatPanelProps {
|
|
|
194
194
|
* Handler when a task item is clicked (e.g. to inspect terminal output or logs)
|
|
195
195
|
*/
|
|
196
196
|
onTaskClick?: (task: TaskItem) => void;
|
|
197
|
+
/**
|
|
198
|
+
* Layout variant for PromptInput:
|
|
199
|
+
* - "default": multi-line textarea with toolbar buttons below (attachment, mic)
|
|
200
|
+
* - "row" | "inline": single row where input box and send button are side-by-side without tools icons
|
|
201
|
+
* @default "default"
|
|
202
|
+
*/
|
|
203
|
+
promptInputVariant?: PromptInputVariant;
|
|
204
|
+
/**
|
|
205
|
+
* Whether to enable the attachment button in prompt
|
|
206
|
+
* @default true for default variant, false for row variant
|
|
207
|
+
*/
|
|
208
|
+
enableAttachments?: boolean;
|
|
197
209
|
/**
|
|
198
210
|
* Whether to enable the voice-note microphone button in prompt
|
|
199
211
|
* @default true
|
|
@@ -268,6 +280,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
268
280
|
onTaskRetry,
|
|
269
281
|
onTaskStopAll,
|
|
270
282
|
onTaskClick,
|
|
283
|
+
promptInputVariant = "default",
|
|
284
|
+
enableAttachments,
|
|
271
285
|
enableSpeech = true,
|
|
272
286
|
showHeader = true,
|
|
273
287
|
sessionTitle,
|
|
@@ -465,6 +479,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
465
479
|
/>
|
|
466
480
|
)}
|
|
467
481
|
<PromptInput
|
|
482
|
+
variant={promptInputVariant}
|
|
468
483
|
dialog={dialog}
|
|
469
484
|
placeholder={placeholder}
|
|
470
485
|
value={promptValue}
|
|
@@ -474,6 +489,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
474
489
|
onStop={onStop}
|
|
475
490
|
context={context}
|
|
476
491
|
enableSpeech={enableSpeech}
|
|
492
|
+
enableAttachments={enableAttachments}
|
|
477
493
|
className={cn(
|
|
478
494
|
"border border-neutral-600 bg-background shadow-sm overflow-hidden",
|
|
479
495
|
tasks.length > 0 && !dialog
|