ai-design-system 0.1.44 → 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.
Files changed (31) hide show
  1. package/components/ai-elements/code-block.tsx +2 -2
  2. package/components/ai-elements/message.tsx +10 -2
  3. package/components/blocks/AIConversation/AIConversation.tsx +11 -2
  4. package/components/{composites → blocks}/PromptInput/PromptInput.stories.tsx +77 -2
  5. package/components/{composites → blocks}/PromptInput/PromptInput.tsx +9 -38
  6. package/components/blocks/PromptInput/index.ts +3 -0
  7. package/components/blocks/SectionLayout/SectionLayout.tsx +2 -2
  8. package/components/blocks/index.ts +3 -0
  9. package/components/composites/ApprovalCard/ApprovalCard.stories.tsx +115 -0
  10. package/components/composites/ApprovalCard/ApprovalCard.tsx +533 -0
  11. package/components/composites/ApprovalCard/index.ts +2 -0
  12. package/components/composites/Confirmation/Confirmation.stories.tsx +1 -1
  13. package/components/composites/OrchestratorMessage/OrchestratorMessage.tsx +8 -2
  14. package/components/composites/SpecialistMessage/SpecialistMessage.tsx +6 -2
  15. package/components/composites/index.ts +4 -3
  16. package/components/features/AIDocEditor/AIDocEditor.tsx +38 -12
  17. package/components/features/RefinementPanel/RefinementPanel.mocks.ts +95 -0
  18. package/components/features/RefinementPanel/RefinementPanel.stories.tsx +147 -1
  19. package/components/features/RefinementPanel/RefinementPanel.tsx +151 -36
  20. package/components/features/RefinementPanel/index.ts +3 -0
  21. package/components/features/RefinementPanel/useRefinementPanel.d.ts +19 -0
  22. package/components/features/RefinementPanel/useRefinementPanel.mock.ts +77 -35
  23. package/components/index.ts +2 -1
  24. package/dist/index.cjs +611 -66
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.css +133 -3
  27. package/dist/index.d.ts +84 -34
  28. package/dist/index.js +612 -68
  29. package/dist/index.js.map +1 -1
  30. package/package.json +7 -7
  31. package/components/composites/PromptInput/index.ts +0 -8
@@ -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
  >
@@ -142,7 +151,7 @@ export const AIConversation = React.memo<AIConversationProps>(
142
151
  message={{
143
152
  id: subAgent.id,
144
153
  name: subAgent.subAgentName,
145
- description: typeof subAgent.input === 'string' ? subAgent.input : JSON.stringify(subAgent.input),
154
+ description: undefined,
146
155
  content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
147
156
  status: subAgent.status,
148
157
  toolCalls: [],
@@ -61,7 +61,7 @@ export const ControlledState: Story = {
61
61
  placeholder="Type something (controlled mode)"
62
62
  onSubmit={(message) => {
63
63
  setSubmittedMessages((prev) => [...prev, message.text || ""]);
64
- setValue(""); // Clear after submit
64
+ setValue("");
65
65
  }}
66
66
  />
67
67
 
@@ -164,7 +164,6 @@ export const CustomSubmitHandler: Story = {
164
164
  disabled={isSubmitting}
165
165
  onSubmit={async (message) => {
166
166
  setIsSubmitting(true);
167
- // Simulate async processing
168
167
  await new Promise((resolve) => setTimeout(resolve, 2000));
169
168
  setLastSubmitted(message.text || "");
170
169
  setIsSubmitting(false);
@@ -198,3 +197,79 @@ export const NoAttachmentUI: Story = {
198
197
  },
199
198
  },
200
199
  };
200
+
201
+ /**
202
+ * Dialog replacement — static content
203
+ * Shows PromptInput rendering a dialog instead of the input when `dialog` is set.
204
+ * The dialog content replaces the input entirely; no input is shown.
205
+ */
206
+ export const WithDialog: Story = {
207
+ args: {
208
+ placeholder: "This should not appear — dialog is active",
209
+ onSubmit: (message) => {
210
+ console.log("Submitted:", message);
211
+ },
212
+ dialog: (
213
+ <div className="rounded-lg border border-border bg-card p-6 text-center text-sm text-muted-foreground">
214
+ <p className="font-medium text-foreground">Dialog Content</p>
215
+ <p className="mt-1">This replaces the prompt input when the <code>dialog</code> prop is set.</p>
216
+ </div>
217
+ ),
218
+ },
219
+ parameters: {
220
+ docs: {
221
+ description: {
222
+ story:
223
+ "When the `dialog` prop is set, PromptInput renders that ReactNode instead of the input form. This is used by features like RefinementPanel to show file reviews or HITL approval cards in place of the input.",
224
+ },
225
+ },
226
+ },
227
+ };
228
+
229
+ /**
230
+ * Dialog replacement — interactive toggle
231
+ * Demonstrates switching between input mode and dialog mode.
232
+ */
233
+ export const ToggleDialog: Story = {
234
+ render: () => {
235
+ const [showDialog, setShowDialog] = useState(false);
236
+
237
+ return (
238
+ <div className="space-y-4">
239
+ <div className="rounded-md border border-border bg-muted p-4">
240
+ <label className="flex items-center gap-2">
241
+ <input
242
+ type="checkbox"
243
+ checked={showDialog}
244
+ onChange={(e) => setShowDialog(e.target.checked)}
245
+ />
246
+ <span className="text-sm">Show dialog (replaces input)</span>
247
+ </label>
248
+ </div>
249
+
250
+ <PromptInput
251
+ placeholder="Type a message..."
252
+ onSubmit={(message) => {
253
+ console.log("Submitted:", message);
254
+ }}
255
+ dialog={
256
+ showDialog ? (
257
+ <div className="rounded-lg border border-border bg-card p-6 text-center text-sm text-muted-foreground">
258
+ <p className="font-medium text-foreground">Dialog Active</p>
259
+ <p className="mt-1">Uncheck the toggle above to return to input mode.</p>
260
+ </div>
261
+ ) : undefined
262
+ }
263
+ />
264
+ </div>
265
+ );
266
+ },
267
+ parameters: {
268
+ docs: {
269
+ description: {
270
+ story:
271
+ "Interactive demonstration of the dialog replacement. Toggle the checkbox to switch between showing the prompt input and showing dialog content. This is the same pattern RefinementPanel uses for file review and HITL approval states.",
272
+ },
273
+ },
274
+ },
275
+ };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import type { ReactNode } from "react";
4
5
  import {
5
6
  PromptInput as AIPromptInput,
6
7
  PromptInputBody,
@@ -14,52 +15,22 @@ import {
14
15
  } from "@/components/ai-elements/prompt-input";
15
16
  import type { FormEvent } from "react";
16
17
 
17
- /**
18
- * PromptInput Block
19
- *
20
- * A thin wrapper around the PromptInput AI element that hides attachment functionality.
21
- * This block provides a simplified prompt input interface focused on text-only input.
22
- *
23
- * Features:
24
- * - Controlled and uncontrolled state support
25
- * - Disabled state (blocks submission, keeps textarea editable)
26
- * - Custom placeholder text
27
- * - No attachment UI (compositional hiding)
28
- */
29
-
30
18
  export interface PromptInputBlockProps
31
19
  extends Omit<
32
20
  AIPromptInputProps,
33
21
  "accept" | "multiple" | "maxFiles" | "maxFileSize" | "globalDrop" | "syncHiddenInput" | "onSubmit" | "onChange"
34
22
  > {
35
- /**
36
- * Blocks form submission when true. Textarea remains editable for drafts.
37
- */
38
23
  disabled?: boolean;
39
- /**
40
- * Placeholder text for the textarea input
41
- */
42
24
  placeholder?: string;
43
- /**
44
- * Controlled state value (when provided with onChange)
45
- */
46
25
  value?: string;
47
- /**
48
- * Controlled state change handler (when provided with value)
49
- */
50
26
  onChange?: (value: string) => void;
51
- /**
52
- * Submit handler called when form is submitted
53
- */
54
27
  onSubmit: (
55
28
  message: PromptInputMessage,
56
29
  event: FormEvent<HTMLFormElement>
57
30
  ) => void | Promise<void>;
31
+ dialog?: ReactNode;
58
32
  }
59
33
 
60
- /**
61
- * PromptInput component - text-only prompt input without attachments
62
- */
63
34
  export const PromptInput = React.memo<PromptInputBlockProps>(
64
35
  ({
65
36
  disabled = false,
@@ -67,9 +38,9 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
67
38
  value,
68
39
  onChange,
69
40
  onSubmit,
41
+ dialog,
70
42
  ...props
71
43
  }) => {
72
- // Handle form submission with disabled state
73
44
  const handleSubmit = React.useCallback(
74
45
  (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
75
46
  if (disabled) {
@@ -81,10 +52,8 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
81
52
  [disabled, onSubmit]
82
53
  );
83
54
 
84
- // Controlled mode: wrap in PromptInputProvider
85
55
  const isControlled = value !== undefined && onChange !== undefined;
86
56
 
87
- // Handle controlled state change
88
57
  const handleControlledChange = React.useCallback(
89
58
  (e: React.ChangeEvent<HTMLTextAreaElement>) => {
90
59
  if (onChange) {
@@ -94,26 +63,28 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
94
63
  [onChange]
95
64
  );
96
65
 
97
- // Render the prompt input composition
66
+ if (dialog) {
67
+ return <>{dialog}</>;
68
+ }
69
+
98
70
  const promptInputContent = (
99
71
  <AIPromptInput onSubmit={handleSubmit} {...props}>
100
72
  <PromptInputBody>
101
73
  <PromptInputTextarea
102
74
  placeholder={placeholder}
103
- disabled={false} // Keep editable even when form is disabled
75
+ disabled={false}
104
76
  onChange={isControlled ? handleControlledChange : undefined}
105
77
  />
106
78
  </PromptInputBody>
107
79
  <PromptInputFooter>
108
80
  <PromptInputTools>
109
- {/* No attachment menu - compositionally hidden */}
81
+ {}
110
82
  </PromptInputTools>
111
83
  <PromptInputSubmit disabled={disabled} />
112
84
  </PromptInputFooter>
113
85
  </AIPromptInput>
114
86
  );
115
87
 
116
- // Wrap in provider for controlled mode
117
88
  if (isControlled) {
118
89
  return (
119
90
  <PromptInputProvider initialInput={value}>
@@ -0,0 +1,3 @@
1
+ export { PromptInput } from "./PromptInput";
2
+
3
+ export type { PromptInputBlockProps } from "./PromptInput";
@@ -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>
@@ -10,6 +10,9 @@ export type { DocumentEditorWithCommentsProps } from './DocumentEditorWithCommen
10
10
  export { FileChangeQueue } from './FileChangeQueue'
11
11
  export type { FileChangeQueueProps } from './FileChangeQueue'
12
12
 
13
+ export { PromptInput } from './PromptInput'
14
+ export type { PromptInputBlockProps } from './PromptInput'
15
+
13
16
  // Blocks
14
17
  export * from './LayoutProvider'
15
18
  export * from './SectionLayout'
@@ -0,0 +1,115 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { ApprovalCard } from "./ApprovalCard";
3
+ import { fn } from "@storybook/test";
4
+
5
+ const meta: Meta<typeof ApprovalCard> = {
6
+ title: "Composites/ApprovalCard",
7
+ component: ApprovalCard,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ args: {
12
+ onApprove: fn(),
13
+ onReject: fn(),
14
+ onEdit: fn(),
15
+ isProcessing: false,
16
+ },
17
+ } satisfies Meta<typeof ApprovalCard>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof ApprovalCard>;
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ actionRequest: {
25
+ name: "send_email",
26
+ description: "Review email before sending to team",
27
+ args: {
28
+ to: "team@acme.com",
29
+ subject: "Q4 Results",
30
+ body: "Revenue grew 15% this quarter.",
31
+ },
32
+ },
33
+ reviewConfig: {
34
+ allowedDecisions: ["approve", "edit", "reject"],
35
+ },
36
+ },
37
+ };
38
+
39
+ export const SimpleApprovalOnly: Story = {
40
+ args: {
41
+ actionRequest: {
42
+ name: "publish_content",
43
+ description: "Publish video narration to production database",
44
+ args: {
45
+ document_id: "doc_123",
46
+ environment: "production",
47
+ },
48
+ },
49
+ reviewConfig: {
50
+ allowedDecisions: ["approve", "reject"],
51
+ },
52
+ },
53
+ };
54
+
55
+ export const MultilineArguments: Story = {
56
+ args: {
57
+ actionRequest: {
58
+ name: "apply_patch",
59
+ description: "Approve and apply code patch to target repository",
60
+ args: {
61
+ patch: `diff --git a/src/index.ts b/src/index.ts
62
+ index 83a2d78..d3345f1 100644
63
+ --- a/src/index.ts
64
+ +++ b/src/index.ts
65
+ @@ -10,3 +10,4 @@
66
+ export function hello() {
67
+ - console.log("hello");
68
+ + console.log("hello world!");
69
+ }`,
70
+ author: "AI Developer Agent",
71
+ },
72
+ },
73
+ reviewConfig: {
74
+ allowedDecisions: ["approve", "edit", "reject"],
75
+ },
76
+ },
77
+ };
78
+
79
+ export const InteractiveQuestion: Story = {
80
+ args: {
81
+ actionRequest: {
82
+ name: "ask_question",
83
+ args: {
84
+ question: "Where in the workflow timeline should the SEO review step run?",
85
+ options: [
86
+ "(Recommended) Before the main human content approval, so the editor can see the SEO recommendations and scores when reviewing the script.",
87
+ "After the main human content approval, so that the general content is finalized first before being tuned for SEO."
88
+ ],
89
+ is_multi_select: false
90
+ }
91
+ }
92
+ }
93
+ };
94
+
95
+ export const MultiQuestionPoll: Story = {
96
+ args: {
97
+ actionRequest: {
98
+ name: "ask_question",
99
+ args: {
100
+ questions: [
101
+ {
102
+ question: "Choose preferred publication channel:",
103
+ options: ["YouTube", "Vimeo", "TikTok", "Instagram Reels"],
104
+ is_multi_select: false
105
+ },
106
+ {
107
+ question: "Which departments need to sign off on budget?",
108
+ options: ["Marketing", "Finance", "Legal", "Operations"],
109
+ is_multi_select: true
110
+ }
111
+ ]
112
+ }
113
+ }
114
+ }
115
+ };