@hibira-io/chat-ui 0.1.0 → 0.2.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/README.md +28 -0
- package/hibira.d.ts +107 -0
- package/hibira.js +187 -0
- package/package.json +9 -2
- package/react.d.ts +2 -0
- package/react.js +8 -1
package/README.md
CHANGED
|
@@ -78,6 +78,34 @@ conversation.start();
|
|
|
78
78
|
|
|
79
79
|
`parseMarkdown(text, { origin, imageOrigins })` returns a plain block tree you can render with anything.
|
|
80
80
|
|
|
81
|
+
## Hibira Chat API
|
|
82
|
+
|
|
83
|
+
With the [Hibira Chat API](https://hibira.io) Hibira stores the messages, so
|
|
84
|
+
you write no `load` or `send` routes. Your server creates conversations and
|
|
85
|
+
gives each signed-in user a **user token** (an HS256 JWT signed with the app
|
|
86
|
+
secret, `{ sub, exp }`); the page talks to Hibira directly.
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
import Pusher from 'pusher-js';
|
|
90
|
+
import { createChatClient } from '@hibira-io/chat-ui/hibira';
|
|
91
|
+
import { Chat, useChatConversation } from '@hibira-io/chat-ui/react';
|
|
92
|
+
|
|
93
|
+
const client = createChatClient({ appId: 'your-app-id', tokenUrl: '/hibira/token' });
|
|
94
|
+
const pusher = new Pusher('your-key', { wsHost: 'ws.hibira.io', userAuthentication: { endpoint: '/pusher/user-auth' } });
|
|
95
|
+
pusher.signin(); // Chat API events arrive on the user's own channel
|
|
96
|
+
|
|
97
|
+
function Trip({ userId }) {
|
|
98
|
+
const conversation = useChatConversation(client, 'trip-1', { pusher, me: userId });
|
|
99
|
+
return <Chat conversation={conversation} me={userId} locale="zh-TW" />;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The client does the rest of the API as the token's user: `conversations()`
|
|
104
|
+
(with `unread`), `messages()`, `search(id, q)`, `send(id, { text, attachments })`,
|
|
105
|
+
`edit()`, `remove()`, `react(id, seq, emoji, on)`, `read()`, `typing()`,
|
|
106
|
+
`report()` and `upload(id, file)` (presigned, up to 25 MB). Edits, deletes and
|
|
107
|
+
reactions from other members update the thread live through `pusher.user`.
|
|
108
|
+
|
|
81
109
|
## Server (Node)
|
|
82
110
|
|
|
83
111
|
```js
|
package/hibira.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ChatMessage, Conversation } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_HOST: string;
|
|
4
|
+
export const CHAT_EVENTS: readonly string[];
|
|
5
|
+
|
|
6
|
+
export interface ChatClientOptions {
|
|
7
|
+
/** The Hibira app ID. */
|
|
8
|
+
appId: string;
|
|
9
|
+
/** The user token, or a function returning one; called again before it expires and after a 401. */
|
|
10
|
+
token?: string | (() => string | Promise<string>);
|
|
11
|
+
/** Instead of token: a URL on your site that answers { token }. */
|
|
12
|
+
tokenUrl?: string;
|
|
13
|
+
/** The HTTP API host (default api.hibira.io); may include http://. */
|
|
14
|
+
host?: string;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Attachment {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
content_type: string;
|
|
22
|
+
size: number;
|
|
23
|
+
/** A download link, valid for an hour. */
|
|
24
|
+
url?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A message of the Hibira Chat API. */
|
|
28
|
+
export interface ChatApiMessage extends ChatMessage {
|
|
29
|
+
id: string;
|
|
30
|
+
kind?: string;
|
|
31
|
+
created_at: string;
|
|
32
|
+
edited_at?: string;
|
|
33
|
+
deleted_at?: string;
|
|
34
|
+
/** Each emoji and the members who reacted, in order. */
|
|
35
|
+
reactions?: Record<string, string[]>;
|
|
36
|
+
attachments?: Attachment[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatApiConversation {
|
|
40
|
+
id: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
members: { user_id: string; read_seq: number }[];
|
|
44
|
+
last_seq: number;
|
|
45
|
+
created_at: string;
|
|
46
|
+
updated_at: string;
|
|
47
|
+
/** In conversations(): last_seq minus the user's read_seq. */
|
|
48
|
+
unread?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SendInput {
|
|
52
|
+
text?: string;
|
|
53
|
+
name?: string;
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
attachments?: Attachment[] | { id: string }[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Report {
|
|
59
|
+
id: string;
|
|
60
|
+
conversation_id: string;
|
|
61
|
+
seq: number;
|
|
62
|
+
reported_by: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
message: ChatApiMessage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Opts = { signal?: AbortSignal };
|
|
68
|
+
|
|
69
|
+
export interface ChatClient {
|
|
70
|
+
/** The token's user (sub), once a token is known. */
|
|
71
|
+
user(): string;
|
|
72
|
+
conversations(options?: { limit?: number; before?: string } & Opts): Promise<{ conversations: ChatApiConversation[]; next: string }>;
|
|
73
|
+
conversation(id: string, options?: Opts): Promise<ChatApiConversation>;
|
|
74
|
+
messages(id: string, options?: { after?: number; last?: number; limit?: number } & Opts): Promise<{ messages: ChatApiMessage[]; last_seq: number }>;
|
|
75
|
+
search(id: string, q: string, options?: { before?: number | string; limit?: number } & Opts): Promise<{ messages: ChatApiMessage[]; next: string }>;
|
|
76
|
+
send(id: string, message: string | SendInput): Promise<{ message: ChatApiMessage; push?: { users: number; error?: string } }>;
|
|
77
|
+
edit(id: string, seq: number, change: string | { text?: string; metadata?: Record<string, unknown> }): Promise<ChatApiMessage>;
|
|
78
|
+
remove(id: string, seq: number): Promise<ChatApiMessage>;
|
|
79
|
+
react(id: string, seq: number, emoji: string, on?: boolean): Promise<ChatApiMessage>;
|
|
80
|
+
report(id: string, seq: number, reason?: string): Promise<Report>;
|
|
81
|
+
read(id: string, seq: number): Promise<{ read_seq: number }>;
|
|
82
|
+
typing(id: string, typing?: boolean): Promise<unknown>;
|
|
83
|
+
/** Uploads a File or Blob (up to 25 MB); send the result in a message's attachments. */
|
|
84
|
+
upload(id: string, file: Blob & { name?: string }, options?: { name?: string }): Promise<Attachment>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class ChatApiError extends Error {
|
|
88
|
+
status: number;
|
|
89
|
+
constructor(status: number, message?: string);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createChatClient(options: ChatClientOptions): ChatClient;
|
|
93
|
+
export function tokenUser(token: string): string;
|
|
94
|
+
export function onChatEvents(pusher: unknown, handler: (event: string, data: any) => void): () => void;
|
|
95
|
+
|
|
96
|
+
export interface ChatConversationOptions {
|
|
97
|
+
/** A pusher-js instance signed in as the user (pusher.signin()). Without one it polls. */
|
|
98
|
+
pusher?: unknown;
|
|
99
|
+
/** The user ID (default: the token's sub). */
|
|
100
|
+
me?: string;
|
|
101
|
+
pollMs?: number;
|
|
102
|
+
livePollMs?: number;
|
|
103
|
+
/** chat.read, chat.conversation, chat.left and chat.deleted for this conversation. */
|
|
104
|
+
onEvent?: (event: string, data: any) => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function chatConversation(client: ChatClient, id: string, options?: ChatConversationOptions): Conversation;
|
package/hibira.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// A client for the Hibira Chat API's user routes (/apps/{appId}/chat/me),
|
|
2
|
+
// and an adapter that turns one of its conversations into a
|
|
3
|
+
// createConversation() for the components. The user token comes from your
|
|
4
|
+
// server (an HS256 JWT signed with the app secret, { sub, exp }); messages
|
|
5
|
+
// are stored by Hibira, so you write no load/send routes.
|
|
6
|
+
|
|
7
|
+
import { createConversation } from './conversation.js';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_HOST = 'api.hibira.io';
|
|
10
|
+
|
|
11
|
+
/** A user token's claims, read without checking them ({} when unreadable). */
|
|
12
|
+
function claims(token) {
|
|
13
|
+
try {
|
|
14
|
+
const part = String(token).split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
15
|
+
const bin = atob(part.padEnd(part.length + ((4 - (part.length % 4)) % 4), '='));
|
|
16
|
+
return JSON.parse(new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)))) ?? {};
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The user ID (sub) inside a user token, or '' when it cannot be read. */
|
|
23
|
+
export const tokenUser = (token) => String(claims(token).sub ?? '');
|
|
24
|
+
|
|
25
|
+
/** An error answered by the Chat API, with its HTTP status. */
|
|
26
|
+
export class ChatApiError extends Error {
|
|
27
|
+
constructor(status, message) {
|
|
28
|
+
super(message || `Chat API request failed (${status})`);
|
|
29
|
+
this.name = 'ChatApiError';
|
|
30
|
+
this.status = status;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The Chat API as the signed-in user.
|
|
36
|
+
*
|
|
37
|
+
* options:
|
|
38
|
+
* appId the Hibira app ID (required)
|
|
39
|
+
* token the user token: a string, or a function returning one (or a
|
|
40
|
+
* promise of one), called again before it expires and after a 401
|
|
41
|
+
* tokenUrl instead of token: a URL on your site that answers {token}
|
|
42
|
+
* host the HTTP API host (default api.hibira.io); may include http://
|
|
43
|
+
* fetch a fetch implementation (default globalThis.fetch)
|
|
44
|
+
*/
|
|
45
|
+
export function createChatClient(options = {}) {
|
|
46
|
+
if (!options.appId) throw new TypeError('createChatClient: appId is required');
|
|
47
|
+
if (!options.token && !options.tokenUrl) throw new TypeError('createChatClient: token or tokenUrl is required');
|
|
48
|
+
const doFetch = options.fetch ?? ((...a) => globalThis.fetch(...a));
|
|
49
|
+
let host = String(options.host ?? DEFAULT_HOST);
|
|
50
|
+
while (host.endsWith('/')) host = host.slice(0, -1);
|
|
51
|
+
const base = `${/^https?:\/\//.test(host) ? host : `https://${host}`}/apps/${encodeURIComponent(options.appId)}/chat/me`;
|
|
52
|
+
let token = typeof options.token === 'string' ? options.token : null;
|
|
53
|
+
let pending = null;
|
|
54
|
+
|
|
55
|
+
async function fetchToken() {
|
|
56
|
+
if (typeof options.token === 'string') return options.token;
|
|
57
|
+
if (typeof options.token === 'function') return options.token();
|
|
58
|
+
const res = await doFetch(options.tokenUrl, { credentials: 'include', headers: { Accept: 'application/json' } });
|
|
59
|
+
if (!res.ok) throw new ChatApiError(res.status, `token endpoint answered ${res.status}`);
|
|
60
|
+
return (await res.json()).token;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function currentToken(force) {
|
|
64
|
+
const exp = token ? claims(token).exp ?? 0 : 0;
|
|
65
|
+
if (!force && token && (exp === 0 || exp - Date.now() / 1000 > 30)) return Promise.resolve(token);
|
|
66
|
+
pending ??= Promise.resolve(fetchToken())
|
|
67
|
+
.then((t) => (token = t))
|
|
68
|
+
.finally(() => (pending = null));
|
|
69
|
+
return pending;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function call(method, path, { body, query, signal } = {}) {
|
|
73
|
+
const params = Object.entries(query ?? {}).filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
74
|
+
const qs = new URLSearchParams(params.map(([k, v]) => [k, String(v)])).toString();
|
|
75
|
+
const url = base + path + (qs ? `?${qs}` : '');
|
|
76
|
+
for (let attempt = 0; ; attempt++) {
|
|
77
|
+
const t = await currentToken(attempt > 0);
|
|
78
|
+
const res = await doFetch(url, {
|
|
79
|
+
method,
|
|
80
|
+
signal,
|
|
81
|
+
headers: { Authorization: `Bearer ${t}`, ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
|
|
82
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
83
|
+
});
|
|
84
|
+
if (res.status === 401 && attempt === 0 && typeof options.token !== 'string') continue;
|
|
85
|
+
const data = await res.json().catch(() => ({}));
|
|
86
|
+
if (!res.ok) throw new ChatApiError(res.status, data?.error);
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const conv = (id) => `/conversations/${encodeURIComponent(id)}`;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
/** The token's user, once a token is known. */
|
|
95
|
+
user: () => (token ? tokenUser(token) : ''),
|
|
96
|
+
conversations: ({ limit, before, signal } = {}) => call('GET', '/conversations', { query: { limit, before }, signal }),
|
|
97
|
+
conversation: (id, { signal } = {}) => call('GET', conv(id), { signal }).then((d) => d.conversation),
|
|
98
|
+
messages: (id, { after, last, limit, signal } = {}) => call('GET', `${conv(id)}/messages`, { query: { after, last, limit }, signal }),
|
|
99
|
+
search: (id, q, { before, limit, signal } = {}) => call('GET', `${conv(id)}/search`, { query: { q, before, limit }, signal }),
|
|
100
|
+
send: (id, message) => call('POST', `${conv(id)}/messages`, { body: typeof message === 'string' ? { text: message } : message }),
|
|
101
|
+
edit: (id, seq, change) => call('PUT', `${conv(id)}/messages/${seq}`, { body: typeof change === 'string' ? { text: change } : change }).then((d) => d.message),
|
|
102
|
+
remove: (id, seq) => call('DELETE', `${conv(id)}/messages/${seq}`).then((d) => d.message),
|
|
103
|
+
react: (id, seq, emoji, on = true) =>
|
|
104
|
+
(on
|
|
105
|
+
? call('POST', `${conv(id)}/messages/${seq}/reactions`, { body: { emoji } })
|
|
106
|
+
: call('DELETE', `${conv(id)}/messages/${seq}/reactions`, { query: { emoji } })
|
|
107
|
+
).then((d) => d.message),
|
|
108
|
+
report: (id, seq, reason) => call('POST', `${conv(id)}/messages/${seq}/reports`, { body: { reason } }).then((d) => d.report),
|
|
109
|
+
read: (id, seq) => call('POST', `${conv(id)}/read`, { body: { seq } }),
|
|
110
|
+
typing: (id, typing = true) => call('POST', `${conv(id)}/typing`, { body: { typing } }),
|
|
111
|
+
/** Uploads a File or Blob; send its result in a message's attachments. */
|
|
112
|
+
async upload(id, file, { name } = {}) {
|
|
113
|
+
const fileName = name ?? file?.name ?? 'file';
|
|
114
|
+
const { attachment, upload } = await call('POST', `${conv(id)}/attachments`, {
|
|
115
|
+
body: { name: fileName, content_type: file.type || 'application/octet-stream', size: file.size },
|
|
116
|
+
});
|
|
117
|
+
const res = await doFetch(upload.url, { method: upload.method ?? 'PUT', headers: upload.headers ?? {}, body: file });
|
|
118
|
+
if (!res.ok) throw new ChatApiError(res.status, 'Upload failed');
|
|
119
|
+
return attachment;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Chat API events, sent to each member's user channel (pusher.signin). */
|
|
125
|
+
export const CHAT_EVENTS = Object.freeze(['chat.message', 'chat.message_updated', 'chat.typing', 'chat.read', 'chat.conversation', 'chat.left', 'chat.deleted']);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Binds Chat API events on a signed-in pusher-js client; returns unbind.
|
|
129
|
+
* @param {any} pusher
|
|
130
|
+
* @param {(event: string, data: any) => void} handler
|
|
131
|
+
*/
|
|
132
|
+
export function onChatEvents(pusher, handler) {
|
|
133
|
+
const user = pusher?.user;
|
|
134
|
+
if (!user?.bind) return () => {};
|
|
135
|
+
const fns = CHAT_EVENTS.map((e) => {
|
|
136
|
+
const fn = (data) => handler(e, data);
|
|
137
|
+
user.bind(e, fn);
|
|
138
|
+
return [e, fn];
|
|
139
|
+
});
|
|
140
|
+
return () => fns.forEach(([e, fn]) => user.unbind?.(e, fn));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* One Chat API conversation as a createConversation(), for <Chat> and
|
|
145
|
+
* useConversation(). Live events come from pusher (signed in with
|
|
146
|
+
* pusher.signin()); without it, it polls.
|
|
147
|
+
* @param {ReturnType<typeof createChatClient>} client
|
|
148
|
+
* @param {string} id
|
|
149
|
+
* @param {{ pusher?: any, me?: string, pollMs?: number, livePollMs?: number, onEvent?: (event: string, data: any) => void }} [options]
|
|
150
|
+
*/
|
|
151
|
+
export function chatConversation(client, id, options = {}) {
|
|
152
|
+
const conversation = createConversation({
|
|
153
|
+
me: options.me ?? client.user(),
|
|
154
|
+
pusher: options.pusher,
|
|
155
|
+
pollMs: options.pollMs,
|
|
156
|
+
livePollMs: options.livePollMs,
|
|
157
|
+
onEvent: options.onEvent,
|
|
158
|
+
load: (after, signal) => client.messages(id, after ? { after, signal } : { last: 50, signal }),
|
|
159
|
+
send: (text) => client.send(id, { text }),
|
|
160
|
+
});
|
|
161
|
+
const start = conversation.start;
|
|
162
|
+
let unbind = () => {};
|
|
163
|
+
conversation.start = () => {
|
|
164
|
+
start();
|
|
165
|
+
unbind();
|
|
166
|
+
unbind = onChatEvents(options.pusher, (event, data) => {
|
|
167
|
+
if (data?.conversation_id !== id) return;
|
|
168
|
+
const me = options.me ?? client.user();
|
|
169
|
+
if (event === 'chat.message' || event === 'chat.message_updated') {
|
|
170
|
+
conversation.added(data.message);
|
|
171
|
+
if (event === 'chat.message') conversation.hint('message', { from: data.message?.sender, seq: data.message?.seq });
|
|
172
|
+
} else if (event === 'chat.typing') {
|
|
173
|
+
if (data.user_id !== me && data.typing !== false) conversation.hint('typing', { from: data.user_id });
|
|
174
|
+
} else {
|
|
175
|
+
options.onEvent?.(event, data);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return () => conversation.stop();
|
|
179
|
+
};
|
|
180
|
+
const stop = conversation.stop;
|
|
181
|
+
conversation.stop = () => {
|
|
182
|
+
unbind();
|
|
183
|
+
unbind = () => {};
|
|
184
|
+
stop();
|
|
185
|
+
};
|
|
186
|
+
return conversation;
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hibira-io/chat-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Agentic chat UI for support and AI agents over Hibira realtime channels: Markdown messages, agent steps, typed replies, follow-up suggestions and human handoff, for React",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Agentic chat UI for support and AI agents over Hibira realtime channels: Markdown messages, agent steps, typed replies, follow-up suggestions and human handoff, and a client for the Hibira Chat API, for React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chat",
|
|
7
7
|
"chat-ui",
|
|
8
|
+
"chat-api",
|
|
8
9
|
"ai",
|
|
9
10
|
"agent",
|
|
10
11
|
"ai-chat",
|
|
@@ -41,6 +42,10 @@
|
|
|
41
42
|
"types": "./react.d.ts",
|
|
42
43
|
"default": "./react.js"
|
|
43
44
|
},
|
|
45
|
+
"./hibira": {
|
|
46
|
+
"types": "./hibira.d.ts",
|
|
47
|
+
"default": "./hibira.js"
|
|
48
|
+
},
|
|
44
49
|
"./server": {
|
|
45
50
|
"types": "./server.d.ts",
|
|
46
51
|
"default": "./server.js"
|
|
@@ -58,6 +63,8 @@
|
|
|
58
63
|
"conversation.js",
|
|
59
64
|
"locales.js",
|
|
60
65
|
"types.d.ts",
|
|
66
|
+
"hibira.js",
|
|
67
|
+
"hibira.d.ts",
|
|
61
68
|
"react.js",
|
|
62
69
|
"react.d.ts",
|
|
63
70
|
"server.js",
|
package/react.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from 'react';
|
|
2
2
|
import type { AgentStep, ChatLabels, ChatMessage, Conversation, ConversationState, LabelOverrides } from './types.js';
|
|
3
|
+
import type { ChatClient, ChatConversationOptions } from './hibira.js';
|
|
3
4
|
|
|
4
5
|
export type * from './types.js';
|
|
5
6
|
export { createConversation } from './index.js';
|
|
@@ -29,6 +30,7 @@ export interface ChatThreadProps {
|
|
|
29
30
|
export function ChatThread(props: ChatThreadProps): ReactElement;
|
|
30
31
|
export function Chat(props: Omit<ChatThreadProps, 'messages' | 'typing' | 'step' | 'onSend'> & { conversation: Conversation | null }): ReactElement;
|
|
31
32
|
export function useConversation(conversation: Conversation | null): ConversationState;
|
|
33
|
+
export function useChatConversation(client: ChatClient | null, id: string | null, options?: ChatConversationOptions): Conversation | null;
|
|
32
34
|
export function Markdown(props: { text: string; locale?: string; labels?: LabelOverrides; origin?: string; imageOrigins?: string[] }): ReactElement;
|
|
33
35
|
export function AgentSteps(props: { typing: boolean; step?: AgentStep; locale?: string; labels?: ChatLabels }): ReactElement | null;
|
|
34
36
|
export function Suggestions(props: { items: string[]; onPick: (text: string) => void; disabled?: boolean; locale?: string; labels?: ChatLabels }): ReactElement | null;
|
package/react.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// buttons and a reply box. Import '@hibira-io/chat-ui/styles.css' for the
|
|
4
4
|
// default look.
|
|
5
5
|
|
|
6
|
-
import { createElement as h, Fragment, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
6
|
+
import { createElement as h, Fragment, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
7
7
|
import { parseMarkdown } from './markdown.js';
|
|
8
8
|
import { isAutomatic, STEPS } from './conversation.js';
|
|
9
9
|
import { format, labelsFor } from './locales.js';
|
|
10
|
+
import { chatConversation } from './hibira.js';
|
|
10
11
|
|
|
11
12
|
export { createConversation } from './conversation.js';
|
|
12
13
|
|
|
@@ -377,3 +378,9 @@ export function Chat({ conversation, ...props }) {
|
|
|
377
378
|
const state = useConversation(conversation);
|
|
378
379
|
return h(ChatThread, { messages: state.messages, typing: state.typing, step: state.step, onSend: conversation ? (t) => conversation.send(t) : undefined, ...props });
|
|
379
380
|
}
|
|
381
|
+
|
|
382
|
+
/** A Hibira Chat API conversation (createChatClient) for <Chat>, made once per client, id and pusher. */
|
|
383
|
+
export function useChatConversation(client, id, options = {}) {
|
|
384
|
+
const { pusher, me } = options;
|
|
385
|
+
return useMemo(() => (client && id ? chatConversation(client, id, options) : null), [client, id, pusher, me]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
386
|
+
}
|