@webority/chat-react 0.0.0-stage → 0.22.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 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
- # Temporary Holding Version
1
+ # @webority/chat-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.