@alquimia-ai/ui 2.2.2 → 2.3.0

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/README.md CHANGED
@@ -1,42 +1,372 @@
1
1
  # @alquimia-ai/ui
2
2
 
3
- UI components for Alquimia SDK.
3
+ The component half of the Alquimia frontend SDK. A React + Tailwind library built on shadcn/ui primitives: chat organisms, a themeable design system, and the renderer that turns an agent-authored component tree into real interactive UI.
4
4
 
5
- ## Table of Contents
5
+ It holds no runtime logic. State comes from [`@alquimia-ai/tools`](../tools) — typically `useAlquimia` — and these components render it.
6
6
 
7
- - [Installation](#installation)
8
- - [Usage](#usage)
9
- - [Available Components](#available-components)
7
+ - **Peer deps:** React ≥ 18, React DOM ≥ 18. Tailwind CSS for styling.
8
+ - **Not required:** `next`, `next-themes`. The package is framework-agnostic.
9
+
10
+ ---
11
+
12
+ ## Table of contents
13
+
14
+ - [Install and configure](#install-and-configure)
15
+ - [Theming](#theming)
16
+ - [Building a chat](#building-a-chat)
17
+ - [Chat organisms](#chat-organisms)
18
+ - [Tool approval](#tool-approval)
19
+ - [GenUI](#genui)
20
+ - [Speech](#speech)
21
+ - [Ratings](#ratings)
22
+ - [Documents and viewers](#documents-and-viewers)
23
+ - [Component inventory](#component-inventory)
24
+ - [Hooks and utilities](#hooks-and-utilities)
25
+ - [Export map](#export-map)
10
26
  - [Development](#development)
11
- - [Contributing](#contributing)
12
- - [License](#license)
13
27
 
14
- ## Installation
28
+ ---
15
29
 
16
- To install the `@alquimia-ai/ui` package, use the following command:
30
+ ## Install and configure
17
31
 
18
32
  ```sh
19
- yarn add @alquimia-ai/ui
33
+ npm install @alquimia-ai/tools @alquimia-ai/ui
34
+ npm install tailwindcss tailwindcss-animate
35
+ npm install framer-motion lucide-react # usually transitive
20
36
  ```
21
37
 
22
- Or with npm:
38
+ ### Tailwind
23
39
 
24
- ```sh
25
- npm install @alquimia-ai/ui
40
+ Use the package as a **preset**. One line replaces every colour, radius and animation token:
41
+
42
+ ```js
43
+ /** @type {import('tailwindcss').Config} */
44
+ module.exports = {
45
+ presets: [require('@alquimia-ai/ui/tailwind.config')],
46
+ content: [
47
+ './app/**/*.{ts,tsx}',
48
+ './components/**/*.{ts,tsx}',
49
+ './src/**/*.{ts,tsx}',
50
+ ],
51
+ };
52
+ ```
53
+
54
+ The preset already contributes the UI package's `dist` path to `content`, all token mappings, and the `tailwindcss-animate` plugin.
55
+
56
+ ### CSS
57
+
58
+ Import exactly one theme:
59
+
60
+ ```ts
61
+ import '@alquimia-ai/ui/styles/themes/base.css'; // neutral light + dark
62
+ import '@alquimia-ai/ui/styles/themes/base-alquimia.css'; // Alquimia brand
63
+ import '@alquimia-ai/ui/styles/themes/base-nordic.css';
64
+ import '@alquimia-ai/ui/styles/themes/base-primary.css';
65
+ ```
66
+
67
+ Optional stylesheets for specific surfaces: `styles/globals.css`, `prose.css`, `call-out.css`, `drawer.css`, `ratings.css`.
68
+
69
+ ---
70
+
71
+ ## Theming
72
+
73
+ Every component reads HSL CSS variables through Tailwind, so a theme is just a variable block. To use your own palette, skip the theme file and define them yourself:
74
+
75
+ ```css
76
+ @layer base {
77
+ :root {
78
+ --primary: 223 82% 60%;
79
+ --primary-foreground: 0 0% 98%;
80
+ --background: 0 0% 95%;
81
+ --foreground: 0 0% 3.9%;
82
+ --card: 0 0% 100%;
83
+ --muted: 0 0% 96.1%;
84
+ --muted-foreground: 0 0% 45.1%;
85
+ --border: 0 0% 85%;
86
+ --ring: 221 83% 53%;
87
+ --radius: 0.75rem;
88
+ }
89
+ }
90
+ ```
91
+
92
+ Dark mode is class-based. `AlquimiaUIProvider` toggles `.dark` on `<html>` and tracks the system preference — no `next-themes` dependency, and it coexists with one if you already have it:
93
+
94
+ ```tsx
95
+ import { AlquimiaUIProvider } from '@alquimia-ai/ui';
96
+
97
+ <AlquimiaUIProvider defaultMode="system"> {/* 'light' | 'dark' | 'system' */}
98
+ {children}
99
+ </AlquimiaUIProvider>
100
+ ```
101
+
102
+ `useAlquimiaTheme()` reads the resolved theme and lets you set it.
103
+
104
+ ---
105
+
106
+ ## Building a chat
107
+
108
+ The library favours composition over a monolith: an area that renders messages, a form that sends them, and slots for everything optional.
109
+
110
+ ```tsx
111
+ 'use client';
112
+ import { useRef } from 'react';
113
+ import { useAlquimia } from '@alquimia-ai/tools/hooks';
114
+ import { createNextJsAdapter } from '@alquimia-ai/tools/adapters/next';
115
+ import { AssistantMessageArea, AssistantInput } from '@alquimia-ai/ui/components/organisms';
116
+ import { ThinkIndicator } from '@alquimia-ai/ui/components/atoms';
117
+
118
+ const adapter = createNextJsAdapter();
119
+
120
+ export function Chat({ assistantId, conversationId }: Props) {
121
+ const messagesEndRef = useRef<HTMLDivElement>(null);
122
+ const alquimia = useAlquimia({ assistantId, adapter });
123
+
124
+ return (
125
+ <div className="flex h-full flex-col">
126
+ <AssistantMessageArea
127
+ messages={alquimia.messages.filter((m) => m.role !== 'system')}
128
+ messagesEndRef={messagesEndRef}
129
+ isLoading={alquimia.isMessageLoading}
130
+ isMessageStreaming={alquimia.isMessageStreaming}
131
+ streamingMessageId={alquimia.streamingMessageId}
132
+ thinkIndicator={<ThinkIndicator thoughts={['Thinking…']} />}
133
+ />
134
+
135
+ <AssistantInput
136
+ input={alquimia.input}
137
+ handleInputChange={alquimia.handleInputChange}
138
+ sendMessageFunc={(e) => alquimia.handleSubmit(e, undefined, conversationId)}
139
+ isMessageStreaming={alquimia.isMessageStreaming}
140
+ isButtonDisabled={!alquimia.input.trim()}
141
+ onFileDrop={alquimia.addAttachments}
142
+ />
143
+ </div>
144
+ );
145
+ }
26
146
  ```
27
147
 
28
- ## Available Components
148
+ `Assistant` wraps this in a shell with an optional title and description, and exports `AssistantHeader`, `AssistantTitle` and `AssistantDescription` for custom headers.
149
+
150
+ ---
151
+
152
+ ## Chat organisms
153
+
154
+ ### `AssistantMessageArea`
155
+
156
+ Renders the conversation: markdown, streaming text, timestamps, error states, tool cards and per-message actions.
157
+
158
+ | Prop | Type | Notes |
159
+ |---|---|---|
160
+ | `messages` | `AlquimiaMessage[]` | required |
161
+ | `messagesEndRef` | ref | scroll anchor |
162
+ | `isLoading` | `boolean` | required |
163
+ | `isMessageStreaming` | `boolean` | |
164
+ | `streamingMessageId` | `string \| null` | required — which bubble is live |
165
+ | `thinkIndicator` | `ReactNode` | replaces the default typing bubble |
166
+ | `actions` | `MessageAction[]` | per-message buttons (copy, rate, speak…) |
167
+ | `toolFactory` | `ToolFactory` | renders tool events inline |
168
+ | `showDetailedErrors` | `boolean` | surfaces `error_code` / `error_detail` |
169
+ | `handleIsTextStreaming` | `(streaming) => void` | typewriter lifecycle |
170
+ | `interleave` | `(afterIndex) => ReactNode` | inject nodes between messages — called with `0` before the first message and `i + 1` after message `i` |
171
+ | `awaitingUser` | `boolean` | suppresses the typing indicator while an interactive surface waits |
172
+
173
+ `interleave` and `awaitingUser` are the seams GenUI uses. The organism itself knows nothing about GenUI.
174
+
175
+ ### `AssistantInput`
176
+
177
+ A form with autosizing textarea, drag-and-drop and slots.
178
+
179
+ | Prop | Type | Notes |
180
+ |---|---|---|
181
+ | `input` | `string` | required |
182
+ | `handleInputChange` | `(e) => void` | required |
183
+ | `sendMessageFunc` | `(e) => Promise<void>` | required |
184
+ | `isMessageStreaming` | `boolean` | required |
185
+ | `isButtonDisabled` | `boolean` | required |
186
+ | `placeholders` | `[string, string]` | idle / streaming |
187
+ | `speechToTextComponent` | `ReactNode` | mic slot |
188
+ | `userToolboxComponent` | `ReactNode` | extra controls slot |
189
+ | `attachmentsSlot` | `ReactNode` | attachment chips |
190
+ | `onFileDrop` | `(files: File[]) => void` | enables drag-and-drop |
191
+
192
+ ---
193
+
194
+ ## Tool approval
195
+
196
+ When the runtime parks a tool call for human approval, `useAlquimia` exposes the gate in `toolApprovals` and answers it with `approveTool`. `ToolApprovalCard` renders one: the tool name, its arguments, and Approve / Reject — rejecting takes an optional reason, which the runtime keeps in its audit trail.
197
+
198
+ ```tsx
199
+ import { ToolApprovalCard } from '@alquimia-ai/ui/components/organisms';
200
+
201
+ {alquimia.toolApprovals.map((approval) => (
202
+ <ToolApprovalCard
203
+ key={approval.controlId}
204
+ approval={approval}
205
+ onDecide={alquimia.approveTool}
206
+ channelName="WhatsApp"
207
+ />
208
+ ))}
209
+ ```
210
+
211
+ The card follows `approval.status` rather than deciding anything itself, so a decision made by another operator or on the originating channel shows up as settled here too — "Approved by you" or "Approved elsewhere", with the reason for a rejection. When the gate can only be answered on its original channel, or the runtime already refused this client (already decided, no longer pending, not allowed), the buttons are replaced by an explanation; `channelName` names the channel in it, since the runtime's refusal does not.
212
+
213
+ `AssistantChat` does all of this for you when given the whole hook return: approvals render after the message they followed, the typing indicator is suppressed while one waits, and `approvalChannelName` is passed through to the card.
214
+
215
+ ---
216
+
217
+ ## GenUI
218
+
219
+ The agent answers with a **component tree**, not markup. `@alquimia-ai/tools` validates it against a catalog; this package renders it.
29
220
 
30
- - **Input**: A customizable input component.
31
- - **Select**: A customizable select component.
32
- - **Avatar**: A component for displaying user avatars.
33
- - **ScrollArea**: A component for creating scrollable areas.
34
- - **RichText**: A component for rendering rich text content.
35
- - **Skeleton**: A component for displaying skeleton loaders.
36
- - **Card**: A component for displaying card layouts.
37
- - **Drawer**: A component for creating drawers.
221
+ Batteries included:
38
222
 
223
+ ```tsx
224
+ import { AssistantChat } from '@alquimia-ai/ui/components/genui';
39
225
 
40
- ## License
226
+ const alquimia = useAlquimia({ assistantId, adapter, genui: { allow: 'all' } });
227
+
228
+ <AssistantChat alquimia={alquimia} conversationId={conversationId} />
229
+ ```
230
+
231
+ `AssistantChat` is a thin composition of `AssistantMessageArea` + `AssistantInput` — it inherits markdown, streaming and error rendering rather than reimplementing them. Props: `alquimia` (required), `registry`, `conversationId`, `placeholder`, `suggestions`, `className`, `thinkIndicator`, `approvalChannelName`. Pending tool approvals render in place — see [Tool approval](#tool-approval).
232
+
233
+ Lower level, when you own the shell:
234
+
235
+ ```tsx
236
+ import { A2uiRenderer, coreUiRegistry } from '@alquimia-ai/ui/components/genui';
237
+
238
+ <A2uiRenderer
239
+ surface={surface}
240
+ registry={coreUiRegistry}
241
+ onAgentAction={alquimia.genui.onAgentAction}
242
+ onLocalAction={(action) => { /* client-only buttons */ }}
243
+ />
244
+ ```
245
+
246
+ The renderer owns the surface's data model, resolves `{ path }` bindings, and splits actions into agent-bound (posted back to the runtime, resuming the run) and local (handled in the browser).
247
+
248
+ `useAlquimia({ genui })` returns the controller that drives all of this: `surfaces` (what to render and after which message), `onAgentAction` (posts the result and resumes the run), `awaitingUser` (an interactive surface is waiting) and `dismiss`.
249
+
250
+ ### Your own components
251
+
252
+ `coreUiRegistry` maps catalog component names to React components — roughly 40 of them:
253
+
254
+ - **layout** — `Stack`, `Row`, `Column`, `Grid`, `Divider`, `Spacer`, `Card`, `Tabs`, `Accordion`
255
+ - **content** — `Text`, `Heading`, `Markdown`, `Image`, `Icon`, `Badge`, `Avatar`, `Alert`, `Stat`, `KeyValue`, `List`, `Quote`
256
+ - **inputs** — `TextField`, `TextArea`, `NumberField`, `Select`, `MultiSelect`, `Combobox`, `Checkbox`, `CheckboxGroup`, `Radio`, `Switch`, `Slider`, `DatePicker`, `DateRange`, `FileUpload`, `Rating`
257
+ - **data** — `Table`, `Chart`, `Progress`, `Timeline`
258
+ - **actions** — `Button`, `ButtonGroup`, `Link`
259
+
260
+ To render your design system instead, pass your catalog to the hook and your registry to the renderer. Each component receives `A2uiNodeProps`:
261
+
262
+ ```tsx
263
+ import type { A2uiNodeProps } from '@alquimia-ai/ui/components/genui';
264
+
265
+ function MyTextField({ node, value, onChange }: A2uiNodeProps) {
266
+ return <input value={String(value ?? '')} onChange={(e) => onChange?.(e.target.value)} />;
267
+ }
268
+
269
+ const registry = { ...coreUiRegistry, TextField: MyTextField };
270
+ ```
271
+
272
+ `{ node, value, onChange, onAction, children }` — the node's validated props are on `node`; binding and action plumbing is already resolved.
273
+
274
+ ---
275
+
276
+ ## Speech
277
+
278
+ Both components are presentational. The actual TTS/STT call is a function you pass in, usually from `useAlquimia`'s SDK, so the vendor stays your choice.
279
+
280
+ ```tsx
281
+ import { Whisper, SpeechToText } from '@alquimia-ai/ui/components/organisms';
282
+
283
+ // Read a message out loud
284
+ <Whisper
285
+ message={message}
286
+ isMessageStreaming={alquimia.isMessageStreaming}
287
+ textToSpeech={alquimia.sdk.textToSpeech}
288
+ />
289
+
290
+ // Dictate into the input — drop into AssistantInput's speechToTextComponent slot
291
+ <SpeechToText
292
+ RecordAudioIcon={<MicOff />}
293
+ IdleAudioIcon={<Mic />}
294
+ speechToText={alquimia.sdk.speechToText}
295
+ handleReplaceInput={alquimia.handleReplaceInput}
296
+ setIsAudioRecording={alquimia.setIsAudioRecording}
297
+ />
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Ratings
303
+
304
+ `RatingDialog` (organism) plus `RatingStars`, `RatingThumbs` and `RatingComment` (molecules). Pair with `useRatings` from `@alquimia-ai/tools/hooks` and a `RatingsProvider` on the SDK.
305
+
306
+ ---
307
+
308
+ ## Documents and viewers
309
+
310
+ `DocumentSelector` and `DocumentViewer` under `/components/molecules/documents`; `PdfViewer` and `PlainTextViewer` under `/components/molecules/viewers`. Useful for rendering knowledge-base citations and uploaded attachments.
311
+
312
+ ---
313
+
314
+ ## Component inventory
315
+
316
+ **Atoms** — `Button`, `Input`, `Textarea`, `Label`, `Select`, `Checkbox`, `RadioGroup`, `Switch`, `Toggle`, `Slider`, `Calendar`, `Avatar`, `Badge`, `Alert`, `Card`, `Separator`, `Skeleton`, `Loader`, `Progress`, `Typography`, `RichText`, `ScrollArea`, `AspectRatio`, `Table`, `Tabs`, `Accordion`, `Breadcrumb`, `Popover`, `Dialog`, `Drawer`, `Command`, `Toast`, `Toaster`, `Chart`, `ThinkIndicator`.
317
+
318
+ **Molecules** — `PageContainer`, `AlertDialog`, `AssistantButton`, `Carousel`, `NavigationMenu`, `Sidebar`, `Sonner`, `CallOut`, rating and document components.
319
+
320
+ **Organisms** — `Assistant`, `AssistantMessageArea`, `AssistantInput`, `ToolApprovalCard`, `Whisper`, `SpeechToText`, `RatingDialog`.
321
+
322
+ **Templates** — `MessagesWindow`, `QueryBox`, `Hero`, `Cards`.
323
+
324
+ `Sidebar` and `Drawer` are what the reasoning/thinkings panel is built from, driven by `alquimia.hasThinkings` and the message `tooler` field.
325
+
326
+ ---
327
+
328
+ ## Hooks and utilities
329
+
330
+ | Export | Purpose |
331
+ |---|---|
332
+ | `useToast` | toast queue behind `Toaster` |
333
+ | `useDocument` | document viewer state |
334
+ | `useResizeObserver` | element size tracking |
335
+ | `useTextStreaming` | typewriter rendering for streamed text |
336
+ | `cn` | `clsx` + `tailwind-merge` class merge |
337
+ | `parseTextToSpeech` | strips characters that make TTS engines stumble |
338
+ | `blobToBase64` | file helper |
339
+
340
+ ---
341
+
342
+ ## Export map
343
+
344
+ | Entry | Contents |
345
+ |---|---|
346
+ | `.` | everything re-exported |
347
+ | `./components/atoms` | primitives |
348
+ | `./components/molecules` | composed components |
349
+ | `./components/molecules/viewers` | PDF / plain-text viewers |
350
+ | `./components/molecules/documents` | document selector + viewer |
351
+ | `./components/organisms` | chat, tool approval, speech, ratings |
352
+ | `./components/templates` | page-level layouts |
353
+ | `./components/genui` | `A2uiRenderer`, `coreUiRegistry`, `AssistantChat` |
354
+ | `./components/hooks` | UI hooks |
355
+ | `./providers` | `AlquimiaUIProvider`, `useAlquimiaTheme` |
356
+ | `./lib/utils` | `cn` and helpers |
357
+ | `./types` | shared component types |
358
+ | `./styles/*`, `./styles/themes/*` | stylesheets |
359
+ | `./tailwind.config` | the Tailwind preset |
360
+ | `./styles.css` | bundled base styles |
361
+
362
+ ---
363
+
364
+ ## Development
365
+
366
+ ```sh
367
+ yarn test # vitest
368
+ yarn lint # eslint --max-warnings 0
369
+ yarn build # tsup
370
+ ```
41
371
 
42
- This project is licensed under the MIT License. See the LICENSE file for more details.
372
+ Storybook lives in [`apps/storybook`](../../apps/storybook). Changes to published behaviour need a changeset — see [`docs/how-to-use-changeset.md`](../../docs/how-to-use-changeset.md).
@@ -1,6 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { A2uiComponent, SurfaceAction, A2uiSurface, UIAction } from '@alquimia-ai/tools/genui';
4
+ import { b as ToolApprovalView } from '../../tool-approval-K8aLzb1_.mjs';
5
+ import '@alquimia-ai/tools/hooks';
4
6
 
5
7
  /** Props every A2UI component receives from the renderer. */
6
8
  interface A2uiNodeProps {
@@ -48,6 +50,11 @@ interface GenuiChatController {
48
50
  awaitingUser: boolean;
49
51
  dismiss?: (userMessage?: string) => void;
50
52
  };
53
+ /** Human-approval gates (`useAlquimia().toolApprovals`), rendered in place with Approve / Reject. */
54
+ toolApprovals?: Array<ToolApprovalView & {
55
+ afterCount: number;
56
+ }>;
57
+ approveTool?: (controlId: string, approved: boolean, message?: string) => unknown;
51
58
  }
52
59
  interface AssistantChatProps {
53
60
  /** The value returned by `useAlquimia({ genui: {...} })`. */
@@ -61,6 +68,8 @@ interface AssistantChatProps {
61
68
  className?: string;
62
69
  /** Override the typing indicator shown while the agent is working. Defaults to animated dots. */
63
70
  thinkIndicator?: React.ReactNode;
71
+ /** Channel name shown when an approval can only be answered on its originating channel. */
72
+ approvalChannelName?: string;
64
73
  }
65
74
  /**
66
75
  * Batteries-included GenUI chat. A thin composition of the SDK's existing organisms
@@ -74,6 +83,6 @@ interface AssistantChatProps {
74
83
  * the hook). To customize the shell, compose the organisms directly instead of using
75
84
  * this wrapper.
76
85
  */
77
- declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
86
+ declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, approvalChannelName, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
78
87
 
79
88
  export { type A2uiNodeProps, A2uiRenderer, type A2uiRendererProps, AssistantChat, type AssistantChatProps, type GenuiChatController, coreUiRegistry };
@@ -1,6 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { A2uiComponent, SurfaceAction, A2uiSurface, UIAction } from '@alquimia-ai/tools/genui';
4
+ import { b as ToolApprovalView } from '../../tool-approval-K8aLzb1_.js';
5
+ import '@alquimia-ai/tools/hooks';
4
6
 
5
7
  /** Props every A2UI component receives from the renderer. */
6
8
  interface A2uiNodeProps {
@@ -48,6 +50,11 @@ interface GenuiChatController {
48
50
  awaitingUser: boolean;
49
51
  dismiss?: (userMessage?: string) => void;
50
52
  };
53
+ /** Human-approval gates (`useAlquimia().toolApprovals`), rendered in place with Approve / Reject. */
54
+ toolApprovals?: Array<ToolApprovalView & {
55
+ afterCount: number;
56
+ }>;
57
+ approveTool?: (controlId: string, approved: boolean, message?: string) => unknown;
51
58
  }
52
59
  interface AssistantChatProps {
53
60
  /** The value returned by `useAlquimia({ genui: {...} })`. */
@@ -61,6 +68,8 @@ interface AssistantChatProps {
61
68
  className?: string;
62
69
  /** Override the typing indicator shown while the agent is working. Defaults to animated dots. */
63
70
  thinkIndicator?: React.ReactNode;
71
+ /** Channel name shown when an approval can only be answered on its originating channel. */
72
+ approvalChannelName?: string;
64
73
  }
65
74
  /**
66
75
  * Batteries-included GenUI chat. A thin composition of the SDK's existing organisms
@@ -74,6 +83,6 @@ interface AssistantChatProps {
74
83
  * the hook). To customize the shell, compose the organisms directly instead of using
75
84
  * this wrapper.
76
85
  */
77
- declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
86
+ declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, approvalChannelName, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
78
87
 
79
88
  export { type A2uiNodeProps, A2uiRenderer, type A2uiRendererProps, AssistantChat, type AssistantChatProps, type GenuiChatController, coreUiRegistry };