@alquimia-ai/ui 2.2.1 → 2.2.4
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 +330 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,42 +1,348 @@
|
|
|
1
1
|
# @alquimia-ai/ui
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
It holds no runtime logic. State comes from [`@alquimia-ai/tools`](../tools) — typically `useAlquimia` — and these components render it.
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
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
|
+
- [GenUI](#genui)
|
|
19
|
+
- [Speech](#speech)
|
|
20
|
+
- [Ratings](#ratings)
|
|
21
|
+
- [Documents and viewers](#documents-and-viewers)
|
|
22
|
+
- [Component inventory](#component-inventory)
|
|
23
|
+
- [Hooks and utilities](#hooks-and-utilities)
|
|
24
|
+
- [Export map](#export-map)
|
|
10
25
|
- [Development](#development)
|
|
11
|
-
- [Contributing](#contributing)
|
|
12
|
-
- [License](#license)
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
---
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
## Install and configure
|
|
17
30
|
|
|
18
31
|
```sh
|
|
19
|
-
|
|
32
|
+
npm install @alquimia-ai/tools @alquimia-ai/ui
|
|
33
|
+
npm install tailwindcss tailwindcss-animate
|
|
34
|
+
npm install framer-motion lucide-react # usually transitive
|
|
20
35
|
```
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
### Tailwind
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
Use the package as a **preset**. One line replaces every colour, radius and animation token:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
/** @type {import('tailwindcss').Config} */
|
|
43
|
+
module.exports = {
|
|
44
|
+
presets: [require('@alquimia-ai/ui/tailwind.config')],
|
|
45
|
+
content: [
|
|
46
|
+
'./app/**/*.{ts,tsx}',
|
|
47
|
+
'./components/**/*.{ts,tsx}',
|
|
48
|
+
'./src/**/*.{ts,tsx}',
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The preset already contributes the UI package's `dist` path to `content`, all token mappings, and the `tailwindcss-animate` plugin.
|
|
54
|
+
|
|
55
|
+
### CSS
|
|
56
|
+
|
|
57
|
+
Import exactly one theme:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import '@alquimia-ai/ui/styles/themes/base.css'; // neutral light + dark
|
|
61
|
+
import '@alquimia-ai/ui/styles/themes/base-alquimia.css'; // Alquimia brand
|
|
62
|
+
import '@alquimia-ai/ui/styles/themes/base-nordic.css';
|
|
63
|
+
import '@alquimia-ai/ui/styles/themes/base-primary.css';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Optional stylesheets for specific surfaces: `styles/globals.css`, `prose.css`, `call-out.css`, `drawer.css`, `ratings.css`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Theming
|
|
71
|
+
|
|
72
|
+
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:
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
@layer base {
|
|
76
|
+
:root {
|
|
77
|
+
--primary: 223 82% 60%;
|
|
78
|
+
--primary-foreground: 0 0% 98%;
|
|
79
|
+
--background: 0 0% 95%;
|
|
80
|
+
--foreground: 0 0% 3.9%;
|
|
81
|
+
--card: 0 0% 100%;
|
|
82
|
+
--muted: 0 0% 96.1%;
|
|
83
|
+
--muted-foreground: 0 0% 45.1%;
|
|
84
|
+
--border: 0 0% 85%;
|
|
85
|
+
--ring: 221 83% 53%;
|
|
86
|
+
--radius: 0.75rem;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
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:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { AlquimiaUIProvider } from '@alquimia-ai/ui';
|
|
95
|
+
|
|
96
|
+
<AlquimiaUIProvider defaultMode="system"> {/* 'light' | 'dark' | 'system' */}
|
|
97
|
+
{children}
|
|
98
|
+
</AlquimiaUIProvider>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`useAlquimiaTheme()` reads the resolved theme and lets you set it.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Building a chat
|
|
106
|
+
|
|
107
|
+
The library favours composition over a monolith: an area that renders messages, a form that sends them, and slots for everything optional.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
'use client';
|
|
111
|
+
import { useRef } from 'react';
|
|
112
|
+
import { useAlquimia } from '@alquimia-ai/tools/hooks';
|
|
113
|
+
import { createNextJsAdapter } from '@alquimia-ai/tools/adapters/next';
|
|
114
|
+
import { AssistantMessageArea, AssistantInput } from '@alquimia-ai/ui/components/organisms';
|
|
115
|
+
import { ThinkIndicator } from '@alquimia-ai/ui/components/atoms';
|
|
116
|
+
|
|
117
|
+
const adapter = createNextJsAdapter();
|
|
118
|
+
|
|
119
|
+
export function Chat({ assistantId, conversationId }: Props) {
|
|
120
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
121
|
+
const alquimia = useAlquimia({ assistantId, adapter });
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex h-full flex-col">
|
|
125
|
+
<AssistantMessageArea
|
|
126
|
+
messages={alquimia.messages.filter((m) => m.role !== 'system')}
|
|
127
|
+
messagesEndRef={messagesEndRef}
|
|
128
|
+
isLoading={alquimia.isMessageLoading}
|
|
129
|
+
isMessageStreaming={alquimia.isMessageStreaming}
|
|
130
|
+
streamingMessageId={alquimia.streamingMessageId}
|
|
131
|
+
thinkIndicator={<ThinkIndicator thoughts={['Thinking…']} />}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<AssistantInput
|
|
135
|
+
input={alquimia.input}
|
|
136
|
+
handleInputChange={alquimia.handleInputChange}
|
|
137
|
+
sendMessageFunc={(e) => alquimia.handleSubmit(e, undefined, conversationId)}
|
|
138
|
+
isMessageStreaming={alquimia.isMessageStreaming}
|
|
139
|
+
isButtonDisabled={!alquimia.input.trim()}
|
|
140
|
+
onFileDrop={alquimia.addAttachments}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
26
145
|
```
|
|
27
146
|
|
|
28
|
-
|
|
147
|
+
`Assistant` wraps this in a shell with an optional title and description, and exports `AssistantHeader`, `AssistantTitle` and `AssistantDescription` for custom headers.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Chat organisms
|
|
152
|
+
|
|
153
|
+
### `AssistantMessageArea`
|
|
154
|
+
|
|
155
|
+
Renders the conversation: markdown, streaming text, timestamps, error states, tool cards and per-message actions.
|
|
156
|
+
|
|
157
|
+
| Prop | Type | Notes |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `messages` | `AlquimiaMessage[]` | required |
|
|
160
|
+
| `messagesEndRef` | ref | scroll anchor |
|
|
161
|
+
| `isLoading` | `boolean` | required |
|
|
162
|
+
| `isMessageStreaming` | `boolean` | |
|
|
163
|
+
| `streamingMessageId` | `string \| null` | required — which bubble is live |
|
|
164
|
+
| `thinkIndicator` | `ReactNode` | replaces the default typing bubble |
|
|
165
|
+
| `actions` | `MessageAction[]` | per-message buttons (copy, rate, speak…) |
|
|
166
|
+
| `toolFactory` | `ToolFactory` | renders tool events inline |
|
|
167
|
+
| `showDetailedErrors` | `boolean` | surfaces `error_code` / `error_detail` |
|
|
168
|
+
| `handleIsTextStreaming` | `(streaming) => void` | typewriter lifecycle |
|
|
169
|
+
| `interleave` | `(afterIndex) => ReactNode` | inject nodes between messages — called with `0` before the first message and `i + 1` after message `i` |
|
|
170
|
+
| `awaitingUser` | `boolean` | suppresses the typing indicator while an interactive surface waits |
|
|
29
171
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
172
|
+
`interleave` and `awaitingUser` are the seams GenUI uses. The organism itself knows nothing about GenUI.
|
|
173
|
+
|
|
174
|
+
### `AssistantInput`
|
|
175
|
+
|
|
176
|
+
A form with autosizing textarea, drag-and-drop and slots.
|
|
177
|
+
|
|
178
|
+
| Prop | Type | Notes |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `input` | `string` | required |
|
|
181
|
+
| `handleInputChange` | `(e) => void` | required |
|
|
182
|
+
| `sendMessageFunc` | `(e) => Promise<void>` | required |
|
|
183
|
+
| `isMessageStreaming` | `boolean` | required |
|
|
184
|
+
| `isButtonDisabled` | `boolean` | required |
|
|
185
|
+
| `placeholders` | `[string, string]` | idle / streaming |
|
|
186
|
+
| `speechToTextComponent` | `ReactNode` | mic slot |
|
|
187
|
+
| `userToolboxComponent` | `ReactNode` | extra controls slot |
|
|
188
|
+
| `attachmentsSlot` | `ReactNode` | attachment chips |
|
|
189
|
+
| `onFileDrop` | `(files: File[]) => void` | enables drag-and-drop |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## GenUI
|
|
194
|
+
|
|
195
|
+
The agent answers with a **component tree**, not markup. `@alquimia-ai/tools` validates it against a catalog; this package renders it.
|
|
196
|
+
|
|
197
|
+
Batteries included:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { AssistantChat } from '@alquimia-ai/ui/components/genui';
|
|
201
|
+
|
|
202
|
+
const alquimia = useAlquimia({ assistantId, adapter, genui: { allow: 'all' } });
|
|
203
|
+
|
|
204
|
+
<AssistantChat alquimia={alquimia} conversationId={conversationId} />
|
|
205
|
+
```
|
|
38
206
|
|
|
207
|
+
`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`.
|
|
39
208
|
|
|
40
|
-
|
|
209
|
+
Lower level, when you own the shell:
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { A2uiRenderer, coreUiRegistry } from '@alquimia-ai/ui/components/genui';
|
|
213
|
+
|
|
214
|
+
<A2uiRenderer
|
|
215
|
+
surface={surface}
|
|
216
|
+
registry={coreUiRegistry}
|
|
217
|
+
onAgentAction={alquimia.genui.onAgentAction}
|
|
218
|
+
onLocalAction={(action) => { /* client-only buttons */ }}
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
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).
|
|
223
|
+
|
|
224
|
+
`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`.
|
|
225
|
+
|
|
226
|
+
### Your own components
|
|
227
|
+
|
|
228
|
+
`coreUiRegistry` maps catalog component names to React components — roughly 40 of them:
|
|
229
|
+
|
|
230
|
+
- **layout** — `Stack`, `Row`, `Column`, `Grid`, `Divider`, `Spacer`, `Card`, `Tabs`, `Accordion`
|
|
231
|
+
- **content** — `Text`, `Heading`, `Markdown`, `Image`, `Icon`, `Badge`, `Avatar`, `Alert`, `Stat`, `KeyValue`, `List`, `Quote`
|
|
232
|
+
- **inputs** — `TextField`, `TextArea`, `NumberField`, `Select`, `MultiSelect`, `Combobox`, `Checkbox`, `CheckboxGroup`, `Radio`, `Switch`, `Slider`, `DatePicker`, `DateRange`, `FileUpload`, `Rating`
|
|
233
|
+
- **data** — `Table`, `Chart`, `Progress`, `Timeline`
|
|
234
|
+
- **actions** — `Button`, `ButtonGroup`, `Link`
|
|
235
|
+
|
|
236
|
+
To render your design system instead, pass your catalog to the hook and your registry to the renderer. Each component receives `A2uiNodeProps`:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import type { A2uiNodeProps } from '@alquimia-ai/ui/components/genui';
|
|
240
|
+
|
|
241
|
+
function MyTextField({ node, value, onChange }: A2uiNodeProps) {
|
|
242
|
+
return <input value={String(value ?? '')} onChange={(e) => onChange?.(e.target.value)} />;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const registry = { ...coreUiRegistry, TextField: MyTextField };
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`{ node, value, onChange, onAction, children }` — the node's validated props are on `node`; binding and action plumbing is already resolved.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Speech
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
import { Whisper, SpeechToText } from '@alquimia-ai/ui/components/organisms';
|
|
258
|
+
|
|
259
|
+
// Read a message out loud
|
|
260
|
+
<Whisper
|
|
261
|
+
message={message}
|
|
262
|
+
isMessageStreaming={alquimia.isMessageStreaming}
|
|
263
|
+
textToSpeech={alquimia.sdk.textToSpeech}
|
|
264
|
+
/>
|
|
265
|
+
|
|
266
|
+
// Dictate into the input — drop into AssistantInput's speechToTextComponent slot
|
|
267
|
+
<SpeechToText
|
|
268
|
+
RecordAudioIcon={<MicOff />}
|
|
269
|
+
IdleAudioIcon={<Mic />}
|
|
270
|
+
speechToText={alquimia.sdk.speechToText}
|
|
271
|
+
handleReplaceInput={alquimia.handleReplaceInput}
|
|
272
|
+
setIsAudioRecording={alquimia.setIsAudioRecording}
|
|
273
|
+
/>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Ratings
|
|
279
|
+
|
|
280
|
+
`RatingDialog` (organism) plus `RatingStars`, `RatingThumbs` and `RatingComment` (molecules). Pair with `useRatings` from `@alquimia-ai/tools/hooks` and a `RatingsProvider` on the SDK.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Documents and viewers
|
|
285
|
+
|
|
286
|
+
`DocumentSelector` and `DocumentViewer` under `/components/molecules/documents`; `PdfViewer` and `PlainTextViewer` under `/components/molecules/viewers`. Useful for rendering knowledge-base citations and uploaded attachments.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Component inventory
|
|
291
|
+
|
|
292
|
+
**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`.
|
|
293
|
+
|
|
294
|
+
**Molecules** — `PageContainer`, `AlertDialog`, `AssistantButton`, `Carousel`, `NavigationMenu`, `Sidebar`, `Sonner`, `CallOut`, rating and document components.
|
|
295
|
+
|
|
296
|
+
**Organisms** — `Assistant`, `AssistantMessageArea`, `AssistantInput`, `Whisper`, `SpeechToText`, `RatingDialog`.
|
|
297
|
+
|
|
298
|
+
**Templates** — `MessagesWindow`, `QueryBox`, `Hero`, `Cards`.
|
|
299
|
+
|
|
300
|
+
`Sidebar` and `Drawer` are what the reasoning/thinkings panel is built from, driven by `alquimia.hasThinkings` and the message `tooler` field.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Hooks and utilities
|
|
305
|
+
|
|
306
|
+
| Export | Purpose |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `useToast` | toast queue behind `Toaster` |
|
|
309
|
+
| `useDocument` | document viewer state |
|
|
310
|
+
| `useResizeObserver` | element size tracking |
|
|
311
|
+
| `useTextStreaming` | typewriter rendering for streamed text |
|
|
312
|
+
| `cn` | `clsx` + `tailwind-merge` class merge |
|
|
313
|
+
| `parseTextToSpeech` | strips characters that make TTS engines stumble |
|
|
314
|
+
| `blobToBase64` | file helper |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Export map
|
|
319
|
+
|
|
320
|
+
| Entry | Contents |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `.` | everything re-exported |
|
|
323
|
+
| `./components/atoms` | primitives |
|
|
324
|
+
| `./components/molecules` | composed components |
|
|
325
|
+
| `./components/molecules/viewers` | PDF / plain-text viewers |
|
|
326
|
+
| `./components/molecules/documents` | document selector + viewer |
|
|
327
|
+
| `./components/organisms` | chat, speech, ratings |
|
|
328
|
+
| `./components/templates` | page-level layouts |
|
|
329
|
+
| `./components/genui` | `A2uiRenderer`, `coreUiRegistry`, `AssistantChat` |
|
|
330
|
+
| `./components/hooks` | UI hooks |
|
|
331
|
+
| `./providers` | `AlquimiaUIProvider`, `useAlquimiaTheme` |
|
|
332
|
+
| `./lib/utils` | `cn` and helpers |
|
|
333
|
+
| `./types` | shared component types |
|
|
334
|
+
| `./styles/*`, `./styles/themes/*` | stylesheets |
|
|
335
|
+
| `./tailwind.config` | the Tailwind preset |
|
|
336
|
+
| `./styles.css` | bundled base styles |
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Development
|
|
341
|
+
|
|
342
|
+
```sh
|
|
343
|
+
yarn test # vitest
|
|
344
|
+
yarn lint # eslint --max-warnings 0
|
|
345
|
+
yarn build # tsup
|
|
346
|
+
```
|
|
41
347
|
|
|
42
|
-
|
|
348
|
+
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).
|