@inglorious/ui 9.4.0 → 9.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "9.4.0",
3
+ "version": "9.5.1",
4
4
  "description": "A design system for Inglorious Web with render-function components and customizable themes",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -54,6 +54,11 @@
54
54
  "import": "./src/controls/button-group/index.js"
55
55
  },
56
56
  "./button-group.css": "./src/controls/button-group/style.css",
57
+ "./chat": {
58
+ "types": "./types/data-display/chat.d.ts",
59
+ "import": "./src/data-display/chat/index.js"
60
+ },
61
+ "./chat.css": "./src/data-display/chat/style.css",
57
62
  "./carousel": {
58
63
  "types": "./types/data-display/carousel.d.ts",
59
64
  "import": "./src/data-display/carousel/index.js"
@@ -308,19 +313,19 @@
308
313
  "peerDependencies": {
309
314
  "@inglorious/store": "12.0.0",
310
315
  "@inglorious/utils": "5.1.0",
311
- "@inglorious/web": "6.9.0"
316
+ "@inglorious/web": "6.9.1"
312
317
  },
313
318
  "devDependencies": {
314
319
  "@iconify-icons/fa7-brands": "^2.0.0",
315
320
  "@iconify-icons/fa7-regular": "^2.0.0",
316
321
  "@iconify-icons/fa7-solid": "^2.0.0",
317
322
  "@iconify-icons/material-symbols": "^2.0.6",
318
- "@inglorious/charts": "12.2.1",
323
+ "@inglorious/charts": "12.2.2",
319
324
  "@inglorious/eslint-config": "1.2.0",
320
325
  "@inglorious/store": "12.0.0",
321
326
  "@inglorious/vite-plugin-jsx": "2.0.5",
322
327
  "@inglorious/vite-plugin-vue": "1.0.6",
323
- "@inglorious/web": "6.9.0",
328
+ "@inglorious/web": "6.9.1",
324
329
  "@storybook/addon-docs": "^10.6.0",
325
330
  "@storybook/web-components-vite": "^10.6.0",
326
331
  "happy-dom": "^20.14.0",
@@ -4,8 +4,13 @@
4
4
  gap: var(--iw-space-1);
5
5
  font-family: var(--iw-font-family-sans);
6
6
 
7
- &.iw-textarea-full-width .iw-textarea {
7
+ /* The field takes the width it is given, and the box inside fills it. */
8
+ &.iw-textarea-full-width {
8
9
  width: 100%;
10
+
11
+ .iw-textarea {
12
+ width: 100%;
13
+ }
9
14
  }
10
15
 
11
16
  .iw-textarea-label {
@@ -19,8 +24,8 @@
19
24
  margin-inline-start: var(--iw-space-1);
20
25
  }
21
26
 
27
+ /* Its height is its rows', or its content's with isAutoSize. */
22
28
  .iw-textarea {
23
- min-height: 3rem;
24
29
  width: min(100%, 100%);
25
30
  resize: none;
26
31
  padding: var(--iw-space-2) var(--iw-space-3);
@@ -24,6 +24,7 @@ export function render(props) {
24
24
  value = "",
25
25
  placeholder = "",
26
26
  label,
27
+ ariaLabel,
27
28
  hint,
28
29
  error,
29
30
  size = "md",
@@ -37,6 +38,7 @@ export function render(props) {
37
38
  onChange,
38
39
  onBlur,
39
40
  onFocus,
41
+ onKeyDown,
40
42
  ...rest
41
43
  } = props
42
44
 
@@ -76,7 +78,7 @@ export function render(props) {
76
78
  <textarea
77
79
  id=${textareaId || nothing}
78
80
  name=${name}
79
- aria-label=${fallbackLabel(textareaId, label)}
81
+ aria-label=${ariaLabel || fallbackLabel(textareaId, label)}
80
82
  aria-describedby=${messageId || nothing}
81
83
  aria-invalid=${error ? "true" : "false"}
82
84
  rows=${rows}
@@ -89,6 +91,7 @@ export function render(props) {
89
91
  @input=${(event) => onChange?.(event.target.value)}
90
92
  @blur=${onBlur}
91
93
  @focus=${onFocus}
94
+ @keydown=${onKeyDown}
92
95
  ${ref((element) => applyElementProps(element, rest))}
93
96
  ></textarea>
94
97
 
@@ -0,0 +1,90 @@
1
+ import {
2
+ createEntityRender,
3
+ notifyActionArgType,
4
+ } from "../../stories/notify-story.js"
5
+ import { Chat } from "."
6
+
7
+ /** A chat fills its container, which sets its height. */
8
+ const withHeight = (story) => {
9
+ const frame = document.createElement("div")
10
+ frame.style.height = "28rem"
11
+ const chat = story()
12
+ chat.style.height = "100%"
13
+ frame.append(chat)
14
+ return frame
15
+ }
16
+
17
+ const conversation = [
18
+ { id: "m1", role: "user", content: "Where did my money go this month?" },
19
+ {
20
+ id: "m2",
21
+ role: "assistant",
22
+ content:
23
+ "Rent took €900 of the €1,076 you spent. Food came next, at €173, then transport at €58.",
24
+ },
25
+ { id: "m3", role: "user", content: "How can I spend less on food?" },
26
+ ]
27
+
28
+ export default {
29
+ title: "Data Display/Chat",
30
+ tags: ["autodocs"],
31
+ render: createEntityRender({ Chat }),
32
+ decorators: [withHeight],
33
+ argTypes: {
34
+ ...notifyActionArgType,
35
+ messages: {
36
+ control: "object",
37
+ description:
38
+ "Messages: `{ id, role, content }`, or `{ id, role, entity }` to render another entity, such as a generated answer.",
39
+ },
40
+ draft: { control: "text", description: "What the user is typing." },
41
+ isBusy: {
42
+ control: "boolean",
43
+ description: "A reply is on its way: shows it, and holds sending.",
44
+ },
45
+ error: { control: "text", description: "Why the last reply failed." },
46
+ suggestions: {
47
+ control: "object",
48
+ description: "Messages to start with, while the chat is empty.",
49
+ },
50
+ placeholder: { control: "text" },
51
+ sendLabel: { control: "text" },
52
+ },
53
+ parameters: {
54
+ docs: {
55
+ description: {
56
+ component:
57
+ "A conversation and a box to write in. The chat keeps the messages and the draft; the app answers by handling `messageSend`, and adds replies with `messageAdd`, `replyStart` and `replyEnd`. Enter sends, Shift+Enter starts a new line.",
58
+ },
59
+ },
60
+ },
61
+ }
62
+
63
+ export const Default = {
64
+ args: { id: "chat", type: "Chat", messages: conversation },
65
+ }
66
+
67
+ export const Suggestions = {
68
+ args: {
69
+ id: "chatSuggestions",
70
+ type: "Chat",
71
+ messages: [],
72
+ suggestions: [
73
+ "Where did my money go this month?",
74
+ "Show my food expenses",
75
+ "I want to add an expense",
76
+ ],
77
+ },
78
+ }
79
+
80
+ export const Replying = {
81
+ args: { ...Default.args, id: "chatReplying", isBusy: true },
82
+ }
83
+
84
+ export const Failed = {
85
+ args: {
86
+ ...Default.args,
87
+ id: "chatFailed",
88
+ error: "Could not reach the model. Is Ollama running?",
89
+ },
90
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @typedef {import('../../../types/data-display/chat').ChatEntity} ChatEntity
3
+ * @typedef {import('../../../types/data-display/chat').ChatMessage} ChatMessage
4
+ */
5
+
6
+ /**
7
+ * Fills in a chat's defaults. A chat keeps its messages and the draft being
8
+ * typed; who answers, and how, is the app's to decide by handling
9
+ * `messageSend`.
10
+ * @param {ChatEntity} entity
11
+ */
12
+ export function create(entity) {
13
+ entity.messages ??= []
14
+ entity.draft ??= ""
15
+ entity.isBusy ??= false
16
+ entity.error ??= null
17
+ entity.suggestions ??= []
18
+ entity.messageCount ??= entity.messages.length
19
+ }
20
+
21
+ /**
22
+ * Keeps what the user is typing.
23
+ * @param {ChatEntity} entity
24
+ * @param {string} draft
25
+ */
26
+ export function draftChange(entity, draft) {
27
+ entity.draft = draft
28
+ }
29
+
30
+ /**
31
+ * Adds the user's message: the text given, such as a suggestion picked, or
32
+ * else the draft, which it clears. Nothing is sent while a reply is coming,
33
+ * nor an empty message. Apps answer by handling this event after the chat.
34
+ * @param {ChatEntity} entity
35
+ * @param {string} [text]
36
+ */
37
+ export function messageSend(entity, text) {
38
+ const content = (text ?? entity.draft).trim()
39
+ if (!content || entity.isBusy) return
40
+
41
+ addMessage(entity, { role: "user", content })
42
+ entity.draft = ""
43
+ entity.error = null
44
+ }
45
+
46
+ /**
47
+ * Adds a message, such as the assistant's reply: its text as `content`, or
48
+ * the id of an entity that renders it as `entity`.
49
+ * @param {ChatEntity} entity
50
+ * @param {Partial<ChatMessage>} message
51
+ */
52
+ export function messageAdd(entity, message) {
53
+ addMessage(entity, { role: "assistant", ...message })
54
+ }
55
+
56
+ /**
57
+ * A reply is on its way: the chat says so, and waits for it to send again.
58
+ * @param {ChatEntity} entity
59
+ */
60
+ export function replyStart(entity) {
61
+ entity.isBusy = true
62
+ entity.error = null
63
+ }
64
+
65
+ /**
66
+ * The reply is whole.
67
+ * @param {ChatEntity} entity
68
+ */
69
+ export function replyEnd(entity) {
70
+ entity.isBusy = false
71
+ }
72
+
73
+ /**
74
+ * The reply failed: the chat shows why, and can send again.
75
+ * @param {ChatEntity} entity
76
+ * @param {string} error
77
+ */
78
+ export function replyFail(entity, error) {
79
+ entity.isBusy = false
80
+ entity.error = error
81
+ }
82
+
83
+ /**
84
+ * Starts the conversation over.
85
+ * @param {ChatEntity} entity
86
+ */
87
+ export function messagesClear(entity) {
88
+ entity.messages = []
89
+ entity.error = null
90
+ }
91
+
92
+ function addMessage(entity, message) {
93
+ entity.messageCount = (entity.messageCount ?? entity.messages.length) + 1
94
+ entity.messages.push({ id: `m${entity.messageCount}`, ...message })
95
+ }
@@ -0,0 +1,19 @@
1
+ import * as handlers from "./handlers.js"
2
+ import { Chat as renderers } from "./template.js"
3
+
4
+ export const Chat = {
5
+ ...handlers,
6
+ ...renderers,
7
+ render(entity, api) {
8
+ const id = entity.id
9
+ const props = {
10
+ ...entity,
11
+ onDraftChange: (draft) => api.notify(`#${id}:draftChange`, draft),
12
+ onSend: (text) => api.notify(`#${id}:messageSend`, text),
13
+ // A message naming an entity, such as a generated answer, shows it.
14
+ renderEntity: (entityId) => api.render(entityId),
15
+ }
16
+
17
+ return this.renderChat(props)
18
+ },
19
+ }
@@ -0,0 +1,114 @@
1
+ @import "../../controls/button/style.css";
2
+ @import "../../controls/textarea/style.css";
3
+ @import "../../feedback/alert/style.css";
4
+
5
+ .iw-chat {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--iw-space-3);
9
+ block-size: 100%;
10
+ min-block-size: 0;
11
+ font-family: var(--iw-font-family-sans);
12
+ color: var(--iw-color-text);
13
+
14
+ .iw-chat-messages {
15
+ display: flex;
16
+ flex: 1 1 auto;
17
+ flex-direction: column;
18
+ gap: var(--iw-space-4);
19
+ min-block-size: 0;
20
+ overflow-y: auto;
21
+ overscroll-behavior: contain;
22
+ }
23
+
24
+ .iw-chat-message {
25
+ display: flex;
26
+ }
27
+
28
+ .iw-chat-message-user {
29
+ justify-content: flex-end;
30
+
31
+ .iw-chat-message-content {
32
+ max-inline-size: 80%;
33
+ padding: var(--iw-space-2) var(--iw-space-3);
34
+ border-radius: var(--iw-radius-lg);
35
+ background: color-mix(in srgb, var(--iw-color-primary) 14%, transparent);
36
+ overflow-wrap: anywhere;
37
+ }
38
+ }
39
+
40
+ .iw-chat-message-assistant .iw-chat-message-content {
41
+ inline-size: 100%;
42
+ min-inline-size: 0;
43
+ }
44
+
45
+ /* A message's own line breaks show; the template's around it do not. */
46
+ .iw-chat-text {
47
+ white-space: pre-wrap;
48
+ }
49
+
50
+ .iw-chat-typing {
51
+ display: flex;
52
+ gap: var(--iw-space-1);
53
+ padding: var(--iw-space-2) 0;
54
+ }
55
+
56
+ .iw-chat-typing-dot {
57
+ inline-size: 0.5rem;
58
+ block-size: 0.5rem;
59
+ border-radius: 50%;
60
+ background: var(--iw-color-text-muted);
61
+ animation: iw-chat-typing 1.2s infinite ease-in-out;
62
+
63
+ &:nth-child(2) {
64
+ animation-delay: 0.15s;
65
+ }
66
+
67
+ &:nth-child(3) {
68
+ animation-delay: 0.3s;
69
+ }
70
+ }
71
+
72
+ .iw-chat-suggestions {
73
+ display: flex;
74
+ flex-wrap: wrap;
75
+ gap: var(--iw-space-2);
76
+ }
77
+
78
+ .iw-chat-composer {
79
+ display: flex;
80
+ align-items: flex-end;
81
+ gap: var(--iw-space-2);
82
+
83
+ .iw-textarea-field {
84
+ flex: 1 1 auto;
85
+ min-inline-size: 0;
86
+ }
87
+
88
+ /* The box grows with the message up to a point, then scrolls. */
89
+ .iw-textarea {
90
+ max-block-size: 10rem;
91
+ }
92
+ }
93
+ }
94
+
95
+ @keyframes iw-chat-typing {
96
+ 0%,
97
+ 80%,
98
+ 100% {
99
+ opacity: 0.3;
100
+ transform: translateY(0);
101
+ }
102
+
103
+ 40% {
104
+ opacity: 1;
105
+ transform: translateY(-0.2rem);
106
+ }
107
+ }
108
+
109
+ @media (prefers-reduced-motion: reduce) {
110
+ .iw-chat .iw-chat-typing-dot {
111
+ animation: none;
112
+ opacity: 0.6;
113
+ }
114
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @typedef {import('../../../types/data-display/chat').ChatProps} ChatProps
3
+ * @typedef {import('../../../types/data-display/chat').ChatMessage} ChatMessage
4
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
5
+ */
6
+
7
+ import { html } from "@inglorious/web"
8
+ import { ref } from "@inglorious/web/directives/ref"
9
+ import { repeat } from "@inglorious/web/directives/repeat"
10
+ import { when } from "@inglorious/web/directives/when"
11
+
12
+ import { Button } from "../../controls/button/index.js"
13
+ import { Textarea } from "../../controls/textarea/index.js"
14
+ import { Alert } from "../../feedback/alert/index.js"
15
+
16
+ /** How close to the bottom, in pixels, still counts as reading the latest. */
17
+ const PINNED_DISTANCE = 40
18
+ const TYPING_DOTS = 3
19
+
20
+ export const Chat = {
21
+ /**
22
+ * Main entrypoint for the chat template. It delegates to `renderChat` so overrides can still reuse it.
23
+ * A chat shows a conversation, the assistant's replies as they come, and a box to write the next message.
24
+ * @param {ChatProps} props The chat props.
25
+ * @returns {TemplateResult} The rendered chat.
26
+ */
27
+ render(props) {
28
+ return this.renderChat(props)
29
+ },
30
+
31
+ /**
32
+ * Renders the conversation, any error, the suggestions while it is empty, and the composer.
33
+ * @param {ChatProps} props The chat props.
34
+ * @returns {TemplateResult} The rendered chat.
35
+ */
36
+ renderChat(props) {
37
+ const {
38
+ messages = [],
39
+ isBusy = false,
40
+ error,
41
+ suggestions = [],
42
+ label = "Chat",
43
+ } = props
44
+
45
+ return html`<section class="iw-chat" aria-label=${label}>
46
+ <div
47
+ class="iw-chat-messages"
48
+ role="log"
49
+ aria-live="polite"
50
+ @scroll=${keepPinned}
51
+ ${ref(followLatest)}
52
+ >
53
+ ${repeat(
54
+ messages,
55
+ (message) => message.id,
56
+ (message) => this.renderMessage?.(props, message),
57
+ )}
58
+ ${when(isBusy, () => this.renderTyping?.(props))}
59
+ </div>
60
+ ${when(error, () => Alert.render({ severity: "error", description: error }))}
61
+ ${when(!messages.length && suggestions.length, () =>
62
+ this.renderSuggestions?.(props),
63
+ )}
64
+ ${this.renderComposer?.(props)}
65
+ </section>`
66
+ },
67
+
68
+ /**
69
+ * Renders one message: the user's in a bubble, the assistant's across the chat.
70
+ * Override to show who wrote it, or when.
71
+ * @param {ChatProps} props The chat props.
72
+ * @param {ChatMessage} message The message.
73
+ * @returns {TemplateResult} The rendered message.
74
+ */
75
+ renderMessage(props, message) {
76
+ const isUser = message.role === "user"
77
+ const author = isUser
78
+ ? (props.userLabel ?? "You")
79
+ : (props.assistantLabel ?? "Assistant")
80
+
81
+ return html`<article
82
+ class="iw-chat-message iw-chat-message-${isUser ? "user" : "assistant"}"
83
+ aria-label=${author}
84
+ >
85
+ <div class="iw-chat-message-content">
86
+ ${this.renderContent?.(props, message)}
87
+ </div>
88
+ </article>`
89
+ },
90
+
91
+ /**
92
+ * Renders what a message says: its `content`, or what `renderEntity` draws for its `entity`.
93
+ * @param {ChatProps} props The chat props.
94
+ * @param {ChatMessage} message The message.
95
+ * @returns {unknown} The message's content.
96
+ */
97
+ renderContent(props, message) {
98
+ return when(
99
+ message.entity && props.renderEntity,
100
+ () => props.renderEntity(message.entity),
101
+ () => html`<span class="iw-chat-text">${message.content}</span>`,
102
+ )
103
+ },
104
+
105
+ /**
106
+ * Renders the sign that a reply is on its way.
107
+ * @param {ChatProps} props The chat props.
108
+ * @returns {TemplateResult} The rendered indicator.
109
+ */
110
+ renderTyping(props) {
111
+ const author = props.assistantLabel ?? "Assistant"
112
+ return html`<div
113
+ class="iw-chat-typing"
114
+ role="status"
115
+ aria-label=${`${author} is replying`}
116
+ >
117
+ ${Array.from(
118
+ { length: TYPING_DOTS },
119
+ () => html`<span class="iw-chat-typing-dot"></span>`,
120
+ )}
121
+ </div>`
122
+ },
123
+
124
+ /**
125
+ * Renders the messages the user may start with, while the chat is empty.
126
+ * @param {ChatProps} props The chat props.
127
+ * @returns {TemplateResult} The rendered suggestions.
128
+ */
129
+ renderSuggestions(props) {
130
+ return html`<div class="iw-chat-suggestions">
131
+ ${props.suggestions.map((suggestion) =>
132
+ Button.render({
133
+ variant: "outline",
134
+ size: "sm",
135
+ children: suggestion,
136
+ isDisabled: props.isBusy,
137
+ onClick: () => props.onSend?.(suggestion),
138
+ }),
139
+ )}
140
+ </div>`
141
+ },
142
+
143
+ /**
144
+ * Renders the box to write a message in, which grows with it, and the button that sends it.
145
+ * Enter sends; Shift+Enter starts a new line.
146
+ * @param {ChatProps} props The chat props.
147
+ * @returns {TemplateResult} The rendered composer.
148
+ */
149
+ renderComposer(props) {
150
+ const { draft = "", isBusy = false } = props
151
+ const send = () => props.onSend?.()
152
+
153
+ return html`<form
154
+ class="iw-chat-composer"
155
+ @submit=${(event) => {
156
+ event.preventDefault()
157
+ send()
158
+ }}
159
+ >
160
+ ${Textarea.render({
161
+ name: "message",
162
+ ariaLabel: props.placeholder ?? "Message",
163
+ placeholder: props.placeholder ?? "Message",
164
+ value: draft,
165
+ rows: 1,
166
+ isAutoSize: true,
167
+ isFullWidth: true,
168
+ onChange: (value) => props.onDraftChange?.(value),
169
+ onKeyDown: (event) => {
170
+ // Enter sends, unless it closes a character being composed.
171
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) {
172
+ return
173
+ }
174
+ event.preventDefault()
175
+ send()
176
+ },
177
+ })}
178
+ ${Button.render({
179
+ buttonType: "submit",
180
+ color: "primary",
181
+ children: props.sendLabel ?? "Send",
182
+ isDisabled: isBusy || !draft.trim(),
183
+ })}
184
+ </form>`
185
+ },
186
+ }
187
+
188
+ /**
189
+ * Notes whether the user is reading the latest messages, at the bottom, or
190
+ * has scrolled up to read earlier ones.
191
+ * @param {Event} event
192
+ */
193
+ function keepPinned(event) {
194
+ const log = event.currentTarget
195
+ const distance = log.scrollHeight - log.scrollTop - log.clientHeight
196
+ log.dataset.isPinned = String(distance < PINNED_DISTANCE)
197
+ }
198
+
199
+ /**
200
+ * After each render, scrolls to the latest message, unless the user has
201
+ * scrolled up to read.
202
+ * @param {Element | undefined} log
203
+ */
204
+ function followLatest(log) {
205
+ if (!log) return
206
+ queueMicrotask(() => {
207
+ if (log.dataset.isPinned !== "false") log.scrollTop = log.scrollHeight
208
+ })
209
+ }
@@ -2,6 +2,7 @@
2
2
  @import "./badge/style.css";
3
3
  @import "./before-after/style.css";
4
4
  @import "./carousel/style.css";
5
+ @import "./chat/style.css";
5
6
  @import "./chip/style.css";
6
7
  @import "./data-grid/style.css";
7
8
  @import "./divider/style.css";
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ export { Avatar } from "./data-display/avatar/index.js"
20
20
  export { Badge } from "./data-display/badge/index.js"
21
21
  export { BeforeAfter } from "./data-display/before-after/index.js"
22
22
  export { Carousel } from "./data-display/carousel/index.js"
23
+ export { Chat } from "./data-display/chat/index.js"
23
24
  export { Chip } from "./data-display/chip/index.js"
24
25
  export { DataGrid } from "./data-display/data-grid/index.js"
25
26
  export { Divider } from "./data-display/divider/index.js"
@@ -6,6 +6,8 @@ export interface TextareaProps {
6
6
  value?: string
7
7
  placeholder?: string
8
8
  label?: string
9
+ /** Names the field for assistive tech when it has no visible `label`. */
10
+ ariaLabel?: string
9
11
  hint?: string
10
12
  error?: string
11
13
  size?: "sm" | "md" | "lg"
@@ -19,6 +21,8 @@ export interface TextareaProps {
19
21
  onChange?: (value: string) => void
20
22
  onBlur?: () => void
21
23
  onFocus?: () => void
24
+ /** Called on every key press, such as to send a message on Enter. */
25
+ onKeyDown?: (event: KeyboardEvent) => void
22
26
  [key: string]: unknown
23
27
  }
24
28
 
@@ -0,0 +1,73 @@
1
+ import type { Api, TemplateResult } from "@inglorious/web"
2
+
3
+ /** One message of a chat: its text, or an entity that renders it. */
4
+ export interface ChatMessage {
5
+ id: string
6
+ role: "user" | "assistant"
7
+ /** What the message says, as text. */
8
+ content?: string
9
+ /** The id of an entity to render as the message, such as a generated answer. */
10
+ entity?: string
11
+ [key: string]: unknown
12
+ }
13
+
14
+ /** What a chat keeps and shows. */
15
+ export interface ChatProps {
16
+ id?: string
17
+ type?: string
18
+ messages?: ChatMessage[]
19
+ /** What the user is typing. */
20
+ draft?: string
21
+ /** A reply is on its way: the chat shows it, and does not send meanwhile. */
22
+ isBusy?: boolean
23
+ /** Why the last reply failed. */
24
+ error?: string | null
25
+ /** Messages to start with, offered while the chat is empty. */
26
+ suggestions?: string[]
27
+ /** Names the chat for assistive tech. Defaults to "Chat". */
28
+ label?: string
29
+ /** The message box's placeholder and name. Defaults to "Message". */
30
+ placeholder?: string
31
+ /** Defaults to "Send". */
32
+ sendLabel?: string
33
+ /** Who the user's messages are by, for assistive tech. Defaults to "You". */
34
+ userLabel?: string
35
+ /** Who replies. Defaults to "Assistant". */
36
+ assistantLabel?: string
37
+ onDraftChange?: (draft: string) => void
38
+ /** Sends `text`, such as a suggestion picked, or else the draft. */
39
+ onSend?: (text?: string) => void
40
+ /** Draws a message's `entity`. */
41
+ renderEntity?: (entityId: string) => unknown
42
+ [key: string]: unknown
43
+ }
44
+
45
+ /** A chat entity: its props, and a count that keeps message ids unique. */
46
+ export interface ChatEntity extends ChatProps {
47
+ id?: string
48
+ type: string
49
+ messages: ChatMessage[]
50
+ messageCount?: number
51
+ }
52
+
53
+ export type ChatType = {
54
+ create(entity: ChatEntity): void
55
+ draftChange(entity: ChatEntity, draft: string): void
56
+ /** Adds the user's message, `text` or the draft; apps answer by handling it too. */
57
+ messageSend(entity: ChatEntity, text?: string): void
58
+ /** Adds a message, the assistant's by default. */
59
+ messageAdd(entity: ChatEntity, message: Partial<ChatMessage>): void
60
+ replyStart(entity: ChatEntity): void
61
+ replyEnd(entity: ChatEntity): void
62
+ replyFail(entity: ChatEntity, error: string): void
63
+ messagesClear(entity: ChatEntity): void
64
+ render(entity: ChatEntity, api: Api): TemplateResult
65
+ renderChat(props: ChatProps): TemplateResult
66
+ renderMessage(props: ChatProps, message: ChatMessage): TemplateResult
67
+ renderContent(props: ChatProps, message: ChatMessage): unknown
68
+ renderTyping(props: ChatProps): TemplateResult
69
+ renderSuggestions(props: ChatProps): TemplateResult
70
+ renderComposer(props: ChatProps): TemplateResult
71
+ }
72
+
73
+ export declare const Chat: ChatType
package/types/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { Avatar } from "./data-display/avatar"
16
16
  export { Badge } from "./data-display/badge"
17
17
  export { BeforeAfter } from "./data-display/before-after"
18
18
  export { Carousel } from "./data-display/carousel"
19
+ export { Chat } from "./data-display/chat"
19
20
  export { Chip } from "./data-display/chip"
20
21
  export { DataGrid } from "./data-display/data-grid"
21
22
  export { Divider } from "./data-display/divider"