ai-design-system 0.1.45 → 0.1.46

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.
@@ -110,12 +110,12 @@ export const CodeBlock = ({
110
110
  >
111
111
  <div className="relative">
112
112
  <div
113
- className="overflow-hidden dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
113
+ className="overflow-x-auto overflow-y-hidden dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
114
114
  // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
115
115
  dangerouslySetInnerHTML={{ __html: html }}
116
116
  />
117
117
  <div
118
- className="hidden overflow-hidden dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
118
+ className="hidden overflow-x-auto overflow-y-hidden dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
119
119
  // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
120
120
  dangerouslySetInnerHTML={{ __html: darkHtml }}
121
121
  />
@@ -16,7 +16,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
16
16
  <div
17
17
  className={cn(
18
18
  "group flex w-full items-start gap-2 py-2",
19
- from === "user" ? "is-user flex-row-reverse" : "is-assistant bg-muted/50 -mx-4 px-4",
19
+ from === "user" ? "is-user flex-row-reverse" : "is-assistant",
20
20
  className
21
21
  )}
22
22
  {...props}
@@ -24,7 +24,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
24
24
  );
25
25
 
26
26
  const messageContentVariants = cva(
27
- "is-user:dark flex flex-col gap-2 overflow-hidden rounded-lg text-sm group-[.is-user]:ml-auto",
27
+ "is-user:dark flex flex-col gap-2 overflow-hidden rounded-lg text-sm group-[.is-user]:ml-auto min-w-0",
28
28
  {
29
29
  variants: {
30
30
  variant: {
@@ -78,3 +78,11 @@ export const MessageAvatar = ({
78
78
  <AvatarFallback>{name?.slice(0, 2) || "ME"}</AvatarFallback>
79
79
  </Avatar>
80
80
  );
81
+
82
+ export const MessageTypingIndicator = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
83
+ <div className={cn("flex items-center space-x-1 py-1.5 px-1 h-[24px]", className)} {...props}>
84
+ <div className="w-1.5 h-1.5 bg-current rounded-full animate-bounce [animation-delay:-0.3s]"></div>
85
+ <div className="w-1.5 h-1.5 bg-current rounded-full animate-bounce [animation-delay:-0.15s]"></div>
86
+ <div className="w-1.5 h-1.5 bg-current rounded-full animate-bounce"></div>
87
+ </div>
88
+ );
@@ -11,6 +11,7 @@ import { UserMessage } from "@/components/composites/UserMessage"
11
11
  import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
12
12
  import { OrchestratorMessage } from "@/components/composites/OrchestratorMessage"
13
13
  import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay"
14
+ import { ApprovalCard } from "@/components/composites/ApprovalCard"
14
15
 
15
16
  /**
16
17
  * AIConversation Section
@@ -31,6 +32,7 @@ interface AIMessage {
31
32
  avatarName?: string;
32
33
  toolCalls?: ToolCall[];
33
34
  subAgents?: SubAgent[];
35
+ isLoading?: boolean;
34
36
  }
35
37
 
36
38
  export interface AIConversationProps
@@ -114,11 +116,17 @@ export const AIConversation = React.memo<AIConversationProps>(
114
116
  const subAgents = message.subAgents || []
115
117
 
116
118
  // Filter tool calls that aren't "task" type (those become sub-agents)
119
+ // Also completely hide "ask_user" and "ask_question" tools so they are ONLY rendered in the prompt input area
117
120
  const directToolCalls =
118
121
  message.toolCalls?.filter(
119
- (tc) => tc.name !== "task"
122
+ (tc) => tc.name !== "task" && tc.name !== "ask_user"
120
123
  ) || []
121
124
 
125
+ const hasContent = message.content && message.content.trim() !== ""
126
+ if (!hasContent && directToolCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
127
+ return null;
128
+ }
129
+
122
130
  return (
123
131
  <OrchestratorMessage
124
132
  key={message.id}
@@ -127,6 +135,7 @@ export const AIConversation = React.memo<AIConversationProps>(
127
135
  content: message.content,
128
136
  avatarSrc: message.avatarSrc,
129
137
  avatarName: message.avatarName,
138
+ isLoading: message.isLoading,
130
139
  }}
131
140
  showAvatar={showAvatars}
132
141
  >
@@ -143,7 +152,7 @@ export const AIConversation = React.memo<AIConversationProps>(
143
152
  id: subAgent.id,
144
153
  name: subAgent.subAgentName,
145
154
  description: undefined,
146
- content: `**Task:** ${typeof subAgent.input === 'string' ? subAgent.input : JSON.stringify(subAgent.input)}${subAgent.output ? `\n\n**Output:** ${typeof subAgent.output === 'string' ? subAgent.output : JSON.stringify(subAgent.output)}` : ''}`,
155
+ content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
147
156
  status: subAgent.status,
148
157
  toolCalls: [],
149
158
  }}
@@ -28,11 +28,11 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
28
28
  ...section,
29
29
  resizable,
30
30
  content: (
31
- <div className="h-full min-h-0 flex flex-col overflow-hidden">
31
+ <div className="h-full min-h-0 flex flex-col overflow-auto">
32
32
  {section.header && (
33
33
  <AppHeader {...section.header} />
34
34
  )}
35
- <div className="min-h-0 flex-1 overflow-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
35
+ <div className="min-h-0 flex-1 overflow-hidden">
36
36
  {section.content}
37
37
  </div>
38
38
  </div>
@@ -3,6 +3,7 @@ import {
3
3
  Message,
4
4
  MessageContent,
5
5
  MessageAvatar,
6
+ MessageTypingIndicator,
6
7
  } from "@/components/ai-elements/message"
7
8
 
8
9
  /**
@@ -17,6 +18,7 @@ export interface OrchestratorMessageData {
17
18
  content: string
18
19
  avatarSrc?: string
19
20
  avatarName?: string
21
+ isLoading?: boolean
20
22
  }
21
23
 
22
24
  export interface OrchestratorMessageProps {
@@ -53,9 +55,13 @@ export const OrchestratorMessage = React.memo<OrchestratorMessageProps>(
53
55
  />
54
56
  )}
55
57
 
56
- <div className="flex-1">
58
+ <div className="flex-1 min-w-0">
57
59
  {/* Orchestrator's message content */}
58
- {hasContent && (
60
+ {message.isLoading ? (
61
+ <MessageContent variant="contained" className="w-fit pr-6">
62
+ <MessageTypingIndicator />
63
+ </MessageContent>
64
+ ) : hasContent && (
59
65
  <MessageContent variant="contained">
60
66
  {message.content}
61
67
  </MessageContent>
@@ -9,6 +9,7 @@ import {
9
9
  } from "@/components/ai-elements/plan"
10
10
  import { ToolCallDisplay, type ToolCall } from "@/components/composites/ToolCallDisplay"
11
11
  import { cn } from "@/lib/utils"
12
+ import { Streamdown } from "streamdown"
12
13
 
13
14
  /**
14
15
  * SpecialistMessage Block
@@ -85,9 +86,12 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
85
86
  <PlanContent>
86
87
  {/* Message content */}
87
88
  {hasContent && (
88
- <div className="prose prose-sm dark:prose-invert max-w-none">
89
+ <Streamdown
90
+ className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
91
+ mode="static"
92
+ >
89
93
  {message.content}
90
- </div>
94
+ </Streamdown>
91
95
  )}
92
96
 
93
97
  {/* Tool calls */}
@@ -38,6 +38,7 @@ import { DocumentTabBar } from '@/components/composites/DocumentTabBar'
38
38
  import { cn } from '@/lib/utils'
39
39
  import type { JSONContent } from '@tiptap/core'
40
40
  import type { Annotation, User } from '@/types/ai-editor/annotations'
41
+ import { Streamdown } from 'streamdown'
41
42
 
42
43
  interface DocumentWithAnnotations {
43
44
  file: { id: string; name: string; isDirty?: boolean; format?: 'json' | 'markdown' | string; lastModified?: number }
@@ -145,6 +146,7 @@ function isMultiTabMode(props: AIDocEditorProps): props is AIDocEditorMultiTabPr
145
146
  * - Support for comments, suggestions, and block additions
146
147
  * - Controlled component pattern (all state managed by parent)
147
148
  * - Backward-compatible with single-document consumers
149
+ * - Markdown content rendered via Streamdown for proper formatting
148
150
  *
149
151
  * Note: The refinement panel (right sidebar with Accept All/Reject All) is a
150
152
  * separate component in the parent application, not part of this feature.
@@ -156,7 +158,7 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
156
158
  mode,
157
159
  onAnnotationAdd,
158
160
  onAnnotationUpdate,
159
- onAnnotationDelete: _onAnnotationDelete, // Callback for consumers; passed through on demand
161
+ onAnnotationDelete: _onAnnotationDelete,
160
162
  className,
161
163
  } = props
162
164
 
@@ -199,6 +201,16 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
199
201
  * Single-document mode
200
202
  */
201
203
  if (!isMultiTab) {
204
+ const isMarkdown = props.format === 'markdown'
205
+ if (isMarkdown) {
206
+ return (
207
+ <div className={cn('ai-doc-editor p-6', className)}>
208
+ <Streamdown mode="streaming">
209
+ {props.content as string}
210
+ </Streamdown>
211
+ </div>
212
+ )
213
+ }
202
214
  return (
203
215
  <DocumentEditorWithComments
204
216
  content={props.content}
@@ -234,6 +246,8 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
234
246
  )
235
247
  }
236
248
 
249
+ const isMarkdown = currentDocument.file.format === 'markdown'
250
+
237
251
  return (
238
252
  <div className={cn('ai-doc-editor flex flex-col h-full', className)}>
239
253
  <DocumentTabBar
@@ -243,17 +257,29 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
243
257
  onTabClose={props.onTabClose}
244
258
  />
245
259
  <div className="flex-1 overflow-auto">
246
- <DocumentEditorWithComments
247
- content={currentDocument.content}
248
- format={currentDocument.file.format as 'json' | 'markdown' | undefined}
249
- annotations={currentDocument.annotations}
250
- currentUserId={currentUser.id}
251
- currentUserName={currentUser.name}
252
- readOnly={mode === 'readonly'}
253
- onAnnotationAdd={handleAnnotationAdd}
254
- onAnnotationUpdate={handleAnnotationUpdate}
255
- className="p-6"
256
- />
260
+ {isMarkdown ? (
261
+ <div className="p-6">
262
+ <Streamdown
263
+ mode="streaming"
264
+ isAnimating
265
+ className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
266
+ >
267
+ {currentDocument.content as string}
268
+ </Streamdown>
269
+ </div>
270
+ ) : (
271
+ <DocumentEditorWithComments
272
+ content={currentDocument.content}
273
+ format={currentDocument.file.format as 'json' | 'markdown' | undefined}
274
+ annotations={currentDocument.annotations}
275
+ currentUserId={currentUser.id}
276
+ currentUserName={currentUser.name}
277
+ readOnly={mode === 'readonly'}
278
+ onAnnotationAdd={handleAnnotationAdd}
279
+ onAnnotationUpdate={handleAnnotationUpdate}
280
+ className="p-6"
281
+ />
282
+ )}
257
283
  </div>
258
284
  </div>
259
285
  )
@@ -105,6 +105,62 @@ export const reviewStateMessages: RefinementMessage[] = [
105
105
  },
106
106
  ],
107
107
  },
108
+ {
109
+ id: '3',
110
+ type: 'human',
111
+ role: 'user',
112
+ content: 'Can we also make sure the colors meet the contrast requirements for dark mode?',
113
+ avatarSrc:
114
+ 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
115
+ avatarName: 'User',
116
+ },
117
+ {
118
+ id: '4',
119
+ type: 'ai',
120
+ role: 'orchestrator',
121
+ content: 'Absolutely. I will have the accessibility specialist double-check the color contrast ratios in both light and dark mode.',
122
+ avatarSrc:
123
+ 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
124
+ avatarName: 'Coordinator',
125
+ subAgents: [
126
+ {
127
+ id: 'agent_3',
128
+ name: 'accessibility-specialist',
129
+ subAgentName: 'a11y-agent',
130
+ input: 'Verify color contrast ratios for the new Button styles in both light and dark mode themes according to WCAG 2.1 AA.',
131
+ output: 'Contrast ratios checked: Primary button passes at 4.5:1 on light mode, but fails on dark mode (3.2:1). Adjusting the primary-dark token to #66b2ff to meet the 4.5:1 requirement.',
132
+ status: 'completed',
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ id: '5',
138
+ type: 'human',
139
+ role: 'user',
140
+ content: 'Perfect! How about the hover state? Does it also pass?',
141
+ avatarSrc:
142
+ 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
143
+ avatarName: 'User',
144
+ },
145
+ {
146
+ id: '6',
147
+ type: 'ai',
148
+ role: 'orchestrator',
149
+ content: 'Good catch. Let me check the hover and active states for both themes.',
150
+ avatarSrc:
151
+ 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
152
+ avatarName: 'Coordinator',
153
+ subAgents: [
154
+ {
155
+ id: 'agent_4',
156
+ name: 'accessibility-specialist',
157
+ subAgentName: 'a11y-agent',
158
+ input: 'Verify color contrast for hover and active states.',
159
+ output: 'Hover states pass contrast checks. The active state requires a slight tweak for the outline variant. Applied fixes.',
160
+ status: 'completed',
161
+ }
162
+ ]
163
+ },
108
164
  ]
109
165
 
110
166
  /**
@@ -114,7 +170,7 @@ export const reviewStateMessages: RefinementMessage[] = [
114
170
  * Sample single-question approval request (HITL interactive question)
115
171
  */
116
172
  export const approvalQuestionRequest: ActionRequest = {
117
- name: "ask_question",
173
+ name: "ask_user",
118
174
  description: "The agent needs your input to determine the workflow order",
119
175
  args: {
120
176
  question: "Where in the workflow timeline should the SEO review step run?",
@@ -130,7 +186,7 @@ export const approvalQuestionRequest: ActionRequest = {
130
186
  * Sample multi-question approval request (HITL poll with multiple questions)
131
187
  */
132
188
  export const approvalMultiQuestionRequest: ActionRequest = {
133
- name: "ask_question",
189
+ name: "ask_user",
134
190
  description: "The agent needs your preferences to proceed",
135
191
  args: {
136
192
  questions: [
@@ -35,6 +35,7 @@ export interface RefinementMessage {
35
35
  avatarName?: string;
36
36
  toolCalls?: ToolCall[];
37
37
  subAgents?: SubAgent[];
38
+ isLoading?: boolean;
38
39
  }
39
40
 
40
41
  /**
@@ -157,12 +158,47 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
157
158
  onApprovalReject?.(reason);
158
159
  }, [onApprovalReject]);
159
160
 
161
+ // Extract pending ask_user tool call from messages
162
+ const pendingAskUser = React.useMemo(() => {
163
+ // Look from the end
164
+ for (let i = messages.length - 1; i >= 0; i--) {
165
+ const msg = messages[i];
166
+ if (msg.toolCalls) {
167
+ const askUserTc = msg.toolCalls.find(tc => tc.name === "ask_user" && tc.status === "pending");
168
+ if (askUserTc) {
169
+ return askUserTc;
170
+ }
171
+ }
172
+ }
173
+ return null;
174
+ }, [messages]);
175
+
176
+ const activeApprovalRequest = React.useMemo(() => {
177
+ if (approvalRequest) return approvalRequest;
178
+ if (pendingAskUser) {
179
+ return {
180
+ name: pendingAskUser.name,
181
+ args: pendingAskUser.args,
182
+ };
183
+ }
184
+ return undefined;
185
+ }, [approvalRequest, pendingAskUser]);
186
+
160
187
  // Handle edit for HITL approval request
161
188
  const handleApprovalEdit = React.useCallback((editedArgs: Record<string, unknown>) => {
162
189
  setApprovalCardState("approval-responded");
163
190
  setApprovalCardApproval({ approved: true });
164
191
  onApprovalEdit?.(editedArgs);
165
- }, [onApprovalEdit]);
192
+
193
+ // If there's a pendingAskUser and answers are provided, submit them!
194
+ if (pendingAskUser && editedArgs.answers) {
195
+ const answers = Array.isArray(editedArgs.answers) ? editedArgs.answers : [editedArgs.answers];
196
+ const text = answers.join(', ');
197
+ // Submit the text response
198
+ const dummyEvent = { preventDefault: () => {} } as React.FormEvent<HTMLFormElement>;
199
+ onSubmit({ text, files: [] }, dummyEvent);
200
+ }
201
+ }, [onApprovalEdit, pendingAskUser, onSubmit]);
166
202
 
167
203
  // Reset file change state when fileChanges are cleared
168
204
  React.useEffect(() => {
@@ -172,24 +208,22 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
172
208
  }
173
209
  }, [fileChanges.length]);
174
210
 
175
- // Reset approval card state when approvalRequest is cleared
211
+ // Reset approval card state when activeApprovalRequest is cleared
176
212
  React.useEffect(() => {
177
- if (!approvalRequest) {
213
+ if (!activeApprovalRequest) {
178
214
  setApprovalCardState("approval-requested");
179
215
  setApprovalCardApproval({});
180
216
  }
181
- }, [approvalRequest]);
182
-
183
-
217
+ }, [activeApprovalRequest]);
184
218
 
185
219
  // Construct dialog content that replaces the prompt input when active
186
220
  let dialog: React.ReactNode = null;
187
221
 
188
- if (approvalRequest) {
222
+ if (activeApprovalRequest) {
189
223
  dialog = (
190
224
  <div className="w-full flex-shrink-0 bg-background p-4">
191
225
  <ApprovalCard
192
- actionRequest={approvalRequest}
226
+ actionRequest={activeApprovalRequest}
193
227
  reviewConfig={reviewConfig}
194
228
  onApprove={handleApprovalApprove}
195
229
  onReject={handleApprovalReject}
@@ -218,18 +252,21 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
218
252
  }
219
253
 
220
254
  return (
221
- <div className={`relative flex h-dvh flex-col ${className || ""}`}>
222
- <AIConversation
223
- messages={messages}
224
- showAvatars={true}
225
- className="relative min-h-0 flex-1 overflow-y-auto"
226
- />
227
- <PromptInput
228
- dialog={dialog}
229
- placeholder={placeholder}
230
- onSubmit={onSubmit}
231
- className="border-t"
232
- />
255
+ <div className={`relative flex h-full flex-col ${className || ""}`}>
256
+ <div className="flex-1 flex flex-col pb-4 min-h-0">
257
+ <AIConversation
258
+ messages={messages}
259
+ showAvatars={true}
260
+ className="relative"
261
+ />
262
+ </div>
263
+ <div className="sticky bottom-0 z-10 bg-background border-t">
264
+ <PromptInput
265
+ dialog={dialog}
266
+ placeholder={placeholder}
267
+ onSubmit={onSubmit}
268
+ />
269
+ </div>
233
270
  </div>
234
271
  );
235
272
  }