ai-design-system 0.1.61 → 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: [],
@@ -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
  )
@@ -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
@@ -263,9 +264,9 @@ export const TextEditor = React.memo<TextEditorProps>(
263
264
  */
264
265
  if (!isMultiTab) {
265
266
  const format = props.format || 'markdown'
266
- const isMarkdown = format === 'markdown'
267
+ const isMarkdown = format === 'markdown' || format === 'md' || format === 'mdx'
267
268
  const isCode = !isMarkdown && typeof props.content === 'string'
268
-
269
+
269
270
  if (isMarkdown || isCode) {
270
271
  const contentStr = props.content as string
271
272
  const content = isCode
@@ -300,7 +301,7 @@ export const TextEditor = React.memo<TextEditorProps>(
300
301
  /**
301
302
  * Multi-tab mode
302
303
  */
303
- const { hideTabBar } = props as TextEditorMultiTabProps
304
+ const { hideTabBar, onDownloadAllFiles } = props as TextEditorMultiTabProps
304
305
 
305
306
  let editorPane: React.ReactNode
306
307
 
@@ -317,13 +318,13 @@ export const TextEditor = React.memo<TextEditorProps>(
317
318
  />
318
319
  )}
319
320
  <div className="flex-1 flex items-center justify-center p-6 text-muted-foreground">
320
- No documents open
321
+ No documents selected
321
322
  </div>
322
323
  </div>
323
324
  )
324
325
  } else {
325
326
  const format = currentDocument.file.format || 'markdown'
326
- const isMarkdownMulti = format === 'markdown'
327
+ const isMarkdownMulti = format === 'markdown' || format === 'md' || format === 'mdx'
327
328
  const isCodeMulti = !isMarkdownMulti && typeof currentDocument.content === 'string'
328
329
  const renderAsStreamdown = isMarkdownMulti || isCodeMulti
329
330
 
@@ -388,6 +389,7 @@ export const TextEditor = React.memo<TextEditorProps>(
388
389
  tree={fileTreeNodes}
389
390
  selectedPath={props.activeDocumentId}
390
391
  onSelect={props.onTabSelect}
392
+ onDownloadClick={onDownloadAllFiles}
391
393
  />
392
394
  ),
393
395
  },