@bitforgehq/ui 0.4.0 → 0.5.0-next.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/CHANGELOG.md +350 -0
- package/README.md +12 -12
- package/ai/MANIFEST.index.json +74 -2
- package/ai/MANIFEST.json +3507 -1763
- package/ai/REFERENCE.index.json +189 -0
- package/ai/REFERENCE.json +1617 -139
- package/ai/UI_GUIDE.md +70 -20
- package/ai/components/app-frame.md +1 -1
- package/ai/components/badge.md +41 -1
- package/ai/components/chat-composer.md +357 -0
- package/ai/components/chat-message.md +251 -0
- package/ai/components/chat-thread.md +521 -0
- package/ai/components/checkbox.md +7 -0
- package/ai/components/combobox.md +12 -0
- package/ai/components/date-input.md +9 -0
- package/ai/components/datetime-input.md +2 -0
- package/ai/components/field.md +10 -0
- package/ai/components/file-input.md +498 -0
- package/ai/components/header.md +1 -1
- package/ai/components/image-input.md +368 -0
- package/ai/components/image-tile.md +452 -0
- package/ai/components/image-viewer.md +309 -0
- package/ai/components/media-card.md +478 -0
- package/ai/components/menu-button.md +67 -9
- package/ai/components/menu.md +10 -0
- package/ai/components/multi-combobox.md +7 -0
- package/ai/components/nav-group.md +10 -3
- package/ai/components/nav-item.md +11 -2
- package/ai/components/number-input.md +2 -7
- package/ai/components/pagination.md +59 -11
- package/ai/components/popover.md +13 -0
- package/ai/components/radio-group.md +6 -0
- package/ai/components/segmented.md +14 -2
- package/ai/components/select.md +100 -42
- package/ai/components/sidebar-toggle.md +16 -9
- package/ai/components/sidebar.md +41 -13
- package/ai/components/tab-group.md +18 -2
- package/ai/components/table.md +17 -2
- package/ai/components/theme-menu-button.md +1 -0
- package/ai/components/time-input.md +2 -0
- package/ai/components/toolbar.md +7 -18
- package/ai/reference/bf-chat-composer-harness.md +64 -0
- package/ai/reference/bf-chat-message-role.md +27 -0
- package/ai/reference/bf-chat-message-status.md +28 -0
- package/ai/reference/bf-fade-in-directive.md +18 -0
- package/ai/reference/bf-file-input-harness.md +67 -0
- package/ai/reference/bf-file-input-variant.md +27 -0
- package/ai/reference/bf-image-input-harness.md +67 -0
- package/ai/reference/bf-image-input-shape.md +27 -0
- package/ai/reference/bf-image-tile-aspect.md +28 -0
- package/ai/reference/bf-image-tile-caption-placement.md +27 -0
- package/ai/reference/bf-image-tile-harness.md +70 -0
- package/ai/reference/bf-image-viewer-actions-context.md +29 -0
- package/ai/reference/bf-image-viewer-harness.md +87 -0
- package/ai/reference/bf-image-viewer-image.md +25 -0
- package/ai/reference/bf-image-viewer-options.md +33 -0
- package/ai/reference/bf-image-viewer-origin.md +23 -0
- package/ai/reference/bf-image-viewer-ref.md +55 -0
- package/ai/reference/bf-image-viewer-service.md +32 -0
- package/ai/reference/bf-labels.md +107 -75
- package/ai/reference/bf-media-card-aspect.md +28 -0
- package/ai/reference/bf-media-card-harness.md +66 -0
- package/ai/reference/bf-motion-list-directive.md +22 -0
- package/ai/reference/bf-pagination-position-label.md +22 -0
- package/ai/reference/bf-prose-directive.md +16 -0
- package/ai/reference/bf-rejected-file-reason.md +28 -0
- package/ai/reference/bf-rejected-file.md +28 -0
- package/ai/reference/bf-select-harness.md +14 -5
- package/ai/reference/bf-select-option.md +2 -0
- package/ai/reference/bf-size.md +3 -0
- package/ai/reference/bf_table_sort_select.md +23 -0
- package/ai/reference/provide-bf-labels.md +11 -3
- package/ai/reference/provide-bf-select.md +20 -0
- package/bin/bf-ui.js +1 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs +2660 -594
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +414 -10
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +8056 -3993
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +4923 -132
- package/llms.txt +36 -1
- package/package.json +1 -1
- package/theme.css +60 -0
- package/types/bitforgehq-ui-overlay.d.ts +695 -7
- package/types/bitforgehq-ui-testing.d.ts +517 -6
- package/types/bitforgehq-ui.d.ts +1699 -181
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# bf-chat-composer
|
|
2
|
+
|
|
3
|
+
Chat input that grows with the text and sends on Enter, or stops a reply
|
|
4
|
+
|
|
5
|
+
`BfChatComposerComponent` from `@bitforgehq/ui/overlay` · Forms · since 0.5.0
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, DestroyRef, inject, signal } from '@angular/core';
|
|
11
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
12
|
+
import { BfChatComposerComponent } from '@bitforgehq/ui/overlay';
|
|
13
|
+
import { ArrowUp, Square } from 'lucide';
|
|
14
|
+
|
|
15
|
+
/* How long the pretend reply runs before it finishes by itself. */
|
|
16
|
+
const REPLY_TIME = 3000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The bf-chat-composer overview.
|
|
20
|
+
*
|
|
21
|
+
* Type and press Enter (Shift+Enter for a new line) or the send button: the text is sent, the
|
|
22
|
+
* field clears, and the send button turns into Stop while the pretend reply runs. Stop ends it
|
|
23
|
+
* early. Icons go in the `send-icon` and `stop-icon` slots; their names stay as text.
|
|
24
|
+
*/
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-example-chat-composer-overview',
|
|
27
|
+
imports: [BfChatComposerComponent, BfIconComponent],
|
|
28
|
+
template: `
|
|
29
|
+
<div class="flex flex-col gap-3">
|
|
30
|
+
<bf-chat-composer
|
|
31
|
+
placeholder="Ask for ideas"
|
|
32
|
+
[(value)]="draft"
|
|
33
|
+
[running]="running()"
|
|
34
|
+
(sent)="send($event)"
|
|
35
|
+
(stopped)="finish('Stopped.')"
|
|
36
|
+
>
|
|
37
|
+
<bf-icon bfSlot="send-icon" [icon]="arrowUp" />
|
|
38
|
+
<bf-icon bfSlot="stop-icon" [icon]="square" />
|
|
39
|
+
</bf-chat-composer>
|
|
40
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
41
|
+
</div>
|
|
42
|
+
`,
|
|
43
|
+
})
|
|
44
|
+
export class ChatComposerOverviewExampleComponent {
|
|
45
|
+
protected readonly arrowUp = ArrowUp;
|
|
46
|
+
protected readonly square = Square;
|
|
47
|
+
|
|
48
|
+
readonly draft = signal('');
|
|
49
|
+
readonly running = signal(false);
|
|
50
|
+
readonly status = signal('Nothing sent yet.');
|
|
51
|
+
|
|
52
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
inject(DestroyRef).onDestroy(() => this.clearTimer());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
send(text: string): void {
|
|
59
|
+
this.running.set(true);
|
|
60
|
+
this.status.set(`Sent: "${text}". Replying...`);
|
|
61
|
+
this.timer = setTimeout(() => this.finish('Reply done.'), REPLY_TIME);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
finish(status: string): void {
|
|
65
|
+
this.clearTimer();
|
|
66
|
+
this.running.set(false);
|
|
67
|
+
this.status.set(status);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private clearTimer(): void {
|
|
71
|
+
if (this.timer !== null) {
|
|
72
|
+
clearTimeout(this.timer);
|
|
73
|
+
this.timer = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Overview
|
|
80
|
+
|
|
81
|
+
The input row of a chat: a `bf-textarea` that grows with the text (`autoResize`, up to
|
|
82
|
+
`maxRows`, then it scrolls), a start and an end slot for extra controls, and one button that
|
|
83
|
+
sends the text or, while a reply is `running`, stops it. `Enter` sends and `Shift+Enter`
|
|
84
|
+
starts a new line; nothing sends during IME composition or
|
|
85
|
+
when the text is only whitespace. After a send the text clears and focus stays in the field.
|
|
86
|
+
`disabled` with a `disabledReason` shows the reason as the button's tooltip and as the empty
|
|
87
|
+
field's placeholder.
|
|
88
|
+
|
|
89
|
+
The input row of a chat: a text field that grows with the text, slots for extra controls, and one button that sends the text or stops the running reply. Import it from `@bitforgehq/ui/overlay` (it needs `@angular/cdk` for the disabled reason's tooltip).
|
|
90
|
+
|
|
91
|
+
## When to use
|
|
92
|
+
|
|
93
|
+
Use `bf-chat-composer` below a [bf-chat-thread](https://bitforge-ui.web.app/components/chat-thread) to write to an assistant. Use [bf-textarea](https://bitforge-ui.web.app/components/textarea) instead for free text in a form that is saved with the form, such as a comment field.
|
|
94
|
+
|
|
95
|
+
## Basic usage
|
|
96
|
+
|
|
97
|
+
Bind the text with `[(value)]` and handle `(sent)`, which emits the trimmed text; the field then clears and keeps focus. <kbd>Enter</kbd> sends and <kbd>Shift</kbd>+<kbd>Enter</kbd> starts a new line. Nothing sends while an input method is composing (Japanese, Chinese or Korean input confirm their text with <kbd>Enter</kbd>), and empty or whitespace-only text never sends: the send button is disabled then. `(changed)` emits the text after each edit by the user.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { Component, DestroyRef, inject, signal } from '@angular/core';
|
|
101
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
102
|
+
import { BfChatComposerComponent } from '@bitforgehq/ui/overlay';
|
|
103
|
+
import { ArrowUp, Square } from 'lucide';
|
|
104
|
+
|
|
105
|
+
/* How long the pretend reply runs before it finishes by itself. */
|
|
106
|
+
const REPLY_TIME = 3000;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The bf-chat-composer overview.
|
|
110
|
+
*
|
|
111
|
+
* Type and press Enter (Shift+Enter for a new line) or the send button: the text is sent, the
|
|
112
|
+
* field clears, and the send button turns into Stop while the pretend reply runs. Stop ends it
|
|
113
|
+
* early. Icons go in the `send-icon` and `stop-icon` slots; their names stay as text.
|
|
114
|
+
*/
|
|
115
|
+
@Component({
|
|
116
|
+
selector: 'app-example-chat-composer-overview',
|
|
117
|
+
imports: [BfChatComposerComponent, BfIconComponent],
|
|
118
|
+
template: `
|
|
119
|
+
<div class="flex flex-col gap-3">
|
|
120
|
+
<bf-chat-composer
|
|
121
|
+
placeholder="Ask for ideas"
|
|
122
|
+
[(value)]="draft"
|
|
123
|
+
[running]="running()"
|
|
124
|
+
(sent)="send($event)"
|
|
125
|
+
(stopped)="finish('Stopped.')"
|
|
126
|
+
>
|
|
127
|
+
<bf-icon bfSlot="send-icon" [icon]="arrowUp" />
|
|
128
|
+
<bf-icon bfSlot="stop-icon" [icon]="square" />
|
|
129
|
+
</bf-chat-composer>
|
|
130
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
131
|
+
</div>
|
|
132
|
+
`,
|
|
133
|
+
})
|
|
134
|
+
export class ChatComposerOverviewExampleComponent {
|
|
135
|
+
protected readonly arrowUp = ArrowUp;
|
|
136
|
+
protected readonly square = Square;
|
|
137
|
+
|
|
138
|
+
readonly draft = signal('');
|
|
139
|
+
readonly running = signal(false);
|
|
140
|
+
readonly status = signal('Nothing sent yet.');
|
|
141
|
+
|
|
142
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
143
|
+
|
|
144
|
+
constructor() {
|
|
145
|
+
inject(DestroyRef).onDestroy(() => this.clearTimer());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
send(text: string): void {
|
|
149
|
+
this.running.set(true);
|
|
150
|
+
this.status.set(`Sent: "${text}". Replying...`);
|
|
151
|
+
this.timer = setTimeout(() => this.finish('Reply done.'), REPLY_TIME);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
finish(status: string): void {
|
|
155
|
+
this.clearTimer();
|
|
156
|
+
this.running.set(false);
|
|
157
|
+
this.status.set(status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private clearTimer(): void {
|
|
161
|
+
if (this.timer !== null) {
|
|
162
|
+
clearTimeout(this.timer);
|
|
163
|
+
this.timer = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Send and stop
|
|
170
|
+
|
|
171
|
+
While `running` is true (the app's reply is streaming) the button is Stop: it emits `(stopped)` and the app ends the reply and sets `running` back to false. <kbd>Enter</kbd> does not send while a reply runs, and the text stays in the field. The button keeps its place and focus while it switches.
|
|
172
|
+
|
|
173
|
+
## Icons
|
|
174
|
+
|
|
175
|
+
The `send-icon` and `stop-icon` slots take the button's icons (the library never imports an icon into a widget). With an icon the button is a round icon button and its text ("Send", "Stop", from `chatSend` and `chatStop`) stays as its accessible name; without one the button shows the text.
|
|
176
|
+
|
|
177
|
+
## Extra controls
|
|
178
|
+
|
|
179
|
+
The `start` slot (before the field) and the `end` slot (between the field and the button) take controls of the app, such as a preset picker or an attach button. They line up with the bottom of the field as it grows.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { Component, signal } from '@angular/core';
|
|
183
|
+
import { BfIconButtonComponent, type BfSelectOption } from '@bitforgehq/ui';
|
|
184
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
185
|
+
import { BfChatComposerComponent, BfSelectComponent } from '@bitforgehq/ui/overlay';
|
|
186
|
+
import { Paperclip } from 'lucide';
|
|
187
|
+
|
|
188
|
+
type Preset = 'names' | 'slogans' | 'menu';
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extra controls in the start and end slots.
|
|
192
|
+
*
|
|
193
|
+
* A preset picker in the `start` slot and an attach button in the `end` slot, beside the field;
|
|
194
|
+
* without icons in its slots the send button shows its text. The sent line names the preset the
|
|
195
|
+
* message went out with.
|
|
196
|
+
*/
|
|
197
|
+
@Component({
|
|
198
|
+
selector: 'app-example-chat-composer-slots',
|
|
199
|
+
imports: [BfChatComposerComponent, BfSelectComponent, BfIconButtonComponent, BfIconComponent],
|
|
200
|
+
template: `
|
|
201
|
+
<div class="flex flex-col gap-3">
|
|
202
|
+
<bf-chat-composer placeholder="Describe the cafe" (sent)="sent.set(preset() + ': ' + $event)">
|
|
203
|
+
<bf-select
|
|
204
|
+
bfSlot="start"
|
|
205
|
+
ariaLabel="Preset"
|
|
206
|
+
class="w-36"
|
|
207
|
+
[options]="presets"
|
|
208
|
+
[(value)]="preset"
|
|
209
|
+
/>
|
|
210
|
+
<bf-icon-button bfSlot="end" variant="text" label="Attach a file">
|
|
211
|
+
<bf-icon [icon]="paperclip" />
|
|
212
|
+
</bf-icon-button>
|
|
213
|
+
</bf-chat-composer>
|
|
214
|
+
<p data-sent class="text-caption text-text-muted">{{ sent() || 'Nothing sent yet.' }}</p>
|
|
215
|
+
</div>
|
|
216
|
+
`,
|
|
217
|
+
})
|
|
218
|
+
export class ChatComposerSlotsExampleComponent {
|
|
219
|
+
protected readonly paperclip = Paperclip;
|
|
220
|
+
|
|
221
|
+
readonly presets: readonly BfSelectOption<Preset>[] = [
|
|
222
|
+
{ value: 'names', label: 'Names' },
|
|
223
|
+
{ value: 'slogans', label: 'Slogans' },
|
|
224
|
+
{ value: 'menu', label: 'Menu' },
|
|
225
|
+
];
|
|
226
|
+
readonly preset = signal<Preset | null>('names');
|
|
227
|
+
readonly sent = signal('');
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Growing field
|
|
232
|
+
|
|
233
|
+
The field is a [bf-textarea](https://bitforge-ui.web.app/components/textarea) with `autoResize`: it starts at one line and grows with the text up to `maxRows` lines (8 by default), then it scrolls. `maxLength` limits the text. A browser without CSS field sizing keeps the one-line field and scrolls inside it.
|
|
234
|
+
|
|
235
|
+
## Disabled
|
|
236
|
+
|
|
237
|
+
`disabled` stops typing and sending. With a `disabledReason` (a usage limit, a missing choice) the reason is the empty field's placeholder and the send button's tooltip on hover and keyboard focus; the button then stays focusable, marked `aria-disabled`, so the tooltip can show.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { Component } from '@angular/core';
|
|
241
|
+
import { BfChatComposerComponent } from '@bitforgehq/ui/overlay';
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Disabled with a reason.
|
|
245
|
+
*
|
|
246
|
+
* While nothing can be sent, `disabled` with a `disabledReason` says why: it is the empty
|
|
247
|
+
* field's placeholder, and the send button's tooltip on hover and keyboard focus (the button
|
|
248
|
+
* stays focusable for that, marked `aria-disabled`).
|
|
249
|
+
*/
|
|
250
|
+
@Component({
|
|
251
|
+
selector: 'app-example-chat-composer-disabled',
|
|
252
|
+
imports: [BfChatComposerComponent],
|
|
253
|
+
template: `
|
|
254
|
+
<bf-chat-composer disabled disabledReason="You have used today's 20 messages. More tomorrow." />
|
|
255
|
+
`,
|
|
256
|
+
})
|
|
257
|
+
export class ChatComposerDisabledExampleComponent {}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Sizes
|
|
261
|
+
|
|
262
|
+
`size` sets the field's padding and type and the button's height together: `sm` 32 px, `md` 40 px (default), `lg` 48 px buttons. Unset, it follows `provideBfDefaults()`'s `fieldSize`; a touch app that sets `fieldSize: 'lg'` gets the 48 px composer.
|
|
263
|
+
|
|
264
|
+
## Desktop
|
|
265
|
+
|
|
266
|
+
The field shows the text cursor and its focus ring on every focus. The button has bf-button's hover and pressed steps (primary for send, tonal for stop); disabled it shows no hover. With a `disabledReason` its tooltip shows after a 500 ms hover or at once on keyboard focus. Under a coarse pointer the button's hit area grows to 44 px; the field gets no touch extension (set `fieldSize: 'lg'` for touch).
|
|
267
|
+
|
|
268
|
+
## Theming
|
|
269
|
+
|
|
270
|
+
- Field: see [bf-textarea](https://bitforge-ui.web.app/components/textarea) (`border-strong`, `primary` focus border, `ring` ring, `text-muted` placeholder).
|
|
271
|
+
- Send button: `primary` with `on-primary` text (`primary-hover`, `primary-pressed`); stop button: `primary-soft` with `primary-strong`; disabled: `surface-sunken` with `text-disabled`.
|
|
272
|
+
- Tooltip: see the `bfTooltip` directive.
|
|
273
|
+
|
|
274
|
+
## Accessibility
|
|
275
|
+
|
|
276
|
+
The field is a native `textarea`, named by `label` (visible), else `ariaLabel` or `ariaLabelledby`, else the `chatComposer` label ("Message"). The button is a native `button` whose text is its name, also when an icon shows ("Send", "Stop"); it is natively `disabled` with nothing to send, and `aria-disabled` while disabled with a reason, which the tooltip then describes (`aria-describedby`) when it shows. After a send focus returns to the field. The tooltip shows nothing under a coarse pointer, so the reason is also the placeholder of the empty field.
|
|
277
|
+
|
|
278
|
+
### Keyboard
|
|
279
|
+
|
|
280
|
+
| Key | Where | Action |
|
|
281
|
+
| ------------------------------------------------ | -------- | ----------------------------------------------------------------------------- |
|
|
282
|
+
| <kbd>Enter</kbd> | Field | Sends the text (not while composing, while a reply runs, or when it is empty) |
|
|
283
|
+
| <kbd>Shift</kbd>+<kbd>Enter</kbd> | Field | Starts a new line |
|
|
284
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Composer | Moves between the start controls, the field, the end controls and the button |
|
|
285
|
+
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Button | Sends the text, or stops the reply while it runs |
|
|
286
|
+
| <kbd>Escape</kbd> | Button | Hides the disabled reason's tooltip |
|
|
287
|
+
|
|
288
|
+
## Inputs
|
|
289
|
+
|
|
290
|
+
| Name | Type | Default | Description |
|
|
291
|
+
| ---------------- | ----------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
|
|
292
|
+
| `running` | `boolean` | `false` | A reply is running: the button stops it (`stopped`) instead of sending. |
|
|
293
|
+
| `disabled` | `boolean` | `false` | Nothing can be written or sent. |
|
|
294
|
+
| `disabledReason` | `string` | `''` | Why it is disabled, shown as the button's tooltip and the empty field's placeholder. |
|
|
295
|
+
| `label` | `string` | `''` | Visible label above the field; usually left empty in a chat. |
|
|
296
|
+
| `ariaLabel` | `string` | `''` | The field's accessible name without a visible label; empty uses the `chatComposer` label ("Message"). |
|
|
297
|
+
| `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the field without a visible label. |
|
|
298
|
+
| `placeholder` | `string` | `''` | Example text shown while the field is empty. |
|
|
299
|
+
| `maxRows` | `number` | `8` | The lines the field grows to at most; past them it scrolls. |
|
|
300
|
+
| `maxLength` | `number \| undefined` | `undefined` | Maximum number of characters (native `maxlength`). |
|
|
301
|
+
| `name` | `string` | `''` | Native name attribute of the textarea. |
|
|
302
|
+
| `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Padding, type and button step: sm, md, lg; unset follows `provideBfDefaults()` (`fieldSize`, `md` by default). |
|
|
303
|
+
| `value` | `string` | `''` | The text being written; two-way bindable. Cleared after a send. Two-way: `[(value)]`. |
|
|
304
|
+
|
|
305
|
+
## Outputs
|
|
306
|
+
|
|
307
|
+
| Name | Payload | Description |
|
|
308
|
+
| --------- | -------- | ------------------------------------------------------------------------------------ |
|
|
309
|
+
| `changed` | `string` | Emitted with the new text right after the user changes it (each keystroke or paste). |
|
|
310
|
+
| `sent` | `string` | Emitted with the trimmed text when the user sends it (Enter or the send button). |
|
|
311
|
+
| `stopped` | `void` | Emitted when the user presses the stop button while `running`. |
|
|
312
|
+
|
|
313
|
+
## Slots
|
|
314
|
+
|
|
315
|
+
- `start`: Controls before the field, such as a preset picker or an attach button.
|
|
316
|
+
- `end`: Controls between the field and the send button.
|
|
317
|
+
- `send-icon`: The send button's icon; without one the button shows its text.
|
|
318
|
+
- `stop-icon`: The stop button's icon; without one the button shows its text.
|
|
319
|
+
|
|
320
|
+
## Methods
|
|
321
|
+
|
|
322
|
+
### `send(): void`
|
|
323
|
+
|
|
324
|
+
Sends the text: emits `sent` with it trimmed, clears the field and keeps focus in it. Does
|
|
325
|
+
nothing while disabled or running, or when the text is only whitespace.
|
|
326
|
+
|
|
327
|
+
### `focus(): void`
|
|
328
|
+
|
|
329
|
+
Focuses the text field.
|
|
330
|
+
|
|
331
|
+
## Notes
|
|
332
|
+
|
|
333
|
+
### Do
|
|
334
|
+
|
|
335
|
+
- Set `running` from the app's own reply state and turn it off when the reply ends, stops or fails.
|
|
336
|
+
- Give a `disabledReason` whenever the composer is disabled for a reason the user can act on.
|
|
337
|
+
|
|
338
|
+
### Don't
|
|
339
|
+
|
|
340
|
+
- Don't clear the value yourself after `(sent)`: the composer does it.
|
|
341
|
+
- Don't put the reply's progress in the composer; show it in the thread's streaming message.
|
|
342
|
+
|
|
343
|
+
## Migration notes
|
|
344
|
+
|
|
345
|
+
### 0.5.0
|
|
346
|
+
|
|
347
|
+
New: `bf-chat-composer` in `@bitforgehq/ui/overlay`, the input row of a chat. A `bf-textarea`
|
|
348
|
+
with `autoResize` grows from one line to `maxRows` (8), then scrolls. `Enter` sends and
|
|
349
|
+
`Shift+Enter` starts a new line; nothing sends during IME composition or with whitespace-only
|
|
350
|
+
text. `value` is a `model()` that clears after a send; `sent` emits the trimmed text, `changed`
|
|
351
|
+
each edit. While `running` the send button is a stop button that emits `stopped`. Icons go in the
|
|
352
|
+
`send-icon` and `stop-icon` slots (the text stays as the button's name, and shows without an
|
|
353
|
+
icon); `start` and `end` slots take extra controls. `disabled` with `disabledReason` shows the
|
|
354
|
+
reason as the button's tooltip (`aria-disabled`, still focusable) and the empty field's
|
|
355
|
+
placeholder. `size` follows `fieldSize`. New labels `chatComposer`, `chatSend` and `chatStop`.
|
|
356
|
+
`@bitforgehq/ui/testing`: new `BfChatComposerHarness` (`setValue()`, `getValue()`, `send()`,
|
|
357
|
+
`pressEnter()`, `stop()`, `isRunning()`, `isDisabled()`, `canAct()`, `getButtonLabel()`).
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# bf-chat-message
|
|
2
|
+
|
|
3
|
+
One chat message, a user bubble or a full-width assistant reply that can stream
|
|
4
|
+
|
|
5
|
+
`BfChatMessageComponent` from `@bitforgehq/ui` · Data display · since 0.5.0
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component } from '@angular/core';
|
|
11
|
+
import { BfChatMessageComponent, BfChatThreadComponent } from '@bitforgehq/ui';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bf-chat-message overview.
|
|
15
|
+
*
|
|
16
|
+
* A user message is a tinted bubble at the inline end and an assistant message spans the row
|
|
17
|
+
* with no bubble. Each one names its author and time in a small header; the time is a `time`
|
|
18
|
+
* element with the exact value, shown as hours and minutes or as `timeText`.
|
|
19
|
+
*/
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'app-example-chat-message-overview',
|
|
22
|
+
imports: [BfChatThreadComponent, BfChatMessageComponent],
|
|
23
|
+
template: `
|
|
24
|
+
<bf-chat-thread label="Two messages">
|
|
25
|
+
<bf-chat-message role="user" author="You" time="2026-11-06T09:30:00Z" timeText="9:30">
|
|
26
|
+
What should the cafe's first menu board say?
|
|
27
|
+
</bf-chat-message>
|
|
28
|
+
<bf-chat-message author="Assistant" time="2026-11-06T09:30:00Z" timeText="9:30">
|
|
29
|
+
<p>
|
|
30
|
+
Keep it to five lines: the coffee, the tea, one cake, one sandwich and the price of a
|
|
31
|
+
refill.
|
|
32
|
+
</p>
|
|
33
|
+
<p>Write it by hand. Regulars read a hand-written board as a sign that someone cares.</p>
|
|
34
|
+
</bf-chat-message>
|
|
35
|
+
</bf-chat-thread>
|
|
36
|
+
`,
|
|
37
|
+
})
|
|
38
|
+
export class ChatMessageOverviewExampleComponent {}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Overview
|
|
42
|
+
|
|
43
|
+
One message of a `bf-chat-thread`. A `user` message is a tinted bubble at the inline end; an
|
|
44
|
+
`assistant` message is full width with no bubble. The default slot is the message text, styled
|
|
45
|
+
as prose (`bfProse`) for markdown the app renders. An optional header names the `author` and
|
|
46
|
+
the `time`. While `streaming`, a caret pulses at the end of the text and the message is
|
|
47
|
+
`aria-busy`, so the thread announces it once, when it is done. A `stopped` or `failed` message
|
|
48
|
+
shows a short status line, with the `action` slot beside it (a Retry button).
|
|
49
|
+
|
|
50
|
+
One message of a [bf-chat-thread](https://bitforge-ui.web.app/components/chat-thread): a tinted bubble for the user, a full-width reply without a bubble for the assistant, with an optional header, a streaming caret and a status line.
|
|
51
|
+
|
|
52
|
+
## When to use
|
|
53
|
+
|
|
54
|
+
Use `bf-chat-message` inside a [bf-chat-thread](https://bitforge-ui.web.app/components/chat-thread), one per message. Use [bf-timeline-event](https://bitforge-ui.web.app/components/timeline-event) instead for an event in a history, and [bf-alert](https://bitforge-ui.web.app/components/alert) for a message from the app itself rather than from a conversation.
|
|
55
|
+
|
|
56
|
+
## Basic usage
|
|
57
|
+
|
|
58
|
+
`role` is `assistant` (the default: full width, no bubble) or `user` (a `primary-soft` bubble at the inline end, up to 85 % of the row). The default slot is the text: plain text, paragraphs, or the app's rendered markdown. `author` and `time` (a Date or an ISO 8601 string, shown as hours and minutes in the app's locale, or as `timeText`) fill a small header above the message; leave both empty for none.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { Component } from '@angular/core';
|
|
62
|
+
import { BfChatMessageComponent, BfChatThreadComponent } from '@bitforgehq/ui';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The bf-chat-message overview.
|
|
66
|
+
*
|
|
67
|
+
* A user message is a tinted bubble at the inline end and an assistant message spans the row
|
|
68
|
+
* with no bubble. Each one names its author and time in a small header; the time is a `time`
|
|
69
|
+
* element with the exact value, shown as hours and minutes or as `timeText`.
|
|
70
|
+
*/
|
|
71
|
+
@Component({
|
|
72
|
+
selector: 'app-example-chat-message-overview',
|
|
73
|
+
imports: [BfChatThreadComponent, BfChatMessageComponent],
|
|
74
|
+
template: `
|
|
75
|
+
<bf-chat-thread label="Two messages">
|
|
76
|
+
<bf-chat-message role="user" author="You" time="2026-11-06T09:30:00Z" timeText="9:30">
|
|
77
|
+
What should the cafe's first menu board say?
|
|
78
|
+
</bf-chat-message>
|
|
79
|
+
<bf-chat-message author="Assistant" time="2026-11-06T09:30:00Z" timeText="9:30">
|
|
80
|
+
<p>
|
|
81
|
+
Keep it to five lines: the coffee, the tea, one cake, one sandwich and the price of a
|
|
82
|
+
refill.
|
|
83
|
+
</p>
|
|
84
|
+
<p>Write it by hand. Regulars read a hand-written board as a sign that someone cares.</p>
|
|
85
|
+
</bf-chat-message>
|
|
86
|
+
</bf-chat-thread>
|
|
87
|
+
`,
|
|
88
|
+
})
|
|
89
|
+
export class ChatMessageOverviewExampleComponent {}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Streaming
|
|
93
|
+
|
|
94
|
+
Set `streaming` while the reply's text is still arriving and append the text as it comes. A caret pulses at the end of the text (after bare text, inside the last paragraph or list item, also one wrapper deep), and the message and the thread are `aria-busy`, so a screen reader reads the reply once, when `streaming` goes back to false. An empty streaming message shows the caret alone, as a "thinking" cue.
|
|
95
|
+
|
|
96
|
+
## Stopped and failed
|
|
97
|
+
|
|
98
|
+
`status` is `done` (the default), `stopped` (a muted "Stopped" line) or `failed` (a "Something went wrong" line in the danger colour). `statusMessage` replaces the built-in line, such as with the error the server returned. The `action` slot sits beside the line, for a Retry button.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { Component, signal } from '@angular/core';
|
|
102
|
+
import {
|
|
103
|
+
BfButtonComponent,
|
|
104
|
+
BfChatMessageComponent,
|
|
105
|
+
BfChatThreadComponent,
|
|
106
|
+
type BfChatMessageStatus,
|
|
107
|
+
} from '@bitforgehq/ui';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stopped and failed messages.
|
|
111
|
+
*
|
|
112
|
+
* A reply the user stopped shows "Stopped" under its text; a reply that failed shows "Something
|
|
113
|
+
* went wrong" in the danger colour, with a Retry button in the message's `action` slot. Retry
|
|
114
|
+
* here answers at once; a real app would ask its model again.
|
|
115
|
+
*/
|
|
116
|
+
@Component({
|
|
117
|
+
selector: 'app-example-chat-thread-states',
|
|
118
|
+
imports: [BfChatThreadComponent, BfChatMessageComponent, BfButtonComponent],
|
|
119
|
+
template: `
|
|
120
|
+
<bf-chat-thread label="Message states" class="max-h-[24rem]">
|
|
121
|
+
<bf-chat-message role="user">Write a slogan for the cafe.</bf-chat-message>
|
|
122
|
+
<bf-chat-message status="stopped">
|
|
123
|
+
<p>Good coffee, slow mornings and a</p>
|
|
124
|
+
</bf-chat-message>
|
|
125
|
+
<bf-chat-message role="user">Try a shorter one.</bf-chat-message>
|
|
126
|
+
<bf-chat-message [status]="status()">
|
|
127
|
+
@if (status() === 'done') {
|
|
128
|
+
<p>Stay for the second cup.</p>
|
|
129
|
+
}
|
|
130
|
+
@if (status() === 'failed') {
|
|
131
|
+
<bf-button bfSlot="action" variant="text" size="sm" (clicked)="retry()">Retry</bf-button>
|
|
132
|
+
}
|
|
133
|
+
</bf-chat-message>
|
|
134
|
+
</bf-chat-thread>
|
|
135
|
+
`,
|
|
136
|
+
})
|
|
137
|
+
export class ChatThreadStatesExampleComponent {
|
|
138
|
+
readonly status = signal<BfChatMessageStatus>('failed');
|
|
139
|
+
|
|
140
|
+
retry(): void {
|
|
141
|
+
this.status.set('done');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Rich content
|
|
147
|
+
|
|
148
|
+
The body is a `bfProse` element: headings, lists, links, inline code, code blocks, quotes and rules in the library tokens. The `content` slot below it holds inline content the prose styles leave alone, such as cards, images or chips.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { Component, signal } from '@angular/core';
|
|
152
|
+
import {
|
|
153
|
+
BfCardComponent,
|
|
154
|
+
BfChatMessageComponent,
|
|
155
|
+
BfChatThreadComponent,
|
|
156
|
+
BfChipComponent,
|
|
157
|
+
} from '@bitforgehq/ui';
|
|
158
|
+
|
|
159
|
+
/* The app renders its markdown to HTML (and sanitises it) itself; the message styles it. */
|
|
160
|
+
const REPLY_HTML = `
|
|
161
|
+
<h3>Three name ideas</h3>
|
|
162
|
+
<ol>
|
|
163
|
+
<li><strong>Ember and Oat</strong>: warm and rustic.</li>
|
|
164
|
+
<li><strong>Second Cup</strong>: playful, for regulars.</li>
|
|
165
|
+
<li><strong>Field Notes</strong>: quiet and bookish.</li>
|
|
166
|
+
</ol>
|
|
167
|
+
<p>Check the domain with <code>whois embersandoat.com</code> before you print the menus.</p>
|
|
168
|
+
<blockquote>A short name is easier to say out loud and to find.</blockquote>
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Rendered markdown and inline content.
|
|
173
|
+
*
|
|
174
|
+
* The reply's body is the app's rendered markdown, styled as prose: a heading, a numbered list,
|
|
175
|
+
* inline code and a quote. Below it, the `content` slot holds a card and chips the user can pick
|
|
176
|
+
* from, outside the prose styling.
|
|
177
|
+
*/
|
|
178
|
+
@Component({
|
|
179
|
+
selector: 'app-example-chat-thread-rich-content',
|
|
180
|
+
imports: [BfChatThreadComponent, BfChatMessageComponent, BfCardComponent, BfChipComponent],
|
|
181
|
+
template: `
|
|
182
|
+
<bf-chat-thread label="Rich reply" class="max-h-[30rem]">
|
|
183
|
+
<bf-chat-message role="user" author="You">Name ideas, with a logo mood?</bf-chat-message>
|
|
184
|
+
<bf-chat-message author="Assistant">
|
|
185
|
+
<div [innerHTML]="replyHtml"></div>
|
|
186
|
+
<bf-card bfSlot="content" variant="tint" class="w-64">
|
|
187
|
+
<p class="text-label text-text font-bold">Logo mood</p>
|
|
188
|
+
<p class="text-caption text-text-muted">Hand-drawn, charcoal on oat paper</p>
|
|
189
|
+
</bf-card>
|
|
190
|
+
<div bfSlot="content" class="flex flex-wrap gap-2">
|
|
191
|
+
@for (name of names; track name) {
|
|
192
|
+
<bf-chip [selected]="picked() === name" (changed)="picked.set($event ? name : '')">{{
|
|
193
|
+
name
|
|
194
|
+
}}</bf-chip>
|
|
195
|
+
}
|
|
196
|
+
</div>
|
|
197
|
+
</bf-chat-message>
|
|
198
|
+
</bf-chat-thread>
|
|
199
|
+
`,
|
|
200
|
+
})
|
|
201
|
+
export class ChatThreadRichContentExampleComponent {
|
|
202
|
+
readonly replyHtml = REPLY_HTML;
|
|
203
|
+
readonly names = ['Ember and Oat', 'Second Cup', 'Field Notes'];
|
|
204
|
+
readonly picked = signal('');
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Desktop
|
|
209
|
+
|
|
210
|
+
A message has no hover or pointer state of its own; links in the prose underline more thickly on hover and show the focus ring on keyboard focus, and code blocks scroll sideways with the trackpad. The user bubble stops at 42rem on a wide window.
|
|
211
|
+
|
|
212
|
+
## Theming
|
|
213
|
+
|
|
214
|
+
- User bubble: `primary-soft` background, `text` text, `rounded-field` corners. Assistant reply: no surface.
|
|
215
|
+
- Header: `text-muted` (the time), `text-secondary` (the author), the caption step.
|
|
216
|
+
- Prose: `text` body and headings, `primary-text` links with a `ring` focus ring, `surface-muted` inline code, `surface` code blocks with a `border` edge, `border-strong` quote rule with `text-secondary` text, `text-muted` list markers.
|
|
217
|
+
- Status line: `text-muted` for stopped, `danger` for failed.
|
|
218
|
+
- Caret: the text colour (`CanvasText` under forced colours), pulsing with `--animate-chat-caret`.
|
|
219
|
+
|
|
220
|
+
## Accessibility
|
|
221
|
+
|
|
222
|
+
The message is plain content in the thread's log: no role of its own (the `role` input sets the look, and the host drops the attribute). The header's time is a `time` element whose `datetime` keeps the exact value. While `streaming`, the message is `aria-busy="true"` and the caret is a pseudo-element, which is never read. The status line is text in the log, so a screen reader announces it when the status changes. A `user` message is told apart by its `author` text, not only by its colour and side.
|
|
223
|
+
|
|
224
|
+
## Inputs
|
|
225
|
+
|
|
226
|
+
| Name | Type | Default | Description |
|
|
227
|
+
| --------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
228
|
+
| `role` | `BfChatMessageRole` (`'user' \| 'assistant'`) | `'assistant'` | Who wrote it: `user` (a bubble at the inline end) or `assistant` (full width, no bubble). |
|
|
229
|
+
| `author` | `string` | `''` | Whose message it is, shown in the header, such as "You" or the assistant's name; empty shows none. |
|
|
230
|
+
| `time` | `Date \| string \| null` | `null` | When it was written: a Date or an ISO 8601 string, shown as hours and minutes; empty or invalid shows no time. |
|
|
231
|
+
| `timeText` | `string` | `''` | Shown in place of the formatted time, such as "just now"; the `time` element's `datetime` keeps the exact value. |
|
|
232
|
+
| `streaming` | `boolean` | `false` | The text is still arriving: a caret pulses at its end and the message is `aria-busy`. |
|
|
233
|
+
| `status` | `BfChatMessageStatus` (`'done' \| 'stopped' \| 'failed'`) | `'done'` | How it ended: `done` (no status line), `stopped` or `failed`. |
|
|
234
|
+
| `statusMessage` | `string` | `''` | Shown in place of the built-in status line (`chatMessageStopped`, `chatMessageFailed`), such as the error the server returned. |
|
|
235
|
+
|
|
236
|
+
## Slots
|
|
237
|
+
|
|
238
|
+
- `default`: The message text: plain text or the app's rendered markdown, styled as prose.
|
|
239
|
+
- `content`: Inline content below the text, such as cards, images or chips (not styled as prose).
|
|
240
|
+
- `action`: Beside the status line, such as a Retry button.
|
|
241
|
+
|
|
242
|
+
## Notes
|
|
243
|
+
|
|
244
|
+
### Do
|
|
245
|
+
|
|
246
|
+
- Set `author` on every message, so its speaker is never shown by colour and position alone.
|
|
247
|
+
- Turn `streaming` off when the last piece of text has arrived, also when the request fails or stops.
|
|
248
|
+
|
|
249
|
+
### Don't
|
|
250
|
+
|
|
251
|
+
- Don't put buttons in the text: use the `action` slot for one beside the status line.
|