feat: thinking style optimize
This commit is contained in:
parent
ac9bdf642e
commit
78dd2d4258
|
@ -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;
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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%
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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: {
|
||||
SubTitle: (count: number) => `共 ${count} 条对话`,
|
||||
Thinking: {
|
||||
Title: "深度思考",
|
||||
},
|
||||
EditMessage: {
|
||||
Title: "编辑消息记录",
|
||||
Topic: {
|
||||
|
|
|
@ -25,6 +25,9 @@ const en: LocaleType = {
|
|||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} messages`,
|
||||
Thinking: {
|
||||
Title: "Thinking",
|
||||
},
|
||||
EditMessage: {
|
||||
Title: "Edit All Messages",
|
||||
Topic: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue