@webority/chat-react 0.0.0-stage → 0.21.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/LICENSE +5 -0
- package/README.md +232 -2
- package/dist/chat-react.js +1188 -0
- package/dist/types/client.d.ts +208 -0
- package/dist/types/components/ActionCard.d.ts +27 -0
- package/dist/types/components/ChatComposer.d.ts +20 -0
- package/dist/types/components/ChatConversationList.d.ts +20 -0
- package/dist/types/components/ChatLauncher.d.ts +23 -0
- package/dist/types/components/ChatMessage.d.ts +31 -0
- package/dist/types/components/ChatPanel.d.ts +52 -0
- package/dist/types/components/ChatThread.d.ts +30 -0
- package/dist/types/components/ReplyRating.d.ts +16 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/labels.d.ts +69 -0
- package/dist/types/lib/format.d.ts +8 -0
- package/dist/types/lib/sse.d.ts +33 -0
- package/dist/types/markdown.d.ts +97 -0
- package/dist/types/useChat.d.ts +69 -0
- package/package.json +58 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) Webority Technologies. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code are proprietary and confidential to
|
|
4
|
+
Webority Technologies. Use is restricted to Webority products and teams under
|
|
5
|
+
internal license. Redistribution outside Webority Technologies is prohibited.
|
package/README.md
CHANGED
|
@@ -1,3 +1,233 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @webority/chat-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An AI assistant for a React portal: a client that streams the assistant's reply, a chat hook with
|
|
4
|
+
no store of its own, and the pieces of the assistant's UI (a floating launcher, a side panel, the
|
|
5
|
+
conversation, reply ratings, a past-chats list and support action cards). It talks to the chat
|
|
6
|
+
endpoint the product's backend hosts with `Webority.Ai.Chat.AspNetCore`, and it is built on
|
|
7
|
+
`@webority/ui-react` and `@webority/theme`, so it looks like the rest of the portal with no extra
|
|
8
|
+
CSS.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @webority/chat-react @webority/ui-react react-bootstrap bootstrap@5.3.3
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@webority/ui-react`, `react`, `react-dom`, `react-bootstrap` and `bootstrap` are peer
|
|
17
|
+
dependencies: the portal already has them. Pin `@webority/chat-react` to the same exact version as
|
|
18
|
+
`@webority/ui-react`.
|
|
19
|
+
|
|
20
|
+
## Wire it up
|
|
21
|
+
|
|
22
|
+
Give the client the portal's own axios instance. That instance already carries the bearer token,
|
|
23
|
+
the tenant headers and the refresh on 401, so the chat calls sign in the same way as every other
|
|
24
|
+
call. The package never handles auth.
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
import { ChatLauncher, ChatPanel, createChatClient, useChat } from "@webority/chat-react";
|
|
28
|
+
import { createSupportClient } from "@webority/support-react";
|
|
29
|
+
import { useState } from "react";
|
|
30
|
+
import { Link, useLocation, useNavigate } from "react-router-dom";
|
|
31
|
+
import { userClient } from "./services/api/clients";
|
|
32
|
+
|
|
33
|
+
const chatClient = createChatClient(userClient, { base: "/api/assistant" });
|
|
34
|
+
const supportClient = createSupportClient(userClient, "/api/support");
|
|
35
|
+
|
|
36
|
+
// Reply links look like [label](scheme:kind/id); map the ones the product knows to a route.
|
|
37
|
+
const resolveLink = (href) => {
|
|
38
|
+
const [scheme, rest] = href.split(":");
|
|
39
|
+
if (scheme === "ticket") return `/support?ticket=${rest.split("/")[1]}`;
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function Assistant() {
|
|
44
|
+
const [open, setOpen] = useState(false);
|
|
45
|
+
const navigate = useNavigate();
|
|
46
|
+
const { pathname } = useLocation();
|
|
47
|
+
const chat = useChat({ client: chatClient, pageContext: { path: pathname } });
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<ChatLauncher open={open} onClick={() => setOpen(true)} style={{ bottom: "5rem" }} />
|
|
52
|
+
<ChatPanel
|
|
53
|
+
chat={chat}
|
|
54
|
+
open={open}
|
|
55
|
+
onClose={() => setOpen(false)}
|
|
56
|
+
resolveLink={resolveLink}
|
|
57
|
+
linkComponent={Link}
|
|
58
|
+
confirmAction={supportClient.confirmAction}
|
|
59
|
+
onViewTicket={(ticket) => navigate(`/support?ticket=${ticket.id}`)}
|
|
60
|
+
/>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Call `chat.reset()` when the person signs out or switches account, so nothing of one person's chat
|
|
67
|
+
shows to the next. The hook keeps its state in the component that calls it; mount it once, above
|
|
68
|
+
the routes, so the conversation survives navigation.
|
|
69
|
+
|
|
70
|
+
The streamed reply is read through axios's XHR adapter, the browser default. An instance forced
|
|
71
|
+
onto the fetch adapter still works, but the reply then appears whole at the end.
|
|
72
|
+
|
|
73
|
+
## The client
|
|
74
|
+
|
|
75
|
+
`createChatClient(http, { base })` takes an axios instance (`get`, `post`, `put`, `delete`
|
|
76
|
+
resolving to `{ data }`) and the prefix the API maps the chat endpoints under. Every method
|
|
77
|
+
rejects with a `ChatApiError` carrying `code`, `status`, `resetsAtUtc` and a `message` written for
|
|
78
|
+
the person, which the components show as-is. `code` is the server's code, `network` when no
|
|
79
|
+
response arrived, or `aborted` when the request was cancelled.
|
|
80
|
+
|
|
81
|
+
| Method | Request |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `sendMessage({ message, conversationId, pageContext }, { signal, onEvent })` | `POST {base}/chat`, streamed; resolves with the `done` event |
|
|
84
|
+
| `getAllowance()` | `GET {base}/allowance` |
|
|
85
|
+
| `listConversations({ skip, take })` | `GET {base}/conversations?skip=&take=` |
|
|
86
|
+
| `getConversation(id)` | `GET {base}/conversations/{id}` |
|
|
87
|
+
| `deleteConversation(id)` | `DELETE {base}/conversations/{id}` |
|
|
88
|
+
| `rateMessage(conversationId, messageId, { rating, note })` | `PUT {base}/conversations/{conversationId}/messages/{messageId}/rating` |
|
|
89
|
+
|
|
90
|
+
`onEvent(name, data)` is called with `delta`, `reset` and `actions` as they arrive.
|
|
91
|
+
`createSseParser(onEvent)` is exported too, for reading the same stream elsewhere.
|
|
92
|
+
|
|
93
|
+
## The server contract
|
|
94
|
+
|
|
95
|
+
### `POST {base}/chat`
|
|
96
|
+
|
|
97
|
+
Request body, JSON: `{ message, conversationId?, pageContext? }`. Leave `conversationId` out to
|
|
98
|
+
start a conversation. `pageContext` is whatever the product chooses to describe the screen, and
|
|
99
|
+
it is passed through untouched.
|
|
100
|
+
|
|
101
|
+
A refusal before any reply is plain JSON `{ code, message, resetsAtUtc? }` with a non-2xx status:
|
|
102
|
+
|
|
103
|
+
| Status | `code` |
|
|
104
|
+
|---|---|
|
|
105
|
+
| 400 | `message-required`, `message-too-long` |
|
|
106
|
+
| 404 | `conversation-not-found` |
|
|
107
|
+
| 429 | `daily-limit`, with `resetsAtUtc` |
|
|
108
|
+
| 503 | `assistant-at-capacity`, `assistant-disabled`, `assistant-unavailable` |
|
|
109
|
+
|
|
110
|
+
Otherwise the response is `text/event-stream`:
|
|
111
|
+
|
|
112
|
+
| Event | Data | What the client does |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `delta` | `{ text }` | Appends the text. |
|
|
115
|
+
| `reset` | `{}` | Discards the text shown so far. |
|
|
116
|
+
| `actions` | `{ actions: [{ token, kind, ticketId, ticketNumber, category, subject, body, expiresDateTimeUtc }] }` | Holds support cards; `kind` is `RaiseTicket`, `Reply` or `Reopen`. |
|
|
117
|
+
| `done` | `{ conversationId, messageId, reply, messagesLeftToday }` | Ends the turn. `reply` is the reply of record and replaces the streamed text. `messageId` is null when storing failed; `messagesLeftToday` is null with no daily limit. |
|
|
118
|
+
| `error` | `{ code, message }` | Ends the turn with a failure. |
|
|
119
|
+
|
|
120
|
+
Exactly one `done` or `error` ends each stream.
|
|
121
|
+
|
|
122
|
+
### Read endpoints the product implements
|
|
123
|
+
|
|
124
|
+
The chat endpoint writes one turn. The product hosts these beside it, in this shape, for the
|
|
125
|
+
allowance, past chats and ratings:
|
|
126
|
+
|
|
127
|
+
| Request | Response |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `GET {base}/allowance` | `{ messagesLeftToday }`, null with no daily limit |
|
|
130
|
+
| `GET {base}/conversations?skip&take` | `[{ publicId, title, createdDateTimeUtc, lastMessageDateTimeUtc }]`, newest first |
|
|
131
|
+
| `GET {base}/conversations/{id}` | `{ publicId, title, messages: [{ publicId, role, content, actionsJson, rating, ratingNote, createdDateTimeUtc }] }` |
|
|
132
|
+
| `DELETE {base}/conversations/{id}` | any 2xx |
|
|
133
|
+
| `PUT {base}/conversations/{conversationId}/messages/{messageId}/rating` | body `{ rating, note }`; any 2xx |
|
|
134
|
+
|
|
135
|
+
`role` is `User` or `Assistant`; `rating` is `Up`, `Down` or null; `actionsJson` is the support
|
|
136
|
+
actions offered with a reply, as a JSON array, or null. A failure answers `{ code, message }`.
|
|
137
|
+
|
|
138
|
+
### Support actions
|
|
139
|
+
|
|
140
|
+
A support card's Confirm calls the `confirmAction(token)` it is given. With
|
|
141
|
+
`@webority/support-react` that is `supportClient.confirmAction`, which sends
|
|
142
|
+
`POST {supportBase}/actions/confirm` with `{ token }` and resolves to the ticket it acted on. Nothing
|
|
143
|
+
reaches support until the person confirms.
|
|
144
|
+
|
|
145
|
+
## The hook
|
|
146
|
+
|
|
147
|
+
`useChat({ client, pageContext })` returns:
|
|
148
|
+
|
|
149
|
+
| Field | What it is |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `turns` | `[{ id, question, answer, streaming, error, actions, messageId, rating, ratingNote }]`, oldest first |
|
|
152
|
+
| `conversationId` | The conversation being continued, or null |
|
|
153
|
+
| `messagesLeftToday` | From the last answer or `refreshAllowance()`; null with no daily limit |
|
|
154
|
+
| `streaming` | An answer is arriving |
|
|
155
|
+
| `loading`, `loadError` | `openConversation` is loading, or failed |
|
|
156
|
+
| `submit(message)` | Asks a question. Ignored while an answer is still arriving. |
|
|
157
|
+
| `openConversation(id)` | Replaces the thread with a stored conversation |
|
|
158
|
+
| `newConversation()` | Clears the thread; the next question starts a conversation |
|
|
159
|
+
| `rate(turnId, rating, note?)` | Saves a rating, shown at once; a failed save puts the old one back and rejects |
|
|
160
|
+
| `refreshAllowance()` | Reads the questions left today |
|
|
161
|
+
| `reset()` | Clears everything, for sign-out or an account switch |
|
|
162
|
+
|
|
163
|
+
An answer still arriving is cancelled on `reset`, `newConversation`, `openConversation` and
|
|
164
|
+
unmount. A failed turn keeps the question, drops the half-streamed text and carries `error`
|
|
165
|
+
(`{ code, message, resetsAtUtc }`). After `daily-limit`, `messagesLeftToday` becomes 0; after
|
|
166
|
+
`conversation-not-found`, the next question starts a new conversation.
|
|
167
|
+
|
|
168
|
+
## Components
|
|
169
|
+
|
|
170
|
+
Every component takes `labels`, any subset of `DEFAULT_CHAT_LABELS`, to change its wording.
|
|
171
|
+
|
|
172
|
+
### `<ChatLauncher />`
|
|
173
|
+
|
|
174
|
+
The floating pill that opens the panel. `onClick`, `open` (announced on the button), `position`
|
|
175
|
+
(`end` or `start`, the bottom corner), `icon`, and `className` and `style` on the floating
|
|
176
|
+
wrapper to move it clear of other fixed controls, such as `ReportIssue`. The launcher is left out
|
|
177
|
+
of `ReportIssue`'s page capture.
|
|
178
|
+
|
|
179
|
+
### `<ChatPanel />`
|
|
180
|
+
|
|
181
|
+
The drawer: 440px from the right (`width`), the full screen on a phone. Focus stays inside it,
|
|
182
|
+
Escape closes it, and the question box takes focus when it opens.
|
|
183
|
+
|
|
184
|
+
| Prop | What it does |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `chat` | The `useChat` result. |
|
|
187
|
+
| `open`, `onClose` | Open state and close handler. |
|
|
188
|
+
| `title`, `titleIcon` | Header title (default "Assistant") and its library icon. |
|
|
189
|
+
| `headerActions` | Extra header controls before New chat, e.g. a past-chats button. |
|
|
190
|
+
| `empty` | Shown before the first question. |
|
|
191
|
+
| `disclaimer` | The footnote under the question box. |
|
|
192
|
+
| `maxLength` | The server's message limit. Default 4000. |
|
|
193
|
+
| `allowanceThreshold` | Show the questions left once this many or fewer remain. Default 10. |
|
|
194
|
+
| `resolveLink`, `linkComponent` | Reply links; see Replies below. |
|
|
195
|
+
| `confirmAction`, `onViewTicket` | Support action cards; see `ActionCard`. |
|
|
196
|
+
| `rating` | Offer ratings under stored replies. Default true. |
|
|
197
|
+
|
|
198
|
+
### `<ChatThread />`, `<ChatMessage />`, `<ChatComposer />`
|
|
199
|
+
|
|
200
|
+
The panel's parts, for a product that lays out its own assistant page. `ChatThread` takes
|
|
201
|
+
`turns`, `empty`, `onRate`, `resolveLink`, `linkComponent`, `confirmAction` and `onViewTicket`.
|
|
202
|
+
Each answer is a polite live region, marked busy while it streams. `ChatComposer` takes
|
|
203
|
+
`onSubmit`, `busy`, `disabled`, `maxLength` and `inputRef`; Enter sends, Shift and Enter starts a
|
|
204
|
+
new line.
|
|
205
|
+
|
|
206
|
+
### `<ReplyRating />`
|
|
207
|
+
|
|
208
|
+
Helpful and Not helpful under a reply. Either saves at once; Not helpful then offers an optional
|
|
209
|
+
note, saved with the same rating. Takes `rating` and `onRate(rating, note?)`.
|
|
210
|
+
|
|
211
|
+
### `<ChatConversationList />`
|
|
212
|
+
|
|
213
|
+
Past chats, newest first, for a page or a panel: `client`, `onOpen(conversation)`,
|
|
214
|
+
`onDeleted(id)` and `pageSize` (default 20). It pages with Show more and deletes after a confirm.
|
|
215
|
+
|
|
216
|
+
### `<ActionCard />`
|
|
217
|
+
|
|
218
|
+
One support action under a reply: what will be sent, then Confirm or Not now. It moves through
|
|
219
|
+
sending to done (with View ticket when `onViewTicket` is given), and an expired action shows
|
|
220
|
+
without a button. Takes `action`, `confirmAction(token)` and `onViewTicket(ticket)`.
|
|
221
|
+
|
|
222
|
+
## Replies
|
|
223
|
+
|
|
224
|
+
`<ChatMarkdown text resolveLink linkComponent />` renders a reply from a small, safe subset of
|
|
225
|
+
Markdown: paragraphs, bold, italics, inline code, bullet and numbered lists, simple tables,
|
|
226
|
+
headings (as bold lines) and code blocks. It never renders HTML: every character reaches the page
|
|
227
|
+
as text. Text still arriving updates the last block in place, and an unclosed `**` shows as typed
|
|
228
|
+
until its partner arrives.
|
|
229
|
+
|
|
230
|
+
A link shows only when its href has the form `scheme:kind/id` (for example `ticket:support/3f2a`)
|
|
231
|
+
and `resolveLink(href)` returns an in-app route starting with `/`. Any other link, including every
|
|
232
|
+
web address, shows its label as plain text. Pass the router's `Link` as `linkComponent` so a link
|
|
233
|
+
navigates inside the portal.
|