feat: thinking style optimize

This commit is contained in:
Hk-Gosuto 2025-03-03 15:33:19 +08:00
parent ac9bdf642e
commit 78dd2d4258
8 changed files with 216 additions and 13 deletions

View File

@ -45,6 +45,7 @@ export interface MultimodalContent {
export interface RequestMessage {
role: MessageRole;
content: string | MultimodalContent[];
reasoningContent?: string;
fileInfos?: FileInfo[];
webSearchReferences?: TavilySearchResponse;
}
@ -93,6 +94,7 @@ export interface ChatOptions {
onToolUpdate?: (toolName: string, toolInput: string) => void;
onUpdate?: (message: string, chunk: string) => void;
onReasoningUpdate?: (message: string, chunk: string) => void;
onFinish: (message: string, responseRes: Response) => void;
onError?: (err: Error) => void;
onController?: (controller: AbortController) => void;

View File

@ -147,6 +147,7 @@ import {
WebTranscriptionApi,
} from "../utils/speech";
import { FileInfo } from "../client/platforms/utils";
import { ThinkingContent } from "./thinking-content";
const ttsPlayer = createTTSPlayer();
@ -2151,6 +2152,7 @@ function _Chat() {
))}
</div>
)}
{!isUser && <ThinkingContent message={message} />}
<div className={styles["chat-message-item"]}>
<Markdown
key={message.streaming ? "loading" : "done"}

View 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%
);
}
}

View 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>
);
}

View File

@ -23,6 +23,9 @@ const cn = {
},
Chat: {
SubTitle: (count: number) => `${count} 条对话`,
Thinking: {
Title: "深度思考",
},
EditMessage: {
Title: "编辑消息记录",
Topic: {

View File

@ -25,6 +25,9 @@ const en: LocaleType = {
},
Chat: {
SubTitle: (count: number) => `${count} messages`,
Thinking: {
Title: "Thinking",
},
EditMessage: {
Title: "Edit All Messages",
Topic: {

View File

@ -562,6 +562,15 @@ export const useChatStore = createPersistStore(
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) {
botMessage.streaming = false;
if (message) {

View File

@ -388,6 +388,8 @@ export function streamWithThink(
) {
let responseText = "";
let remainText = "";
let reasoningResponseText = "";
let reasoningRemainText = "";
let finished = false;
let running = false;
let runTools: any[] = [];
@ -414,6 +416,19 @@ export function streamWithThink(
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);
}
@ -582,28 +597,24 @@ export function streamWithThink(
if (!isInThinkingMode || isThinkingChanged) {
// If this is a new thinking block or mode changed, add prefix
isInThinkingMode = true;
if (remainText.length > 0) {
remainText += "\n";
}
remainText += "> " + chunk.content;
// if (remainText.length > 0) {
// remainText += "\n";
// }
// 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 {
// Handle newlines in thinking content
if (chunk.content.includes("\n\n")) {
const lines = chunk.content.split("\n\n");
remainText += lines.join("\n\n> ");
} else {
remainText += chunk.content;
}
reasoningRemainText += chunk.content;
}
} else {
// If in normal mode
if (isInThinkingMode || isThinkingChanged) {
// If switching from thinking mode to normal mode
isInThinkingMode = false;
remainText += "\n\n" + chunk.content;
} else {
remainText += chunk.content;
}
remainText += chunk.content;
}
} catch (e) {
console.error("[Request] parse error", text, msg, e);