Skip to content

Commit 3869f95

Browse files
committed
feat: optimize useChat behaviors
1 parent 1c97ef9 commit 3869f95

File tree

6 files changed

+146
-122
lines changed

6 files changed

+146
-122
lines changed

.changes/optimize-use-chat.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@matechat/react": patch:feat
3+
---
4+
5+
Optimize behavior of `useChat`:
6+
7+
- Add `throwOnEmptyBackend` option to `useChat` function.
8+
- Throw an error when `backend` is nullish and `throwOnEmptyBackend` is `true`.
9+
- Rename `isPending`to`pending` in `useChat` return value.
10+
- Allow empty `backend` in `useChat` function.

playground/src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { useState } from "react";
1+
import { StrictMode, useState } from "react";
22
import { Activate } from "./Activate";
33
import { Chat } from "./Chat";
44

55
export function App() {
66
const [activated, setActivated] = useState<boolean>(false);
77

88
return (
9-
<>
9+
<StrictMode>
1010
{activated ? (
1111
<Chat />
1212
) : (
1313
<Activate onActivate={() => setActivated(true)} />
1414
)}
15-
</>
15+
</StrictMode>
1616
);
1717
}

playground/src/Chat.tsx

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,34 @@ import type { MessageParam } from "../../dist/utils";
1414
import { useChat } from "../../dist/utils/chat";
1515
import { useMateChat } from "../../dist/utils/core";
1616

17-
export function Chat() {
18-
const initialMessages: MessageParam[] = [
19-
{
20-
id: "1",
21-
role: "user",
22-
content: "Hello, how are you?",
23-
avatar: {
24-
text: "U",
25-
},
26-
align: "right",
27-
},
28-
{
29-
id: "2",
30-
role: "assistant",
31-
content:
32-
"I'm doing well, thank you! How can I assist you today? \
17+
const initialMessages: MessageParam[] = [
18+
{
19+
id: "1",
20+
role: "user",
21+
content: "Hello, how are you?",
22+
align: "right",
23+
},
24+
{
25+
id: "2",
26+
role: "assistant",
27+
content:
28+
"I'm doing well, thank you! How can I assist you today? \
3329
I'm a language model, so I can understand and respond to a wide range of questions and requests. \
3430
I can help you with a variety of tasks, such as answering questions, providing information, or helping you with a specific problem.",
35-
avatar: {
36-
text: "A",
37-
},
38-
align: "left",
39-
},
40-
];
31+
align: "left",
32+
},
33+
];
4134

35+
export function Chat() {
4236
const [prompt, setPrompt] = useState("");
4337

4438
const { backend } = useMateChat();
45-
if (!backend) {
46-
throw new Error("Backend is not initialized");
47-
}
48-
const { messages, input, setMessages, isPending } = useChat(
39+
const { messages, input, setMessages, pending } = useChat(
4940
backend,
5041
initialMessages,
42+
{
43+
throwOnEmptyBackend: true,
44+
},
5145
);
5246

5347
const footer = useMemo(() => {
@@ -70,7 +64,7 @@ export function Chat() {
7064
className="px-4 w-full max-w-full"
7165
messages={messages}
7266
background="right-solid"
73-
isPending={isPending}
67+
isPending={pending}
7468
footer={footer}
7569
/>
7670
{messages.length === 0 && (
@@ -91,9 +85,7 @@ export function Chat() {
9185
)}
9286
<Sender
9387
className="w-full"
94-
initialMessage={prompt}
9588
input={input}
96-
onMessageChange={setPrompt}
9789
toolbar={
9890
<div className="flex flex-row justify-between w-full">
9991
<InputCount count={prompt.length} limit={500} />

src/bubble.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,23 +191,18 @@ export function Bubble({
191191
);
192192
}
193193

194-
export interface AvatarProps {
194+
export interface AvatarProps extends React.ComponentProps<"div"> {
195195
text?: string;
196196
imageUrl?: string;
197197
}
198198

199-
export function Avatar({
200-
className,
201-
text,
202-
imageUrl,
203-
...props
204-
}: React.ComponentProps<"div"> & AvatarProps) {
199+
export function Avatar({ className, text, imageUrl, ...props }: AvatarProps) {
205200
return (
206201
<div
207202
data-slot="avatar"
208203
className={twMerge(
209204
clsx(
210-
"flex items-center justify-center w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-800 dark:text-gray-200 text-gray-800",
205+
"flex items-center justify-center w-9 h-9 rounded-full bg-gray-300 dark:bg-gray-800 dark:text-gray-200 text-gray-800",
211206
className,
212207
),
213208
)}

src/utils/chat.ts

Lines changed: 108 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,132 @@
11
import { useCallback, useEffect, useState } from "react";
22
import type { InputOptions } from "./backend";
3-
import type { Backend, Events, EventTypes, MessageParam } from "./types";
3+
import type {
4+
Backend,
5+
Events,
6+
EventTypes,
7+
MessageParam,
8+
Nullable,
9+
} from "./types";
410

5-
export function useChat(
6-
backend: Backend,
7-
initialMessages: MessageParam[] = [],
8-
): {
11+
/**
12+
* Options for `useChat`.
13+
*/
14+
export interface UseChatOptions {
15+
/**
16+
* Whether to throw an error when the backend is nullish.
17+
* @description
18+
* If `true`, an error will be thrown when the backend is nullish.
19+
* The error will be thrown when calling `input` or `on` callback,
20+
* but not thrown immediately.
21+
* @default false
22+
*/
23+
throwOnEmptyBackend?: boolean;
24+
}
25+
26+
export interface UseChatReturn {
927
messages: MessageParam[];
1028
input: (prompt: string, options?: InputOptions) => Promise<void>;
11-
on: <K extends EventTypes["type"]>(type: K, handler: Events[K]) => () => void;
29+
on: <K extends EventTypes["type"]>(
30+
type: K,
31+
handler: Events[K],
32+
) => Nullable<() => void>;
1233
setMessages: React.Dispatch<React.SetStateAction<MessageParam[]>>;
13-
isPending: boolean;
14-
} {
34+
pending: boolean;
35+
}
36+
37+
export function useChat(
38+
backend?: Backend,
39+
initialMessages: MessageParam[] = [],
40+
options: UseChatOptions = {},
41+
): UseChatReturn {
1542
const [messages, setMessages] = useState<MessageParam[]>(initialMessages);
16-
const [isPending, setIsPending] = useState(false);
43+
const [pending, setPending] = useState(false);
1744

1845
const input = useCallback(
19-
async (prompt: string, options?: InputOptions) => {
20-
setIsPending(true);
21-
return backend.input(prompt, {
46+
async (prompt: string, inputOptions?: InputOptions) => {
47+
if (!backend && options.throwOnEmptyBackend) {
48+
throw new Error("Backend is not initialized");
49+
}
50+
return backend?.input(prompt, {
2251
messages,
23-
...options,
52+
...inputOptions,
2453
});
2554
},
26-
[backend, messages],
55+
[backend, messages, options.throwOnEmptyBackend],
2756
);
2857

2958
const on = useCallback(
30-
<K extends EventTypes["type"]>(
31-
type: K,
32-
handler: Events[K],
33-
): (() => void) => {
34-
return backend.on(type, handler);
59+
<K extends EventTypes["type"]>(type: K, handler: Events[K]) => {
60+
if (!backend && options.throwOnEmptyBackend) {
61+
throw new Error("Backend is not initialized");
62+
}
63+
return backend?.on(type, handler);
3564
},
36-
[backend],
65+
[backend, options, options.throwOnEmptyBackend],
3766
);
3867

3968
useEffect(() => {
40-
const cleanCbs: (() => void)[] = [
41-
backend.on("input", (event) => {
42-
setMessages((prevMessages) => [
43-
...prevMessages,
44-
{
45-
id: event.id,
46-
role: "user",
47-
name: "User",
48-
content: event.payload.prompt,
49-
avatar: {
50-
text: "U",
51-
},
52-
align: "right",
53-
},
54-
]);
55-
}),
56-
backend.on("message", (event) => {
57-
setMessages((prevMessages) => [...prevMessages, event.payload]);
58-
}),
59-
backend.on("error", (event) => {
60-
setIsPending(false);
61-
console.error("Error from backend:", event.payload.error);
62-
setMessages((prevMessages) => [
63-
...prevMessages,
64-
{
65-
id: event.id,
66-
role: "system",
67-
name: "Error",
68-
content: event.payload.error,
69-
align: "center",
70-
},
71-
]);
72-
}),
73-
backend.on("finish", () => {
74-
setIsPending(false);
75-
}),
76-
backend.on("chunk", (event) => {
77-
setIsPending(false);
78-
setMessages((prev) => {
79-
const lastMessage = prev[prev.length - 1];
80-
if (lastMessage && lastMessage.role === "assistant") {
81-
return [
82-
...prev.slice(0, -1),
69+
const cleanCbs: (() => void)[] = backend
70+
? [
71+
backend.on("input", (event) => {
72+
setPending(true);
73+
setMessages((prevMessages) => [
74+
...prevMessages,
8375
{
84-
...lastMessage,
85-
content: lastMessage.content + event.payload.chunk,
76+
id: event.id,
77+
role: "user",
78+
name: "User",
79+
content: event.payload.prompt,
80+
align: "right",
8681
},
87-
];
88-
}
89-
return [
90-
...prev,
91-
{
92-
id: event.id,
93-
role: "assistant",
94-
content: event.payload.chunk,
95-
avatar: {
96-
text: "A",
82+
]);
83+
}),
84+
backend.on("message", (event) => {
85+
setMessages((prevMessages) => [...prevMessages, event.payload]);
86+
}),
87+
backend.on("error", (event) => {
88+
setPending(false);
89+
console.error("Error from backend:", event.payload.error);
90+
setMessages((prevMessages) => [
91+
...prevMessages,
92+
{
93+
id: event.id,
94+
role: "system",
95+
name: "Error",
96+
content: event.payload.error,
97+
align: "center",
9798
},
98-
align: "left",
99-
},
100-
];
101-
});
102-
}),
103-
];
99+
]);
100+
}),
101+
backend.on("finish", () => {
102+
setPending(false);
103+
}),
104+
backend.on("chunk", (event) => {
105+
setPending(false);
106+
setMessages((prev) => {
107+
const lastMessage = prev[prev.length - 1];
108+
if (lastMessage && lastMessage.role === "assistant") {
109+
return [
110+
...prev.slice(0, -1),
111+
{
112+
...lastMessage,
113+
content: lastMessage.content + event.payload.chunk,
114+
},
115+
];
116+
}
117+
return [
118+
...prev,
119+
{
120+
id: event.id,
121+
role: "assistant",
122+
content: event.payload.chunk,
123+
align: "left",
124+
},
125+
];
126+
});
127+
}),
128+
]
129+
: [];
104130
return () => {
105131
for (const cb of cleanCbs) {
106132
cb();
@@ -109,5 +135,5 @@ export function useChat(
109135
};
110136
}, [backend]);
111137

112-
return { messages, input, on, setMessages, isPending };
138+
return { messages, input, on, setMessages, pending };
113139
}

src/utils/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface MessageParam {
5656
}
5757

5858
export type Awaitable<T> = T | Promise<T>;
59+
export type Nullable<T> = T | null | undefined;
5960

6061
/**
6162
* Backend interface for chatbot.

0 commit comments

Comments
 (0)