mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
fix: determining thinking process using block status (#5509)
* fix: determining thinking process using block status * refactor: merge MessageThought to ThinkingBlock * style: fix typos * fix: error handling * refactor: set collapsed status as default * refactor: remove pending status * refactor: better collapsing behaviour * refactor: remove processing status
This commit is contained in:
parent
b0d6f209d7
commit
6ee8a72823
@ -100,7 +100,7 @@
|
|||||||
"artifacts.button.preview": "Preview",
|
"artifacts.button.preview": "Preview",
|
||||||
"artifacts.preview.openExternal.error.content": "Error opening the external browser.",
|
"artifacts.preview.openExternal.error.content": "Error opening the external browser.",
|
||||||
"assistant.search.placeholder": "Search",
|
"assistant.search.placeholder": "Search",
|
||||||
"deeply_thought": "Deeply thought ({{secounds}} seconds)",
|
"deeply_thought": "Deeply thought ({{seconds}} seconds)",
|
||||||
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||||
"default.name": "Default Assistant",
|
"default.name": "Default Assistant",
|
||||||
"default.topic.name": "Default Topic",
|
"default.topic.name": "Default Topic",
|
||||||
@ -185,7 +185,7 @@
|
|||||||
"settings.top_p": "Top-P",
|
"settings.top_p": "Top-P",
|
||||||
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
|
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
|
||||||
"suggestions.title": "Suggested Questions",
|
"suggestions.title": "Suggested Questions",
|
||||||
"thinking": "Thinking",
|
"thinking": "Thinking ({{seconds}} seconds)",
|
||||||
"topics.auto_rename": "Auto Rename",
|
"topics.auto_rename": "Auto Rename",
|
||||||
"topics.clear.title": "Clear Messages",
|
"topics.clear.title": "Clear Messages",
|
||||||
"topics.copy.image": "Copy as image",
|
"topics.copy.image": "Copy as image",
|
||||||
|
|||||||
@ -100,7 +100,7 @@
|
|||||||
"artifacts.button.preview": "プレビュー",
|
"artifacts.button.preview": "プレビュー",
|
||||||
"artifacts.preview.openExternal.error.content": "外部ブラウザの起動に失敗しました。",
|
"artifacts.preview.openExternal.error.content": "外部ブラウザの起動に失敗しました。",
|
||||||
"assistant.search.placeholder": "検索",
|
"assistant.search.placeholder": "検索",
|
||||||
"deeply_thought": "深く考えています({{secounds}} 秒)",
|
"deeply_thought": "深く考えています({{seconds}} 秒)",
|
||||||
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
|
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
|
||||||
"default.name": "デフォルトアシスタント",
|
"default.name": "デフォルトアシスタント",
|
||||||
"default.topic.name": "デフォルトトピック",
|
"default.topic.name": "デフォルトトピック",
|
||||||
@ -185,7 +185,7 @@
|
|||||||
"settings.top_p": "Top-P",
|
"settings.top_p": "Top-P",
|
||||||
"settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します",
|
"settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します",
|
||||||
"suggestions.title": "提案された質問",
|
"suggestions.title": "提案された質問",
|
||||||
"thinking": "思考中...",
|
"thinking": "思考中(用時 {{seconds}} 秒)",
|
||||||
"topics.auto_rename": "自動リネーム",
|
"topics.auto_rename": "自動リネーム",
|
||||||
"topics.clear.title": "メッセージをクリア",
|
"topics.clear.title": "メッセージをクリア",
|
||||||
"topics.copy.image": "画像としてコピー",
|
"topics.copy.image": "画像としてコピー",
|
||||||
|
|||||||
@ -100,7 +100,7 @@
|
|||||||
"artifacts.button.preview": "Предпросмотр",
|
"artifacts.button.preview": "Предпросмотр",
|
||||||
"artifacts.preview.openExternal.error.content": "Внешний браузер открылся с ошибкой",
|
"artifacts.preview.openExternal.error.content": "Внешний браузер открылся с ошибкой",
|
||||||
"assistant.search.placeholder": "Поиск",
|
"assistant.search.placeholder": "Поиск",
|
||||||
"deeply_thought": "Мыслим ({{secounds}} секунд)",
|
"deeply_thought": "Мыслим ({{seconds}} секунд)",
|
||||||
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
|
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
|
||||||
"default.name": "Ассистент по умолчанию",
|
"default.name": "Ассистент по умолчанию",
|
||||||
"default.topic.name": "Топик по умолчанию",
|
"default.topic.name": "Топик по умолчанию",
|
||||||
@ -185,7 +185,7 @@
|
|||||||
"settings.top_p": "Top-P",
|
"settings.top_p": "Top-P",
|
||||||
"settings.top_p.tip": "Значение по умолчанию 1, чем меньше значение, тем меньше вариативности в ответах, тем проще понять, чем больше значение, тем больше вариативности в ответах, тем больше разнообразие",
|
"settings.top_p.tip": "Значение по умолчанию 1, чем меньше значение, тем меньше вариативности в ответах, тем проще понять, чем больше значение, тем больше вариативности в ответах, тем больше разнообразие",
|
||||||
"suggestions.title": "Предложенные вопросы",
|
"suggestions.title": "Предложенные вопросы",
|
||||||
"thinking": "Мыслим",
|
"thinking": "Мыслим ({{seconds}} секунд)",
|
||||||
"topics.auto_rename": "Автопереименование",
|
"topics.auto_rename": "Автопереименование",
|
||||||
"topics.clear.title": "Очистить сообщения",
|
"topics.clear.title": "Очистить сообщения",
|
||||||
"topics.copy.image": "Скопировать как изображение",
|
"topics.copy.image": "Скопировать как изображение",
|
||||||
|
|||||||
@ -100,7 +100,7 @@
|
|||||||
"artifacts.button.preview": "预览",
|
"artifacts.button.preview": "预览",
|
||||||
"artifacts.preview.openExternal.error.content": "外部浏览器打开出错",
|
"artifacts.preview.openExternal.error.content": "外部浏览器打开出错",
|
||||||
"assistant.search.placeholder": "搜索",
|
"assistant.search.placeholder": "搜索",
|
||||||
"deeply_thought": "已深度思考(用时 {{secounds}} 秒)",
|
"deeply_thought": "已深度思考(用时 {{seconds}} 秒)",
|
||||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||||
"default.name": "默认助手",
|
"default.name": "默认助手",
|
||||||
"default.topic.name": "默认话题",
|
"default.topic.name": "默认话题",
|
||||||
@ -190,7 +190,7 @@
|
|||||||
"settings.top_p": "Top-P",
|
"settings.top_p": "Top-P",
|
||||||
"settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化",
|
"settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化",
|
||||||
"suggestions.title": "建议的问题",
|
"suggestions.title": "建议的问题",
|
||||||
"thinking": "思考中",
|
"thinking": "思考中(用时 {{seconds}} 秒)",
|
||||||
"topics.auto_rename": "生成话题名",
|
"topics.auto_rename": "生成话题名",
|
||||||
"topics.clear.title": "清空消息",
|
"topics.clear.title": "清空消息",
|
||||||
"topics.copy.image": "复制为图片",
|
"topics.copy.image": "复制为图片",
|
||||||
|
|||||||
@ -100,7 +100,7 @@
|
|||||||
"artifacts.button.preview": "預覽",
|
"artifacts.button.preview": "預覽",
|
||||||
"artifacts.preview.openExternal.error.content": "外部瀏覽器開啟出錯",
|
"artifacts.preview.openExternal.error.content": "外部瀏覽器開啟出錯",
|
||||||
"assistant.search.placeholder": "搜尋",
|
"assistant.search.placeholder": "搜尋",
|
||||||
"deeply_thought": "已深度思考(用時 {{secounds}} 秒)",
|
"deeply_thought": "已深度思考(用時 {{seconds}} 秒)",
|
||||||
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
||||||
"default.name": "預設助手",
|
"default.name": "預設助手",
|
||||||
"default.topic.name": "預設話題",
|
"default.topic.name": "預設話題",
|
||||||
@ -185,7 +185,7 @@
|
|||||||
"settings.top_p": "Top-P",
|
"settings.top_p": "Top-P",
|
||||||
"settings.top_p.tip": "模型生成文字的隨機程度。值越小,AI 生成的內容越單調,也越容易理解;值越大,AI 回覆的詞彙範圍越大,越多樣化",
|
"settings.top_p.tip": "模型生成文字的隨機程度。值越小,AI 生成的內容越單調,也越容易理解;值越大,AI 回覆的詞彙範圍越大,越多樣化",
|
||||||
"suggestions.title": "建議的問題",
|
"suggestions.title": "建議的問題",
|
||||||
"thinking": "思考中",
|
"thinking": "思考中(用時 {{seconds}} 秒)",
|
||||||
"topics.auto_rename": "自動重新命名",
|
"topics.auto_rename": "自動重新命名",
|
||||||
"topics.clear.title": "清空訊息",
|
"topics.clear.title": "清空訊息",
|
||||||
"topics.copy.image": "複製為圖片",
|
"topics.copy.image": "複製為圖片",
|
||||||
|
|||||||
@ -1,14 +1,149 @@
|
|||||||
import type { ThinkingMessageBlock } from '@renderer/types/newMessage'
|
import { CheckOutlined } from '@ant-design/icons'
|
||||||
import React from 'react'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
|
||||||
|
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import BarLoader from 'react-spinners/BarLoader'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import MessageThought from '../MessageThought'
|
import Markdown from '../../Markdown/Markdown'
|
||||||
interface Props {
|
interface Props {
|
||||||
block: ThinkingMessageBlock
|
block: ThinkingMessageBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
||||||
// 创建思考过程的显示组件
|
const [copied, setCopied] = useState(false)
|
||||||
return <MessageThought message={block} />
|
const { t } = useTranslation()
|
||||||
|
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings()
|
||||||
|
const [activeKey, setActiveKey] = useState<'thought' | ''>(thoughtAutoCollapse ? '' : 'thought')
|
||||||
|
|
||||||
|
const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status])
|
||||||
|
|
||||||
|
const fontFamily = useMemo(() => {
|
||||||
|
return messageFont === 'serif'
|
||||||
|
? 'serif'
|
||||||
|
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans","Helvetica Neue", sans-serif'
|
||||||
|
}, [messageFont])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isThinking && thoughtAutoCollapse) {
|
||||||
|
setActiveKey('')
|
||||||
|
} else {
|
||||||
|
setActiveKey('thought')
|
||||||
|
}
|
||||||
|
}, [isThinking, thoughtAutoCollapse])
|
||||||
|
|
||||||
|
const copyThought = useCallback(() => {
|
||||||
|
if (block.content) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(block.content)
|
||||||
|
.then(() => {
|
||||||
|
antdMessage.success({ content: t('message.copied'), key: 'copy-message' })
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to copy text:', error)
|
||||||
|
antdMessage.error({ content: t('message.copy.failed'), key: 'copy-message-error' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [block.content, t])
|
||||||
|
|
||||||
|
if (!block.content) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const thinkingTime = block.thinking_millsec || 0
|
||||||
|
const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapseContainer
|
||||||
|
activeKey={activeKey}
|
||||||
|
size="small"
|
||||||
|
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
||||||
|
className="message-thought-container"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'thought',
|
||||||
|
label: (
|
||||||
|
<MessageTitleLabel>
|
||||||
|
<ThinkingText>
|
||||||
|
{t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', {
|
||||||
|
seconds: thinkingTimeSeconds
|
||||||
|
})}
|
||||||
|
</ThinkingText>
|
||||||
|
{isThinking && <BarLoader color="#9254de" />}
|
||||||
|
{!isThinking && (
|
||||||
|
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||||
|
<ActionButton
|
||||||
|
className="message-action-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
copyThought()
|
||||||
|
}}
|
||||||
|
aria-label={t('common.copy')}>
|
||||||
|
{!copied && <i className="iconfont icon-copy"></i>}
|
||||||
|
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</MessageTitleLabel>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
// FIXME: 临时兼容
|
||||||
|
<div style={{ fontFamily, fontSize }}>
|
||||||
|
<Markdown block={block} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(ThinkingBlock)
|
const CollapseContainer = styled(Collapse)`
|
||||||
|
margin-bottom: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageTitleLabel = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 22px;
|
||||||
|
gap: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ThinkingText = styled.span`
|
||||||
|
color: var(--color-text-2);
|
||||||
|
`
|
||||||
|
|
||||||
|
const ActionButton = styled.button`
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(ThinkingBlock)
|
||||||
|
|||||||
@ -1,136 +0,0 @@
|
|||||||
import { CheckOutlined } from '@ant-design/icons'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import { ThinkingMessageBlock } from '@renderer/types/newMessage'
|
|
||||||
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
|
||||||
import { FC, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import BarLoader from 'react-spinners/BarLoader'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import Markdown from '../Markdown/Markdown'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
message: ThinkingMessageBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageThought: FC<Props> = ({ message }) => {
|
|
||||||
const [activeKey, setActiveKey] = useState<'thought' | ''>('thought')
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const isThinking = !message.content
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings()
|
|
||||||
const fontFamily = useMemo(() => {
|
|
||||||
return messageFont === 'serif'
|
|
||||||
? 'serif'
|
|
||||||
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans","Helvetica Neue", sans-serif'
|
|
||||||
}, [messageFont])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isThinking && thoughtAutoCollapse) setActiveKey('')
|
|
||||||
}, [isThinking, thoughtAutoCollapse])
|
|
||||||
|
|
||||||
if (!message.content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyThought = () => {
|
|
||||||
if (message.content) {
|
|
||||||
navigator.clipboard.writeText(message.content)
|
|
||||||
antdMessage.success({ content: t('message.copied'), key: 'copy-message' })
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const thinkingTime = message.thinking_millsec || 0
|
|
||||||
const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1)
|
|
||||||
const isPaused = message.status === 'paused'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapseContainer
|
|
||||||
activeKey={activeKey}
|
|
||||||
size="small"
|
|
||||||
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
|
||||||
className="message-thought-container"
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'thought',
|
|
||||||
label: (
|
|
||||||
<MessageTitleLabel>
|
|
||||||
<TinkingText>
|
|
||||||
{isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSeconds })}
|
|
||||||
</TinkingText>
|
|
||||||
{isThinking && !isPaused && <BarLoader color="#9254de" />}
|
|
||||||
{(!isThinking || isPaused) && (
|
|
||||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
|
||||||
<ActionButton
|
|
||||||
className="message-action-button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
copyThought()
|
|
||||||
}}
|
|
||||||
aria-label={t('common.copy')}>
|
|
||||||
{!copied && <i className="iconfont icon-copy"></i>}
|
|
||||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</MessageTitleLabel>
|
|
||||||
),
|
|
||||||
children: (
|
|
||||||
// FIXME: 临时兼容
|
|
||||||
<div style={{ fontFamily, fontSize }}>
|
|
||||||
<Markdown block={{ ...message, content: message.content }} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CollapseContainer = styled(Collapse)`
|
|
||||||
margin-bottom: 15px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const MessageTitleLabel = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: 22px;
|
|
||||||
gap: 15px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TinkingText = styled.span`
|
|
||||||
color: var(--color-text-2);
|
|
||||||
`
|
|
||||||
|
|
||||||
const ActionButton = styled.button`
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: auto;
|
|
||||||
opacity: 0.6;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--color-primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default MessageThought
|
|
||||||
Loading…
Reference in New Issue
Block a user