Skip to content

Commit bb45c62

Browse files
authored
Merge pull request ChatGPTNextWeb#45 from Yidadaa/bugfix-0326
v1.3 Stop and Retry Button
2 parents 4180363 + 1e89fe1 commit bb45c62

File tree

11 files changed

+113
-22
lines changed

11 files changed

+113
-22
lines changed

app/components/home.tsx

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Locale from "../locales";
2727

2828
import dynamic from "next/dynamic";
2929
import { REPO_URL } from "../constant";
30+
import { ControllerPool } from "../requests";
3031

3132
export function Loading(props: { noLogo?: boolean }) {
3233
return (
@@ -146,28 +147,67 @@ function useSubmitHandler() {
146147
export function Chat(props: { showSideBar?: () => void }) {
147148
type RenderMessage = Message & { preview?: boolean };
148149

149-
const session = useChatStore((state) => state.currentSession());
150+
const [session, sessionIndex] = useChatStore((state) => [
151+
state.currentSession(),
152+
state.currentSessionIndex,
153+
]);
150154
const [userInput, setUserInput] = useState("");
151155
const [isLoading, setIsLoading] = useState(false);
152156
const { submitKey, shouldSubmit } = useSubmitHandler();
153157

154158
const onUserInput = useChatStore((state) => state.onUserInput);
159+
160+
// submit user input
155161
const onUserSubmit = () => {
156162
if (userInput.length <= 0) return;
157163
setIsLoading(true);
158164
onUserInput(userInput).then(() => setIsLoading(false));
159165
setUserInput("");
160166
};
167+
168+
// stop response
169+
const onUserStop = (messageIndex: number) => {
170+
console.log(ControllerPool, sessionIndex, messageIndex);
171+
ControllerPool.stop(sessionIndex, messageIndex);
172+
};
173+
174+
// check if should send message
161175
const onInputKeyDown = (e: KeyboardEvent) => {
162176
if (shouldSubmit(e)) {
163177
onUserSubmit();
164178
e.preventDefault();
165179
}
166180
};
181+
const onRightClick = (e: any, message: Message) => {
182+
// auto fill user input
183+
if (message.role === "user") {
184+
setUserInput(message.content);
185+
}
186+
187+
// copy to clipboard
188+
if (selectOrCopy(e.currentTarget, message.content)) {
189+
e.preventDefault();
190+
}
191+
};
192+
193+
const onResend = (botIndex: number) => {
194+
// find last user input message and resend
195+
for (let i = botIndex; i >= 0; i -= 1) {
196+
if (messages[i].role === "user") {
197+
setIsLoading(true);
198+
onUserInput(messages[i].content).then(() => setIsLoading(false));
199+
return;
200+
}
201+
}
202+
};
203+
204+
// for auto-scroll
167205
const latestMessageRef = useRef<HTMLDivElement>(null);
168206

169-
const [hoveringMessage, setHoveringMessage] = useState(false);
207+
// wont scroll while hovering messages
208+
const [autoScroll, setAutoScroll] = useState(false);
170209

210+
// preview messages
171211
const messages = (session.messages as RenderMessage[])
172212
.concat(
173213
isLoading
@@ -194,10 +234,11 @@ export function Chat(props: { showSideBar?: () => void }) {
194234
: []
195235
);
196236

237+
// auto scroll
197238
useLayoutEffect(() => {
198239
setTimeout(() => {
199240
const dom = latestMessageRef.current;
200-
if (dom && !isIOS() && !hoveringMessage) {
241+
if (dom && !isIOS() && autoScroll) {
201242
dom.scrollIntoView({
202243
behavior: "smooth",
203244
block: "end",
@@ -252,15 +293,7 @@ export function Chat(props: { showSideBar?: () => void }) {
252293
</div>
253294
</div>
254295

255-
<div
256-
className={styles["chat-body"]}
257-
onMouseOver={() => {
258-
setHoveringMessage(true);
259-
}}
260-
onMouseOut={() => {
261-
setHoveringMessage(false);
262-
}}
263-
>
296+
<div className={styles["chat-body"]}>
264297
{messages.map((message, i) => {
265298
const isUser = message.role === "user";
266299

@@ -283,13 +316,20 @@ export function Chat(props: { showSideBar?: () => void }) {
283316
<div className={styles["chat-message-item"]}>
284317
{!isUser && (
285318
<div className={styles["chat-message-top-actions"]}>
286-
{message.streaming && (
319+
{message.streaming ? (
287320
<div
288321
className={styles["chat-message-top-action"]}
289-
onClick={() => showToast(Locale.WIP)}
322+
onClick={() => onUserStop(i)}
290323
>
291324
{Locale.Chat.Actions.Stop}
292325
</div>
326+
) : (
327+
<div
328+
className={styles["chat-message-top-action"]}
329+
onClick={() => onResend(i)}
330+
>
331+
{Locale.Chat.Actions.Retry}
332+
</div>
293333
)}
294334

295335
<div
@@ -306,11 +346,7 @@ export function Chat(props: { showSideBar?: () => void }) {
306346
) : (
307347
<div
308348
className="markdown-body"
309-
onContextMenu={(e) => {
310-
if (selectOrCopy(e.currentTarget, message.content)) {
311-
e.preventDefault();
312-
}
313-
}}
349+
onContextMenu={(e) => onRightClick(e, message)}
314350
>
315351
<Markdown content={message.content} />
316352
</div>
@@ -341,6 +377,9 @@ export function Chat(props: { showSideBar?: () => void }) {
341377
onInput={(e) => setUserInput(e.currentTarget.value)}
342378
value={userInput}
343379
onKeyDown={(e) => onInputKeyDown(e as any)}
380+
onFocus={() => setAutoScroll(true)}
381+
onBlur={() => setAutoScroll(false)}
382+
autoFocus
344383
/>
345384
<IconButton
346385
icon={<SendWhiteIcon />}

app/locales/cn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const cn = {
1414
Export: "导出聊天记录",
1515
Copy: "复制",
1616
Stop: "停止",
17+
Retry: "重试",
1718
},
1819
Typing: "正在输入…",
1920
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,

app/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const en: LocaleType = {
1717
Export: "Export All Messages as Markdown",
1818
Copy: "Copy",
1919
Stop: "Stop",
20+
Retry: "Retry",
2021
},
2122
Typing: "Typing…",
2223
Input: (submitKey: string) =>

app/requests.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export async function requestChatStream(
6060
modelConfig?: ModelConfig;
6161
onMessage: (message: string, done: boolean) => void;
6262
onError: (error: Error) => void;
63+
onController?: (controller: AbortController) => void;
6364
}
6465
) {
6566
const req = makeRequestParam(messages, {
@@ -96,12 +97,12 @@ export async function requestChatStream(
9697
controller.abort();
9798
};
9899

99-
console.log(res);
100-
101100
if (res.ok) {
102101
const reader = res.body?.getReader();
103102
const decoder = new TextDecoder();
104103

104+
options?.onController?.(controller);
105+
105106
while (true) {
106107
// handle time out, will stop if no response in 10 secs
107108
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
@@ -146,3 +147,34 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
146147

147148
return res.choices.at(0)?.message?.content ?? "";
148149
}
150+
151+
// To store message streaming controller
152+
export const ControllerPool = {
153+
controllers: {} as Record<string, AbortController>,
154+
155+
addController(
156+
sessionIndex: number,
157+
messageIndex: number,
158+
controller: AbortController
159+
) {
160+
const key = this.key(sessionIndex, messageIndex);
161+
this.controllers[key] = controller;
162+
return key;
163+
},
164+
165+
stop(sessionIndex: number, messageIndex: number) {
166+
const key = this.key(sessionIndex, messageIndex);
167+
const controller = this.controllers[key];
168+
console.log(controller);
169+
controller?.abort();
170+
},
171+
172+
remove(sessionIndex: number, messageIndex: number) {
173+
const key = this.key(sessionIndex, messageIndex);
174+
delete this.controllers[key];
175+
},
176+
177+
key(sessionIndex: number, messageIndex: number) {
178+
return `${sessionIndex},${messageIndex}`;
179+
},
180+
};

app/store/app.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { create } from "zustand";
22
import { persist } from "zustand/middleware";
33

44
import { type ChatCompletionResponseMessage } from "openai";
5-
import { requestChatStream, requestWithPrompt } from "../requests";
5+
import {
6+
ControllerPool,
7+
requestChatStream,
8+
requestWithPrompt,
9+
} from "../requests";
610
import { trimTopic } from "../utils";
711

812
import Locale from "../locales";
@@ -296,20 +300,25 @@ export const useChatStore = create<ChatStore>()(
296300
// get recent messages
297301
const recentMessages = get().getMessagesWithMemory();
298302
const sendMessages = recentMessages.concat(userMessage);
303+
const sessionIndex = get().currentSessionIndex;
304+
const messageIndex = get().currentSession().messages.length + 1;
299305

300306
// save user's and bot's message
301307
get().updateCurrentSession((session) => {
302308
session.messages.push(userMessage);
303309
session.messages.push(botMessage);
304310
});
305311

312+
// make request
306313
console.log("[User Input] ", sendMessages);
307314
requestChatStream(sendMessages, {
308315
onMessage(content, done) {
316+
// stream response
309317
if (done) {
310318
botMessage.streaming = false;
311319
botMessage.content = content;
312320
get().onNewMessage(botMessage);
321+
ControllerPool.remove(sessionIndex, messageIndex);
313322
} else {
314323
botMessage.content = content;
315324
set(() => ({}));
@@ -319,6 +328,15 @@ export const useChatStore = create<ChatStore>()(
319328
botMessage.content += "\n\n" + Locale.Store.Error;
320329
botMessage.streaming = false;
321330
set(() => ({}));
331+
ControllerPool.remove(sessionIndex, messageIndex);
332+
},
333+
onController(controller) {
334+
// collect controller for stop/retry
335+
ControllerPool.addController(
336+
sessionIndex,
337+
messageIndex,
338+
controller
339+
);
322340
},
323341
filterBot: !get().config.sendBotMessages,
324342
modelConfig: get().config.modelConfig,

public/android-chrome-192x192.png

11.7 KB
Loading

public/android-chrome-512x512.png

23 KB
Loading

public/apple-touch-icon.png

10.6 KB
Loading

public/favicon-16x16.png

-24 Bytes
Loading

public/favicon-32x32.png

818 Bytes
Loading

0 commit comments

Comments
 (0)