mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-09-27 21:56:38 +08:00
feat: thinking style optimize
This commit is contained in:
parent
ac9bdf642e
commit
78dd2d4258
@ -45,6 +45,7 @@ export interface MultimodalContent {
|
|||||||
export interface RequestMessage {
|
export interface RequestMessage {
|
||||||
role: MessageRole;
|
role: MessageRole;
|
||||||
content: string | MultimodalContent[];
|
content: string | MultimodalContent[];
|
||||||
|
reasoningContent?: string;
|
||||||
fileInfos?: FileInfo[];
|
fileInfos?: FileInfo[];
|
||||||
webSearchReferences?: TavilySearchResponse;
|
webSearchReferences?: TavilySearchResponse;
|
||||||
}
|
}
|
||||||
@ -93,6 +94,7 @@ export interface ChatOptions {
|
|||||||
|
|
||||||
onToolUpdate?: (toolName: string, toolInput: string) => void;
|
onToolUpdate?: (toolName: string, toolInput: string) => void;
|
||||||
onUpdate?: (message: string, chunk: string) => void;
|
onUpdate?: (message: string, chunk: string) => void;
|
||||||
|
onReasoningUpdate?: (message: string, chunk: string) => void;
|
||||||
onFinish: (message: string, responseRes: Response) => void;
|
onFinish: (message: string, responseRes: Response) => void;
|
||||||
onError?: (err: Error) => void;
|
onError?: (err: Error) => void;
|
||||||
onController?: (controller: AbortController) => void;
|
onController?: (controller: AbortController) => void;
|
||||||
|
@ -147,6 +147,7 @@ import {
|
|||||||
WebTranscriptionApi,
|
WebTranscriptionApi,
|
||||||
} from "../utils/speech";
|
} from "../utils/speech";
|
||||||
import { FileInfo } from "../client/platforms/utils";
|
import { FileInfo } from "../client/platforms/utils";
|
||||||
|
import { ThinkingContent } from "./thinking-content";
|
||||||
|
|
||||||
const ttsPlayer = createTTSPlayer();
|
const ttsPlayer = createTTSPlayer();
|
||||||
|
|
||||||
@ -2151,6 +2152,7 @@ function _Chat() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isUser && <ThinkingContent message={message} />}
|
||||||
<div className={styles["chat-message-item"]}>
|
<div className={styles["chat-message-item"]}>
|
||||||
<Markdown
|
<Markdown
|
||||||
key={message.streaming ? "loading" : "done"}
|
key={message.streaming ? "loading" : "done"}
|
||||||
|
106
app/components/thinking-content.module.scss
Normal file
106
app/components/thinking-content.module.scss
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
.thinking-container {
|
||||||
|
position: relative;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
|
||||||
|
.thinking-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--white);
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--border-in-light);
|
||||||
|
|
||||||
|
.thinking-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--black);
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--black);
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--black);
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
max-height: 50px;
|
||||||
|
overflow-y: hidden;
|
||||||
|
position: relative;
|
||||||
|
transition:
|
||||||
|
max-height 0.3s ease,
|
||||||
|
overflow-y 0.3s ease;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.thinking-content-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content-top,
|
||||||
|
.thinking-content-bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 30px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content-top {
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--white) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
var(--white) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
67
app/components/thinking-content.tsx
Normal file
67
app/components/thinking-content.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { ChatMessage } from "../store";
|
||||||
|
import Locale from "../locales";
|
||||||
|
|
||||||
|
import styles from "./thinking-content.module.scss";
|
||||||
|
import MaxIcon from "../icons/max.svg";
|
||||||
|
import MinIcon from "../icons/min.svg";
|
||||||
|
|
||||||
|
export function ThinkingContent({ message }: { message: ChatMessage }) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const thinkingContentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const thinkingContent = message.reasoningContent;
|
||||||
|
const isThinking =
|
||||||
|
message.streaming && thinkingContent && thinkingContent.length > 0;
|
||||||
|
|
||||||
|
// Auto-scroll to bottom of thinking container
|
||||||
|
useEffect(() => {
|
||||||
|
if (isThinking && thinkingContentRef.current) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (thinkingContentRef.current) {
|
||||||
|
thinkingContentRef.current.scrollTop =
|
||||||
|
thinkingContentRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [thinkingContent, isThinking, expanded]);
|
||||||
|
|
||||||
|
if (!thinkingContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles["thinking-container"],
|
||||||
|
expanded && styles["expanded"],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles["thinking-header"]}>
|
||||||
|
<div className={styles["thinking-title"]}>
|
||||||
|
{Locale.Chat.Thinking.Title}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles["thinking-toggle"]}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded ? <MinIcon /> : <MaxIcon />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["thinking-content-wrapper"]}>
|
||||||
|
{!expanded && <div className={styles["thinking-content-top"]}></div>}
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles["thinking-content"],
|
||||||
|
expanded && styles["expanded"],
|
||||||
|
)}
|
||||||
|
ref={thinkingContentRef}
|
||||||
|
>
|
||||||
|
<div className={styles["thinking-content-text"]}>
|
||||||
|
{thinkingContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!expanded && <div className={styles["thinking-content-bottom"]}></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -23,6 +23,9 @@ const cn = {
|
|||||||
},
|
},
|
||||||
Chat: {
|
Chat: {
|
||||||
SubTitle: (count: number) => `共 ${count} 条对话`,
|
SubTitle: (count: number) => `共 ${count} 条对话`,
|
||||||
|
Thinking: {
|
||||||
|
Title: "深度思考",
|
||||||
|
},
|
||||||
EditMessage: {
|
EditMessage: {
|
||||||
Title: "编辑消息记录",
|
Title: "编辑消息记录",
|
||||||
Topic: {
|
Topic: {
|
||||||
|
@ -25,6 +25,9 @@ const en: LocaleType = {
|
|||||||
},
|
},
|
||||||
Chat: {
|
Chat: {
|
||||||
SubTitle: (count: number) => `${count} messages`,
|
SubTitle: (count: number) => `${count} messages`,
|
||||||
|
Thinking: {
|
||||||
|
Title: "Thinking",
|
||||||
|
},
|
||||||
EditMessage: {
|
EditMessage: {
|
||||||
Title: "Edit All Messages",
|
Title: "Edit All Messages",
|
||||||
Topic: {
|
Topic: {
|
||||||
|
@ -562,6 +562,15 @@ export const useChatStore = createPersistStore(
|
|||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onReasoningUpdate(message) {
|
||||||
|
botMessage.streaming = true;
|
||||||
|
if (message) {
|
||||||
|
botMessage.reasoningContent = message;
|
||||||
|
}
|
||||||
|
get().updateTargetSession(session, (session) => {
|
||||||
|
session.messages = session.messages.concat();
|
||||||
|
});
|
||||||
|
},
|
||||||
onFinish(message) {
|
onFinish(message) {
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
if (message) {
|
if (message) {
|
||||||
|
@ -388,6 +388,8 @@ export function streamWithThink(
|
|||||||
) {
|
) {
|
||||||
let responseText = "";
|
let responseText = "";
|
||||||
let remainText = "";
|
let remainText = "";
|
||||||
|
let reasoningResponseText = "";
|
||||||
|
let reasoningRemainText = "";
|
||||||
let finished = false;
|
let finished = false;
|
||||||
let running = false;
|
let running = false;
|
||||||
let runTools: any[] = [];
|
let runTools: any[] = [];
|
||||||
@ -414,6 +416,19 @@ export function streamWithThink(
|
|||||||
options.onUpdate?.(responseText, fetchText);
|
options.onUpdate?.(responseText, fetchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reasoningRemainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(
|
||||||
|
1,
|
||||||
|
Math.round(reasoningRemainText.length / 60),
|
||||||
|
);
|
||||||
|
const fetchText = reasoningRemainText.slice(0, fetchCount);
|
||||||
|
reasoningResponseText += fetchText;
|
||||||
|
// 删除空行
|
||||||
|
reasoningResponseText = reasoningResponseText.replace(/^\s*\n/gm, "");
|
||||||
|
reasoningRemainText = reasoningRemainText.slice(fetchCount);
|
||||||
|
options.onReasoningUpdate?.(reasoningResponseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(animateResponseText);
|
requestAnimationFrame(animateResponseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,28 +597,24 @@ export function streamWithThink(
|
|||||||
if (!isInThinkingMode || isThinkingChanged) {
|
if (!isInThinkingMode || isThinkingChanged) {
|
||||||
// If this is a new thinking block or mode changed, add prefix
|
// If this is a new thinking block or mode changed, add prefix
|
||||||
isInThinkingMode = true;
|
isInThinkingMode = true;
|
||||||
if (remainText.length > 0) {
|
// if (remainText.length > 0) {
|
||||||
remainText += "\n";
|
// remainText += "\n";
|
||||||
}
|
// }
|
||||||
remainText += "> " + chunk.content;
|
// Add thinking prefix with timestamp
|
||||||
|
// const timestamp = new Date().toISOString().substr(11, 8); // HH:MM:SS format
|
||||||
|
// remainText += `> [${timestamp}] ` + chunk.content;
|
||||||
|
reasoningRemainText += chunk.content;
|
||||||
} else {
|
} else {
|
||||||
// Handle newlines in thinking content
|
// Handle newlines in thinking content
|
||||||
if (chunk.content.includes("\n\n")) {
|
reasoningRemainText += chunk.content;
|
||||||
const lines = chunk.content.split("\n\n");
|
|
||||||
remainText += lines.join("\n\n> ");
|
|
||||||
} else {
|
|
||||||
remainText += chunk.content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If in normal mode
|
// If in normal mode
|
||||||
if (isInThinkingMode || isThinkingChanged) {
|
if (isInThinkingMode || isThinkingChanged) {
|
||||||
// If switching from thinking mode to normal mode
|
// If switching from thinking mode to normal mode
|
||||||
isInThinkingMode = false;
|
isInThinkingMode = false;
|
||||||
remainText += "\n\n" + chunk.content;
|
|
||||||
} else {
|
|
||||||
remainText += chunk.content;
|
|
||||||
}
|
}
|
||||||
|
remainText += chunk.content;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Request] parse error", text, msg, e);
|
console.error("[Request] parse error", text, msg, e);
|
||||||
|
Loading…
Reference in New Issue
Block a user