ai-design-system 0.1.77 → 0.1.78
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/blocks/AIConversation/AIConversation.stories.tsx +86 -0
- package/components/blocks/AIConversation/AIConversation.tsx +17 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +8 -10
- package/components/composites/AgentIndicator/AgentIndicator.tsx +1 -0
- package/components/composites/MediaPreview/MediaPreview.stories.tsx +60 -0
- package/components/composites/MediaPreview/MediaPreview.tsx +186 -0
- package/components/composites/MediaPreview/index.ts +9 -0
- package/components/composites/SpecialistMessage/SpecialistMessage.tsx +37 -1
- package/components/composites/StreamingMarkdown.tsx +11 -5
- package/components/composites/SystemMessage/SystemMessage.tsx +49 -1
- package/components/composites/index.ts +3 -1
- package/components/features/TextEditor/TextEditor.tsx +25 -23
- package/components/features/TextEditor/index.ts +3 -0
- package/components/features/TextEditor/useTextEditor.mock.ts +1 -14
- package/dist/index.cjs +283 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +38 -0
- package/dist/index.d.ts +45 -25
- package/dist/index.js +283 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -127,6 +127,92 @@ export const MultipleAgents: Story = {
|
|
|
127
127
|
},
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export const NestedSubAgents: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
messages: [
|
|
133
|
+
{
|
|
134
|
+
id: '1',
|
|
135
|
+
type: 'human',
|
|
136
|
+
role: 'user',
|
|
137
|
+
content: 'Generate a 30 second product demo video with motion graphics overlays',
|
|
138
|
+
avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
|
|
139
|
+
avatarName: 'User',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: '2',
|
|
143
|
+
type: 'ai',
|
|
144
|
+
role: 'orchestrator',
|
|
145
|
+
content: '',
|
|
146
|
+
avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
|
|
147
|
+
avatarName: 'Coordinator',
|
|
148
|
+
subAgents: [
|
|
149
|
+
{
|
|
150
|
+
id: 'call_00_soED4oref3iyY11T',
|
|
151
|
+
name: 'task',
|
|
152
|
+
subAgentName: 'media-generator-agent',
|
|
153
|
+
input: 'Generate a 30 second product demo video with motion graphics overlays',
|
|
154
|
+
status: 'active',
|
|
155
|
+
// Nested children — video-sequencer and motion-graphics are both
|
|
156
|
+
// dispatched by media-generator-agent (task() calls), and stream
|
|
157
|
+
// their own tool-call-started/tool-output-delta/tool-finished
|
|
158
|
+
// events. This is what AIConversation must render one level deep.
|
|
159
|
+
subAgents: [
|
|
160
|
+
{
|
|
161
|
+
id: 'call_00_9LyCOvjRGrApV6II',
|
|
162
|
+
name: 'task',
|
|
163
|
+
subAgentName: 'video-sequencer',
|
|
164
|
+
input: 'Sequence the raw clips into a 30s cut with transitions',
|
|
165
|
+
output: 'Sequenced 12 clips into a 30s timeline. Exported sequence.json (30069 bytes).',
|
|
166
|
+
status: 'completed',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'call_00_pQZBX2VUDWwmKQUz',
|
|
170
|
+
name: 'task',
|
|
171
|
+
subAgentName: 'motion-graphics',
|
|
172
|
+
input: 'Add lower-thirds, logo animation, and title card overlays',
|
|
173
|
+
output: 'Rendering title card overlay (2 of 4 assets)...',
|
|
174
|
+
status: 'active',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
blocks: [
|
|
180
|
+
{ type: 'text', id: 'text-1', text: "I'll generate this using two specialized sub-agents." },
|
|
181
|
+
{
|
|
182
|
+
type: 'subAgent',
|
|
183
|
+
id: 'call_00_soED4oref3iyY11T',
|
|
184
|
+
subAgent: {
|
|
185
|
+
id: 'call_00_soED4oref3iyY11T',
|
|
186
|
+
name: 'task',
|
|
187
|
+
subAgentName: 'media-generator-agent',
|
|
188
|
+
input: 'Generate a 30 second product demo video with motion graphics overlays',
|
|
189
|
+
status: 'active',
|
|
190
|
+
subAgents: [
|
|
191
|
+
{
|
|
192
|
+
id: 'call_00_9LyCOvjRGrApV6II',
|
|
193
|
+
name: 'task',
|
|
194
|
+
subAgentName: 'video-sequencer',
|
|
195
|
+
input: 'Sequence the raw clips into a 30s cut with transitions',
|
|
196
|
+
output: 'Sequenced 12 clips into a 30s timeline. Exported sequence.json (30069 bytes).',
|
|
197
|
+
status: 'completed',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'call_00_pQZBX2VUDWwmKQUz',
|
|
201
|
+
name: 'task',
|
|
202
|
+
subAgentName: 'motion-graphics',
|
|
203
|
+
input: 'Add lower-thirds, logo animation, and title card overlays',
|
|
204
|
+
output: 'Rendering title card overlay (2 of 4 assets)...',
|
|
205
|
+
status: 'active',
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
130
216
|
export const Empty: Story = {
|
|
131
217
|
args: {
|
|
132
218
|
messages: [],
|
|
@@ -34,6 +34,21 @@ type AIMessageBlock =
|
|
|
34
34
|
| { type: 'toolCall'; id: string; toolCall: ToolCall }
|
|
35
35
|
| { type: 'subAgent'; id: string; subAgent: SubAgent }
|
|
36
36
|
|
|
37
|
+
// SpecialistMessage's `subAgents` prop expects string-only `output`, while
|
|
38
|
+
// SubAgent allows structured output — normalize recursively when nesting.
|
|
39
|
+
function toNestedSubAgents(
|
|
40
|
+
agents?: SubAgent[]
|
|
41
|
+
): { id: string; subAgentName: string; status: SubAgent['status']; input?: string | Record<string, unknown>; output?: string; subAgents?: ReturnType<typeof toNestedSubAgents> }[] | undefined {
|
|
42
|
+
return agents?.map((a) => ({
|
|
43
|
+
id: a.id,
|
|
44
|
+
subAgentName: a.subAgentName,
|
|
45
|
+
status: a.status,
|
|
46
|
+
input: a.input,
|
|
47
|
+
output: typeof a.output === 'string' ? a.output : a.output ? JSON.stringify(a.output) : undefined,
|
|
48
|
+
subAgents: toNestedSubAgents(a.subAgents),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
interface AIMessage {
|
|
38
53
|
id: string;
|
|
39
54
|
type: 'human' | 'ai';
|
|
@@ -354,6 +369,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
354
369
|
content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
|
|
355
370
|
status: subAgent.status,
|
|
356
371
|
toolCalls: [],
|
|
372
|
+
subAgents: toNestedSubAgents(subAgent.subAgents),
|
|
357
373
|
}}
|
|
358
374
|
isNested={true}
|
|
359
375
|
/>
|
|
@@ -380,6 +396,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
380
396
|
content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
|
|
381
397
|
status: subAgent.status,
|
|
382
398
|
toolCalls: [],
|
|
399
|
+
subAgents: toNestedSubAgents(subAgent.subAgents),
|
|
383
400
|
}}
|
|
384
401
|
isNested={true}
|
|
385
402
|
/>
|
|
@@ -60,21 +60,20 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
60
60
|
|
|
61
61
|
// Compute default sizes (server-safe — no localStorage access)
|
|
62
62
|
const defaultSizes = React.useMemo(() => {
|
|
63
|
-
const raw = sections.map(section => section.defaultSize ?? (100 / sections.length))
|
|
63
|
+
const raw = sections.map((section) => section.defaultSize ?? (100 / sections.length))
|
|
64
64
|
const total = raw.reduce((sum, size) => sum + size, 0)
|
|
65
|
-
return raw.map(size => (size / total) * 100)
|
|
65
|
+
return raw.map((size) => (size / total) * 100)
|
|
66
66
|
}, [sections])
|
|
67
67
|
|
|
68
68
|
const [sizes, setSizes] = React.useState<number[]>(defaultSizes)
|
|
69
69
|
|
|
70
70
|
// Sync sizes if the number of sections changes (e.g. data loads and new section appears)
|
|
71
|
+
const prevSectionsLengthRef = React.useRef(sections.length)
|
|
71
72
|
React.useEffect(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return prevSizes;
|
|
77
|
-
});
|
|
73
|
+
if (prevSectionsLengthRef.current !== sections.length) {
|
|
74
|
+
prevSectionsLengthRef.current = sections.length
|
|
75
|
+
setSizes(defaultSizes)
|
|
76
|
+
}
|
|
78
77
|
}, [sections.length, defaultSizes])
|
|
79
78
|
|
|
80
79
|
// After hydration, overwrite with persisted sizes if available
|
|
@@ -90,8 +89,7 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
90
89
|
} catch {
|
|
91
90
|
// ignore malformed storage
|
|
92
91
|
}
|
|
93
|
-
|
|
94
|
-
}, [storageKey])
|
|
92
|
+
}, [storageKey, sections.length])
|
|
95
93
|
|
|
96
94
|
const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null)
|
|
97
95
|
const [startX, setStartX] = React.useState(0)
|
|
@@ -17,6 +17,7 @@ export interface SubAgent {
|
|
|
17
17
|
input: string | Record<string, unknown>
|
|
18
18
|
output?: string | Record<string, unknown>
|
|
19
19
|
status: "pending" | "active" | "completed" | "error" | "running" | "in_progress"
|
|
20
|
+
subAgents?: SubAgent[]
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface AgentIndicatorProps {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { MediaPreview } from './MediaPreview'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Composites/MediaPreview',
|
|
6
|
+
component: MediaPreview,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
} satisfies Meta<typeof MediaPreview>
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj<typeof meta>
|
|
15
|
+
|
|
16
|
+
export const Video: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
file: {
|
|
19
|
+
id: 'video-1',
|
|
20
|
+
name: 'sample-video.mp4',
|
|
21
|
+
format: 'mp4',
|
|
22
|
+
mediaType: 'video/mp4',
|
|
23
|
+
url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Audio: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
file: {
|
|
31
|
+
id: 'audio-1',
|
|
32
|
+
name: 'sample-audio.mp3',
|
|
33
|
+
format: 'mp3',
|
|
34
|
+
mediaType: 'audio/mp3',
|
|
35
|
+
url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Image: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
file: {
|
|
43
|
+
id: 'image-1',
|
|
44
|
+
name: 'sample-image.png',
|
|
45
|
+
format: 'png',
|
|
46
|
+
mediaType: 'image/png',
|
|
47
|
+
url: 'https://picsum.photos/800/600',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const NoUrl: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
file: {
|
|
55
|
+
id: 'file-1',
|
|
56
|
+
name: 'unknown-media.bin',
|
|
57
|
+
format: 'bin',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaPreview Composite Component
|
|
3
|
+
*
|
|
4
|
+
* Renders native preview players and viewers for binary media files
|
|
5
|
+
* (video, audio, images, documents) within the TextEditor pane.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import type { DocumentFile } from '@/types/ai-editor/editor'
|
|
10
|
+
import { cn } from '@/lib/utils'
|
|
11
|
+
import { Icon } from '@/components/primitives/Icon'
|
|
12
|
+
|
|
13
|
+
export interface MediaFileLike {
|
|
14
|
+
id?: string
|
|
15
|
+
name?: string
|
|
16
|
+
format?: 'json' | 'markdown' | string
|
|
17
|
+
url?: string
|
|
18
|
+
mediaType?: string
|
|
19
|
+
isDirty?: boolean
|
|
20
|
+
lastModified?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MediaPreviewProps {
|
|
24
|
+
file: MediaFileLike
|
|
25
|
+
className?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isVideoFile(file: MediaFileLike): boolean {
|
|
29
|
+
const format = file.format?.toLowerCase() || ''
|
|
30
|
+
const ext = file.name?.split('.').pop()?.toLowerCase() || ''
|
|
31
|
+
if (['mp4', 'webm', 'mov', 'mkv', 'avi'].includes(format) || ['mp4', 'webm', 'mov', 'mkv', 'avi'].includes(ext)) return true
|
|
32
|
+
if (file.mediaType?.startsWith('video/')) return true
|
|
33
|
+
if (file.url && /\.(mp4|webm|mov|mkv)(\?.*)?$/i.test(file.url)) return true
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isAudioFile(file: MediaFileLike): boolean {
|
|
38
|
+
const format = file.format?.toLowerCase() || ''
|
|
39
|
+
const ext = file.name?.split('.').pop()?.toLowerCase() || ''
|
|
40
|
+
if (['wav', 'mp3', 'ogg', 'm4a', 'flac', 'aac', 'wma'].includes(format) || ['wav', 'mp3', 'ogg', 'm4a', 'flac', 'aac', 'wma'].includes(ext)) return true
|
|
41
|
+
if (file.mediaType?.startsWith('audio/')) return true
|
|
42
|
+
if (file.url && /\.(wav|mp3|ogg|m4a|flac|aac)(\?.*)?$/i.test(file.url)) return true
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isImageFile(file: MediaFileLike): boolean {
|
|
47
|
+
const format = file.format?.toLowerCase() || ''
|
|
48
|
+
const ext = file.name?.split('.').pop()?.toLowerCase() || ''
|
|
49
|
+
if (['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg', 'ico', 'bmp'].includes(format) || ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg', 'ico', 'bmp'].includes(ext)) return true
|
|
50
|
+
if (file.mediaType?.startsWith('image/')) return true
|
|
51
|
+
if (file.url && /\.(png|jpg|jpeg|webp|gif|svg)(\?.*)?$/i.test(file.url)) return true
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isPdfFile(file: MediaFileLike): boolean {
|
|
56
|
+
const format = file.format?.toLowerCase() || ''
|
|
57
|
+
const ext = file.name?.split('.').pop()?.toLowerCase() || ''
|
|
58
|
+
return format === 'pdf' || ext === 'pdf' || file.mediaType === 'application/pdf'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isMediaFile(file: MediaFileLike): boolean {
|
|
62
|
+
return isVideoFile(file) || isAudioFile(file) || isImageFile(file) || isPdfFile(file)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const MediaPreview: React.FC<MediaPreviewProps> = ({ file, className }) => {
|
|
66
|
+
const isVideo = isVideoFile(file)
|
|
67
|
+
const isAudio = isAudioFile(file)
|
|
68
|
+
const isImage = isImageFile(file)
|
|
69
|
+
const isPdf = isPdfFile(file)
|
|
70
|
+
|
|
71
|
+
const mediaUrl = file.url
|
|
72
|
+
|
|
73
|
+
if (!mediaUrl) {
|
|
74
|
+
return (
|
|
75
|
+
<div className={cn('flex flex-col items-center justify-center h-full p-8 text-center', className)}>
|
|
76
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-muted/60 mb-4 text-muted-foreground">
|
|
77
|
+
{isVideo && <Icon name="film" className="h-8 w-8" />}
|
|
78
|
+
{isAudio && <Icon name="music" className="h-8 w-8" />}
|
|
79
|
+
{isImage && <Icon name="image" className="h-8 w-8" />}
|
|
80
|
+
{isPdf && <Icon name="file-text" className="h-8 w-8" />}
|
|
81
|
+
{!isVideo && !isAudio && !isImage && !isPdf && <Icon name="alert-circle" className="h-8 w-8" />}
|
|
82
|
+
</div>
|
|
83
|
+
<h3 className="text-base font-medium text-foreground mb-1">{file.name || 'Media File'}</h3>
|
|
84
|
+
<p className="text-sm text-muted-foreground max-w-sm">
|
|
85
|
+
No direct stream URL is available for this file yet.
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={cn('flex flex-col h-full w-full overflow-y-auto p-6 bg-background/50', className)}>
|
|
93
|
+
{/* Header Info */}
|
|
94
|
+
<div className="flex items-center justify-between pb-4 mb-6 border-b border-border">
|
|
95
|
+
<div className="flex items-center gap-3">
|
|
96
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
97
|
+
{isVideo && <Icon name="film" className="h-5 w-5" />}
|
|
98
|
+
{isAudio && <Icon name="music" className="h-5 w-5" />}
|
|
99
|
+
{isImage && <Icon name="image" className="h-5 w-5" />}
|
|
100
|
+
{isPdf && <Icon name="file-text" className="h-5 w-5" />}
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<h2 className="text-sm font-semibold text-foreground tracking-tight">{file.name}</h2>
|
|
104
|
+
<p className="text-xs text-muted-foreground">
|
|
105
|
+
{file.mediaType || file.format?.toUpperCase() || 'Binary Media'}
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="flex items-center gap-2">
|
|
111
|
+
<a
|
|
112
|
+
href={mediaUrl}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
download={file.name}
|
|
116
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border bg-background hover:bg-muted text-foreground transition-colors shadow-sm"
|
|
117
|
+
>
|
|
118
|
+
<Icon name="download" className="h-3.5 w-3.5" />
|
|
119
|
+
Download
|
|
120
|
+
</a>
|
|
121
|
+
<a
|
|
122
|
+
href={mediaUrl}
|
|
123
|
+
target="_blank"
|
|
124
|
+
rel="noopener noreferrer"
|
|
125
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border bg-background hover:bg-muted text-foreground transition-colors shadow-sm"
|
|
126
|
+
>
|
|
127
|
+
<Icon name="external-link" className="h-3.5 w-3.5" />
|
|
128
|
+
Open
|
|
129
|
+
</a>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Main Media Player / Viewer */}
|
|
134
|
+
<div className="flex-1 flex flex-col items-center justify-center min-h-[300px]">
|
|
135
|
+
{isVideo && (
|
|
136
|
+
<div className="w-full max-w-4xl flex flex-col items-center justify-center bg-black/95 rounded-2xl overflow-hidden shadow-lg border border-border/40">
|
|
137
|
+
<video
|
|
138
|
+
controls
|
|
139
|
+
playsInline
|
|
140
|
+
preload="metadata"
|
|
141
|
+
src={mediaUrl}
|
|
142
|
+
className="max-h-[65vh] w-auto max-w-full rounded-2xl"
|
|
143
|
+
>
|
|
144
|
+
Your browser does not support the video tag.
|
|
145
|
+
</video>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{isAudio && (
|
|
150
|
+
<div className="w-full max-w-xl p-8 rounded-2xl border border-border bg-card shadow-sm flex flex-col items-center gap-6">
|
|
151
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
152
|
+
<Icon name="music" className="h-10 w-10 animate-pulse" />
|
|
153
|
+
</div>
|
|
154
|
+
<div className="text-center">
|
|
155
|
+
<h3 className="text-base font-semibold text-foreground mb-1">{file.name}</h3>
|
|
156
|
+
<span className="text-xs text-muted-foreground uppercase tracking-wider">Audio Playback</span>
|
|
157
|
+
</div>
|
|
158
|
+
<audio controls preload="metadata" src={mediaUrl} className="w-full h-12">
|
|
159
|
+
Your browser does not support the audio element.
|
|
160
|
+
</audio>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{isImage && (
|
|
165
|
+
<div className="flex items-center justify-center w-full max-w-4xl rounded-2xl border border-border bg-muted/20 p-4 shadow-sm">
|
|
166
|
+
<img
|
|
167
|
+
src={mediaUrl}
|
|
168
|
+
alt={file.name}
|
|
169
|
+
className="max-h-[68vh] w-auto max-w-full rounded-xl object-contain shadow-md"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{isPdf && (
|
|
175
|
+
<div className="w-full h-full min-h-[500px] rounded-2xl overflow-hidden border border-border shadow-sm">
|
|
176
|
+
<iframe
|
|
177
|
+
src={mediaUrl}
|
|
178
|
+
title={file.name}
|
|
179
|
+
className="w-full h-full min-h-[500px]"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -16,6 +16,16 @@ import { Response } from "@/components/composites/response"
|
|
|
16
16
|
* Uses Tool AI element for consistent styling with tool calls.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// Inline recursive type — avoids circular import with ChatPanel
|
|
20
|
+
type NestedSubAgent = {
|
|
21
|
+
id: string
|
|
22
|
+
subAgentName: string
|
|
23
|
+
status: string
|
|
24
|
+
input?: string | Record<string, unknown>
|
|
25
|
+
output?: string
|
|
26
|
+
subAgents?: NestedSubAgent[]
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export interface SpecialistMessageData {
|
|
20
30
|
id: string
|
|
21
31
|
name: string
|
|
@@ -23,6 +33,7 @@ export interface SpecialistMessageData {
|
|
|
23
33
|
icon?: React.ReactNode
|
|
24
34
|
content: string
|
|
25
35
|
toolCalls?: ToolCall[]
|
|
36
|
+
subAgents?: NestedSubAgent[]
|
|
26
37
|
status: "pending" | "active" | "completed" | "error" | "running" | "in_progress"
|
|
27
38
|
avatarSrc?: string
|
|
28
39
|
avatarName?: string
|
|
@@ -65,6 +76,10 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
|
|
|
65
76
|
() => message.toolCalls !== undefined && message.toolCalls.length > 0,
|
|
66
77
|
[message.toolCalls]
|
|
67
78
|
)
|
|
79
|
+
const hasSubAgents = React.useMemo(
|
|
80
|
+
() => message.subAgents !== undefined && message.subAgents.length > 0,
|
|
81
|
+
[message.subAgents]
|
|
82
|
+
)
|
|
68
83
|
|
|
69
84
|
const toolState = React.useMemo(() => {
|
|
70
85
|
switch (message.status) {
|
|
@@ -112,7 +127,7 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
|
|
|
112
127
|
</Response>
|
|
113
128
|
)}
|
|
114
129
|
|
|
115
|
-
{/*
|
|
130
|
+
{/* Leaf tool calls (read_file, write_file, etc.) */}
|
|
116
131
|
{hasToolCalls && (
|
|
117
132
|
<div className={cn("space-y-2", hasContent && "mt-4")}>
|
|
118
133
|
{message.toolCalls!.map((toolCall: ToolCall) => (
|
|
@@ -120,6 +135,27 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
|
|
|
120
135
|
))}
|
|
121
136
|
</div>
|
|
122
137
|
)}
|
|
138
|
+
|
|
139
|
+
{/* Nested subagent cards (video-sequencer, motion-graphics, etc.)
|
|
140
|
+
Indented with a left border to visually express hierarchy. */}
|
|
141
|
+
{hasSubAgents && (
|
|
142
|
+
<div className={cn("space-y-2 pl-3 border-l border-border/50", (hasContent || hasToolCalls) && "mt-4")}>
|
|
143
|
+
{message.subAgents!.map((sa) => (
|
|
144
|
+
<SpecialistMessage
|
|
145
|
+
key={sa.id}
|
|
146
|
+
isNested={true}
|
|
147
|
+
message={{
|
|
148
|
+
id: sa.id,
|
|
149
|
+
name: sa.subAgentName,
|
|
150
|
+
content: typeof sa.output === 'string' ? sa.output : '',
|
|
151
|
+
status: sa.status as SpecialistMessageData['status'],
|
|
152
|
+
input: sa.input,
|
|
153
|
+
subAgents: sa.subAgents,
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
123
159
|
</div>
|
|
124
160
|
</ToolContent>
|
|
125
161
|
</Tool>
|
|
@@ -14,6 +14,15 @@ export type StreamingMarkdownProps = ComponentProps<typeof Streamdown> & {
|
|
|
14
14
|
type?: string;
|
|
15
15
|
status?: string;
|
|
16
16
|
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SHIKI_THEME: NonNullable<StreamingMarkdownProps["shikiTheme"]> = [
|
|
19
|
+
"vitesse-light",
|
|
20
|
+
"vitesse-dark",
|
|
21
|
+
];
|
|
22
|
+
const DEFAULT_CONTROLS = {
|
|
23
|
+
code: true,
|
|
24
|
+
mermaid: true,
|
|
25
|
+
};
|
|
17
26
|
/**
|
|
18
27
|
* A composite component that wraps Streamdown with all configured plugins.
|
|
19
28
|
* Features should import this component instead of importing Streamdown directly.
|
|
@@ -142,11 +151,8 @@ export const StreamingMarkdown = memo(
|
|
|
142
151
|
)}
|
|
143
152
|
<Streamdown
|
|
144
153
|
remarkPlugins={remarkPlugins || customRemarkPlugins}
|
|
145
|
-
shikiTheme={
|
|
146
|
-
controls={propsControls ??
|
|
147
|
-
code: true,
|
|
148
|
-
mermaid: true,
|
|
149
|
-
}}
|
|
154
|
+
shikiTheme={props.shikiTheme ?? DEFAULT_SHIKI_THEME}
|
|
155
|
+
controls={propsControls ?? DEFAULT_CONTROLS}
|
|
150
156
|
{...props}
|
|
151
157
|
>
|
|
152
158
|
{content}
|
|
@@ -24,13 +24,61 @@ export interface SystemMessageProps {
|
|
|
24
24
|
renderContent?: (content: string) => React.ReactNode
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function parseSystemMessageContent(content: string): React.ReactNode {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(content)
|
|
30
|
+
if (parsed && typeof parsed === 'object') {
|
|
31
|
+
const url = typeof parsed.url === 'string' ? parsed.url : undefined
|
|
32
|
+
const mediaType = typeof parsed.mediaType === 'string' ? parsed.mediaType : typeof parsed.type === 'string' ? parsed.type : undefined
|
|
33
|
+
const title = typeof parsed.title === 'string' ? parsed.title : typeof parsed.message === 'string' ? parsed.message : undefined
|
|
34
|
+
|
|
35
|
+
if (url) {
|
|
36
|
+
if (mediaType?.startsWith('video/') || /\.(mp4|webm|mov)(\?.*)?$/i.test(url)) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex flex-col items-center gap-2">
|
|
39
|
+
{title && <span className="text-xs text-muted-foreground">{title}</span>}
|
|
40
|
+
<video controls className="w-full max-w-[360px] rounded-md shadow-sm">
|
|
41
|
+
<source src={url} type={mediaType || 'video/mp4'} />
|
|
42
|
+
Your browser does not support the video element.
|
|
43
|
+
</video>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
if (mediaType?.startsWith('audio/') || /\.(wav|mp3|ogg|m4a)(\?.*)?$/i.test(url)) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col items-center gap-2">
|
|
50
|
+
{title && <span className="text-xs text-muted-foreground">{title}</span>}
|
|
51
|
+
<audio controls className="w-full max-w-[360px] h-8">
|
|
52
|
+
<source src={url} type={mediaType || 'audio/wav'} />
|
|
53
|
+
Your browser does not support the audio element.
|
|
54
|
+
</audio>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
if (mediaType?.startsWith('image/') || /\.(png|jpg|jpeg|webp|gif)(\?.*)?$/i.test(url)) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex flex-col items-center gap-2">
|
|
61
|
+
{title && <span className="text-xs text-muted-foreground">{title}</span>}
|
|
62
|
+
<img src={url} alt={title || 'Image output'} className="max-w-[360px] rounded-md shadow-sm" />
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (title) return <span>{title}</span>
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Plain text content
|
|
71
|
+
}
|
|
72
|
+
return <span>{content}</span>
|
|
73
|
+
}
|
|
74
|
+
|
|
27
75
|
export const SystemMessage = React.memo<SystemMessageProps>(
|
|
28
76
|
({ message, renderContent }) => {
|
|
29
77
|
if (!message.content || !message.content.trim()) {
|
|
30
78
|
return null;
|
|
31
79
|
}
|
|
32
80
|
|
|
33
|
-
const contentNode = renderContent ? renderContent(message.content) :
|
|
81
|
+
const contentNode = renderContent ? renderContent(message.content) : parseSystemMessageContent(message.content);
|
|
34
82
|
if (!contentNode) {
|
|
35
83
|
return null;
|
|
36
84
|
}
|
|
@@ -230,4 +230,6 @@ export type { IconButtonProps } from './IconButton'
|
|
|
230
230
|
export { AuthCard } from './AuthCard'
|
|
231
231
|
export type { AuthCardProps, AuthMode, SocialProvider } from './AuthCard'
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
// MediaPreview Composite
|
|
234
|
+
export { MediaPreview, isVideoFile, isAudioFile, isImageFile, isPdfFile, isMediaFile } from './MediaPreview'
|
|
235
|
+
export type { MediaPreviewProps, MediaFileLike } from './MediaPreview'
|