ai-design-system 0.1.60 → 0.1.62

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.
@@ -40,7 +40,7 @@ export type ToolProps = ComponentProps<typeof Collapsible>;
40
40
 
41
41
  export const Tool = ({ className, ...props }: ToolProps) => (
42
42
  <Collapsible
43
- className={cn("not-prose mb-4 w-full rounded-md border", className)}
43
+ className={cn("group not-prose mb-4 w-full rounded-md border overflow-hidden", className)}
44
44
  {...props}
45
45
  />
46
46
  );
@@ -267,6 +267,7 @@ export const AIConversation = React.memo<AIConversationProps>(
267
267
  id: subAgent.id,
268
268
  name: subAgent.subAgentName,
269
269
  description: undefined,
270
+ input: subAgent.input,
270
271
  content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
271
272
  status: subAgent.status,
272
273
  toolCalls: [],
@@ -26,7 +26,7 @@ export interface DocumentEditorWithCommentsProps {
26
26
  * Content format - determines how content prop is interpreted
27
27
  * @default 'json'
28
28
  */
29
- format?: 'json' | 'markdown'
29
+ format?: 'json' | 'markdown' | string
30
30
  /** Array of annotations to display */
31
31
  annotations: Annotation[]
32
32
  /** ID of currently selected annotation */
@@ -24,6 +24,7 @@ export interface FileTreeExplorerProps {
24
24
  searchPlaceholder?: string
25
25
  onCreateClick?: () => void
26
26
  createButtonLabel?: string
27
+ onDownloadClick?: () => void
27
28
  headerClassName?: string
28
29
  className?: string
29
30
  }
@@ -84,6 +85,7 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
84
85
  searchPlaceholder = "Filter files...",
85
86
  onCreateClick,
86
87
  createButtonLabel,
88
+ onDownloadClick,
87
89
  headerClassName,
88
90
  className,
89
91
  }) => {
@@ -145,6 +147,16 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
145
147
  {createButtonLabel ?? "New"}
146
148
  </button>
147
149
  ) : null}
150
+ {onDownloadClick ? (
151
+ <button
152
+ type="button"
153
+ onClick={onDownloadClick}
154
+ title="Download all files"
155
+ className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
156
+ >
157
+ <Icon name="download" size="sm" />
158
+ </button>
159
+ ) : null}
148
160
  </div>
149
161
  <div className="p-2">
150
162
  <FileTree
@@ -1,12 +1,10 @@
1
1
  import * as React from "react"
2
2
  import {
3
- Plan,
4
- PlanHeader,
5
- PlanTitle,
6
- PlanDescription,
7
- PlanTrigger,
8
- PlanContent,
9
- } from "@/components/ai-elements/plan"
3
+ Tool,
4
+ ToolHeader,
5
+ ToolContent,
6
+ ToolInput,
7
+ } from "@/components/ai-elements/tool"
10
8
  import { ToolCallDisplay, type ToolCall } from "@/components/composites/ToolCallDisplay"
11
9
  import { cn } from "@/lib/utils"
12
10
  import { Response } from "@/components/composites/response"
@@ -15,7 +13,7 @@ import { Response } from "@/components/composites/response"
15
13
  * SpecialistMessage Block
16
14
  *
17
15
  * A block component for displaying specialist agent messages with optional collapsible content.
18
- * Uses Plan AI element for consistent styling and optional space-efficient display.
16
+ * Uses Tool AI element for consistent styling with tool calls.
19
17
  */
20
18
 
21
19
  export interface SpecialistMessageData {
@@ -28,6 +26,7 @@ export interface SpecialistMessageData {
28
26
  status: "pending" | "active" | "completed" | "error"
29
27
  avatarSrc?: string
30
28
  avatarName?: string
29
+ input?: string | Record<string, unknown>
31
30
  }
32
31
 
33
32
  export interface SpecialistMessageProps {
@@ -56,6 +55,8 @@ export interface SpecialistMessageProps {
56
55
  */
57
56
  export const SpecialistMessage = React.memo<SpecialistMessageProps>(
58
57
  ({ message, isNested = false, defaultOpen = false, collapsible = true }) => {
58
+ const [isOpen, setIsOpen] = React.useState(collapsible ? defaultOpen : true)
59
+
59
60
  const hasContent = React.useMemo(
60
61
  () => message.content && message.content.trim() !== "",
61
62
  [message.content]
@@ -65,53 +66,63 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
65
66
  [message.toolCalls]
66
67
  )
67
68
 
68
- const description = React.useMemo(() => {
69
- if (message.description) return message.description;
70
- if (message.status === "active") return "Thinking...";
71
- return undefined;
72
- }, [message.description, message.status]);
69
+ const toolState = React.useMemo(() => {
70
+ switch (message.status) {
71
+ case "pending":
72
+ return "input-streaming" as const
73
+ case "active":
74
+ return "input-available" as const
75
+ case "completed":
76
+ return "output-available" as const
77
+ case "error":
78
+ return "output-error" as const
79
+ default:
80
+ return "output-available" as const
81
+ }
82
+ }, [message.status])
73
83
 
74
84
  return (
75
- <Plan
76
- defaultOpen={collapsible ? defaultOpen : true}
77
- className={cn(isNested && "ml-8")}
78
- isStreaming={message.status === "active"}
85
+ <Tool
86
+ open={isOpen}
87
+ onOpenChange={collapsible ? setIsOpen : undefined}
79
88
  >
80
- <PlanHeader>
81
- <div>
82
- <div className="mb-4 flex items-center gap-2">
83
- {message.icon && message.icon}
84
- <PlanTitle>{message.name}</PlanTitle>
85
- </div>
86
- {description && (
87
- <PlanDescription>{description}</PlanDescription>
89
+ <ToolHeader
90
+ title={message.name}
91
+ type={`tool-${message.name}` as const}
92
+ state={toolState}
93
+ />
94
+
95
+ <ToolContent>
96
+ <div className="p-4 pt-0 flex flex-col gap-4">
97
+ {message.input && (
98
+ <ToolInput
99
+ input={typeof message.input === 'string' ? { task: message.input } : message.input}
100
+ className="p-0"
101
+ />
88
102
  )}
89
- </div>
90
- {collapsible && <PlanTrigger />}
91
- </PlanHeader>
92
103
 
93
- <PlanContent>
94
- {/* Message content */}
95
- {hasContent && (
96
- <Response
97
- className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
98
- mode={message.status === "active" ? "streaming" : "static"}
99
- isAnimating={message.status === "active"}
100
- >
101
- {message.content}
102
- </Response>
103
- )}
104
+ {/* Message content */}
105
+ {hasContent && (
106
+ <Response
107
+ className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
108
+ mode={message.status === "active" ? "streaming" : "static"}
109
+ isAnimating={message.status === "active"}
110
+ >
111
+ {message.content}
112
+ </Response>
113
+ )}
104
114
 
105
- {/* Tool calls */}
106
- {hasToolCalls && (
107
- <div className={cn("space-y-2", hasContent && "mt-4")}>
108
- {message.toolCalls!.map((toolCall: ToolCall) => (
109
- <ToolCallDisplay key={toolCall.id} toolCall={toolCall} />
110
- ))}
111
- </div>
112
- )}
113
- </PlanContent>
114
- </Plan>
115
+ {/* Tool calls */}
116
+ {hasToolCalls && (
117
+ <div className={cn("space-y-2", hasContent && "mt-4")}>
118
+ {message.toolCalls!.map((toolCall: ToolCall) => (
119
+ <ToolCallDisplay key={toolCall.id} toolCall={toolCall} />
120
+ ))}
121
+ </div>
122
+ )}
123
+ </div>
124
+ </ToolContent>
125
+ </Tool>
115
126
  )
116
127
  }
117
128
  )
@@ -66,7 +66,7 @@ export interface TextEditorSingleProps {
66
66
  *
67
67
  * @default 'json'
68
68
  */
69
- format?: 'json' | 'markdown'
69
+ format?: 'json' | 'markdown' | string
70
70
  /** Array of annotations to display */
71
71
  annotations: Annotation[]
72
72
  /** ID of currently selected annotation */
@@ -105,6 +105,8 @@ export interface TextEditorMultiTabProps {
105
105
  fileTree?: FileTreeNode[]
106
106
  /** When true, hides the document tab bar — useful for single-file mode where the file tree provides navigation. */
107
107
  hideTabBar?: boolean
108
+ /** Callback when download all button is clicked in the file tree header */
109
+ onDownloadAllFiles?: () => void
108
110
  }
109
111
 
110
112
  /**
@@ -185,8 +187,7 @@ export const TextEditor = React.memo<TextEditorProps>(
185
187
  const currentDocument = useMemo(() => {
186
188
  if (isMultiTab && props.documents) {
187
189
  return (
188
- props.documents.find((doc) => doc.file.id === props.activeDocumentId) ||
189
- props.documents[0]
190
+ props.documents.find((doc) => doc.file.id === props.activeDocumentId) || null
190
191
  )
191
192
  }
192
193
  return null
@@ -213,7 +214,7 @@ export const TextEditor = React.memo<TextEditorProps>(
213
214
  )
214
215
  const fileTreeNodes = useMemo(() => {
215
216
  if (!isMultiTab) return []
216
-
217
+
217
218
  const multiProps = props as TextEditorMultiTabProps
218
219
  if (multiProps.fileTree) {
219
220
  return multiProps.fileTree
@@ -262,12 +263,15 @@ export const TextEditor = React.memo<TextEditorProps>(
262
263
  * Single-document mode
263
264
  */
264
265
  if (!isMultiTab) {
265
- const isMarkdown = props.format === 'markdown'
266
- const isJson = props.format === 'json'
267
- if (isMarkdown || isJson) {
268
- const content = isJson
269
- ? `\`\`\`json\n${formatJson(props.content as string)}\n\`\`\``
270
- : (props.content as string)
266
+ const format = props.format || 'markdown'
267
+ const isMarkdown = format === 'markdown' || format === 'md' || format === 'mdx'
268
+ const isCode = !isMarkdown && typeof props.content === 'string'
269
+
270
+ if (isMarkdown || isCode) {
271
+ const contentStr = props.content as string
272
+ const content = isCode
273
+ ? `\`\`\`${format}\n${format === 'json' ? formatJson(contentStr) : contentStr}\n\`\`\``
274
+ : contentStr
271
275
  return (
272
276
  <div className={cn('text-editor p-6 flex flex-col h-screen w-full flex-1', className)}>
273
277
  <StreamingMarkdown mode="streaming">
@@ -297,7 +301,7 @@ export const TextEditor = React.memo<TextEditorProps>(
297
301
  /**
298
302
  * Multi-tab mode
299
303
  */
300
- const { hideTabBar } = props as TextEditorMultiTabProps
304
+ const { hideTabBar, onDownloadAllFiles } = props as TextEditorMultiTabProps
301
305
 
302
306
  let editorPane: React.ReactNode
303
307
 
@@ -314,14 +318,15 @@ export const TextEditor = React.memo<TextEditorProps>(
314
318
  />
315
319
  )}
316
320
  <div className="flex-1 flex items-center justify-center p-6 text-muted-foreground">
317
- No documents open
321
+ No documents selected
318
322
  </div>
319
323
  </div>
320
324
  )
321
325
  } else {
322
- const isMarkdownMulti = currentDocument.file.format === 'markdown'
323
- const isJsonMulti = currentDocument.file.format === 'json'
324
- const renderAsStreamdown = isMarkdownMulti || isJsonMulti
326
+ const format = currentDocument.file.format || 'markdown'
327
+ const isMarkdownMulti = format === 'markdown' || format === 'md' || format === 'mdx'
328
+ const isCodeMulti = !isMarkdownMulti && typeof currentDocument.content === 'string'
329
+ const renderAsStreamdown = isMarkdownMulti || isCodeMulti
325
330
 
326
331
  editorPane = (
327
332
  <div className="text-editor flex flex-col h-full w-full">
@@ -342,8 +347,8 @@ export const TextEditor = React.memo<TextEditorProps>(
342
347
  isAnimating
343
348
  className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
344
349
  >
345
- {isJsonMulti
346
- ? `\`\`\`json\n${formatJson(currentDocument.content as string)}\n\`\`\``
350
+ {isCodeMulti
351
+ ? `\`\`\`${format}\n${format === 'json' ? formatJson(currentDocument.content as string) : currentDocument.content}\n\`\`\``
347
352
  : (currentDocument.content as string)}
348
353
  </StreamingMarkdown>
349
354
  </div>
@@ -384,6 +389,7 @@ export const TextEditor = React.memo<TextEditorProps>(
384
389
  tree={fileTreeNodes}
385
390
  selectedPath={props.activeDocumentId}
386
391
  onSelect={props.onTabSelect}
392
+ onDownloadClick={onDownloadAllFiles}
387
393
  />
388
394
  ),
389
395
  },
@@ -16,7 +16,7 @@ interface DocumentFile {
16
16
  id: string
17
17
  name: string
18
18
  isDirty: boolean
19
- format?: 'json' | 'markdown'
19
+ format?: 'json' | 'markdown' | string
20
20
  lastModified: number
21
21
  }
22
22