mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-22 08:40:08 +08:00
feat: add pause icon to pause chat completion
This commit is contained in:
parent
a97d6f024b
commit
e8bdf9d5fd
@ -39,7 +39,8 @@ const resources = {
|
|||||||
'error.enter.api.host': 'Please enter your API host first',
|
'error.enter.api.host': 'Please enter your API host first',
|
||||||
'error.enter.model': 'Please select a model first',
|
'error.enter.model': 'Please select a model first',
|
||||||
'api.connection.failed': 'Connection failed',
|
'api.connection.failed': 'Connection failed',
|
||||||
'api.connection.success': 'Connection successful'
|
'api.connection.success': 'Connection successful',
|
||||||
|
'chat.completion.paused': 'Chat completion paused'
|
||||||
},
|
},
|
||||||
assistant: {
|
assistant: {
|
||||||
'default.name': 'Default Assistant',
|
'default.name': 'Default Assistant',
|
||||||
@ -61,7 +62,8 @@ const resources = {
|
|||||||
'input.clear.title': 'Clear all messages?',
|
'input.clear.title': 'Clear all messages?',
|
||||||
'input.clear.content': 'Are you sure to clear all messages?',
|
'input.clear.content': 'Are you sure to clear all messages?',
|
||||||
'input.placeholder': 'Type your message here...',
|
'input.placeholder': 'Type your message here...',
|
||||||
'input.send': 'Send'
|
'input.send': 'Send',
|
||||||
|
'input.pause': 'Pause'
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Agents'
|
title: 'Agents'
|
||||||
@ -146,7 +148,8 @@ const resources = {
|
|||||||
'error.enter.api.host': '请输入您的 API 地址',
|
'error.enter.api.host': '请输入您的 API 地址',
|
||||||
'error.enter.model': '请选择一个模型',
|
'error.enter.model': '请选择一个模型',
|
||||||
'api.connection.failed': '连接失败',
|
'api.connection.failed': '连接失败',
|
||||||
'api.connection.successful': '连接成功'
|
'api.connection.successful': '连接成功',
|
||||||
|
'chat.completion.paused': '会话已停止'
|
||||||
},
|
},
|
||||||
assistant: {
|
assistant: {
|
||||||
'default.name': '默认助手',
|
'default.name': '默认助手',
|
||||||
@ -168,7 +171,8 @@ const resources = {
|
|||||||
'input.clear.title': '清除所有消息?',
|
'input.clear.title': '清除所有消息?',
|
||||||
'input.clear.content': '确定要清除所有消息吗?',
|
'input.clear.content': '确定要清除所有消息吗?',
|
||||||
'input.placeholder': '在这里输入消息...',
|
'input.placeholder': '在这里输入消息...',
|
||||||
'input.send': '发送'
|
'input.send': '发送',
|
||||||
|
'input.pause': '暂停'
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: '智能体'
|
title: '智能体'
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
PlusCircleOutlined
|
PlusCircleOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
@ -19,9 +20,10 @@ import { isEmpty } from 'lodash'
|
|||||||
import SendMessageSetting from './SendMessageSetting'
|
import SendMessageSetting from './SendMessageSetting'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import store, { useAppSelector } from '@renderer/store'
|
||||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@ -86,6 +88,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
|
|
||||||
const clearTopic = () => EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
const clearTopic = () => EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
||||||
|
|
||||||
|
const onPause = () => {
|
||||||
|
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||||
|
store.dispatch(setGenerating(false))
|
||||||
|
}
|
||||||
|
|
||||||
// Command or Ctrl + N create new topic
|
// Command or Ctrl + N create new topic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeydown = (e) => {
|
const onKeydown = (e) => {
|
||||||
@ -148,6 +155,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToolbarMenu>
|
</ToolbarMenu>
|
||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
|
{generating && (
|
||||||
|
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={onPause}>
|
||||||
|
<PauseCircleOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<SendMessageSetting>
|
<SendMessageSetting>
|
||||||
<ToolbarButton type="text" style={{ marginRight: 0 }}>
|
<ToolbarButton type="text" style={{ marginRight: 0 }}>
|
||||||
<MoreOutlined />
|
<MoreOutlined />
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import Logo from '@renderer/assets/images/logo.png'
|
|||||||
import { SyncOutlined } from '@ant-design/icons'
|
import { SyncOutlined } from '@ant-design/icons'
|
||||||
import { firstLetter } from '@renderer/utils'
|
import { firstLetter } from '@renderer/utils'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -53,6 +54,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMessageContent = (message: Message) => {
|
||||||
|
if (isEmpty(message.content) && message.status === 'paused') {
|
||||||
|
return t('message.chat.completion.paused')
|
||||||
|
}
|
||||||
|
return message.content
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContainer key={message.id}>
|
<MessageContainer key={message.id}>
|
||||||
<AvatarWrapper>
|
<AvatarWrapper>
|
||||||
@ -72,7 +80,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
)}
|
)}
|
||||||
{message.status !== 'sending' && (
|
{message.status !== 'sending' && (
|
||||||
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
|
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
|
||||||
{message.content}
|
{getMessageContent(message)}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
)}
|
)}
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
|
|||||||
@ -27,6 +27,8 @@ const getOpenAiProvider = (provider: Provider) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchChatCompletion({ messages, topic, assistant, onResponse }: FetchChatCompletionParams) {
|
export async function fetchChatCompletion({ messages, topic, assistant, onResponse }: FetchChatCompletionParams) {
|
||||||
|
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, false)
|
||||||
|
|
||||||
const provider = getAssistantProvider(assistant)
|
const provider = getAssistantProvider(assistant)
|
||||||
const openaiProvider = getOpenAiProvider(provider)
|
const openaiProvider = getOpenAiProvider(provider)
|
||||||
const defaultModel = getDefaultModel()
|
const defaultModel = getDefaultModel()
|
||||||
@ -34,7 +36,7 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
|||||||
|
|
||||||
store.dispatch(setGenerating(true))
|
store.dispatch(setGenerating(true))
|
||||||
|
|
||||||
const _message: Message = {
|
const message: Message = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: '',
|
||||||
@ -45,7 +47,7 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
|||||||
status: 'sending'
|
status: 'sending'
|
||||||
}
|
}
|
||||||
|
|
||||||
onResponse({ ..._message })
|
onResponse({ ...message })
|
||||||
|
|
||||||
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
||||||
|
|
||||||
@ -54,12 +56,10 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
|||||||
content: message.content
|
content: message.content
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const _messages = [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[]
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await openaiProvider.chat.completions.create({
|
const stream = await openaiProvider.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: _messages,
|
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||||
stream: true
|
stream: true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -67,22 +67,27 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
|||||||
let usage: OpenAI.Completions.CompletionUsage | undefined = undefined
|
let usage: OpenAI.Completions.CompletionUsage | undefined = undefined
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
|
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
content = content + (chunk.choices[0]?.delta?.content || '')
|
content = content + (chunk.choices[0]?.delta?.content || '')
|
||||||
chunk.usage && (usage = chunk.usage)
|
chunk.usage && (usage = chunk.usage)
|
||||||
onResponse({ ..._message, content, status: 'pending' })
|
onResponse({ ...message, content, status: 'pending' })
|
||||||
}
|
}
|
||||||
|
|
||||||
_message.content = content
|
message.content = content
|
||||||
_message.usage = usage
|
message.usage = usage
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
_message.content = `Error: ${error.message}`
|
message.content = `Error: ${error.message}`
|
||||||
}
|
}
|
||||||
|
|
||||||
_message.status = 'success'
|
const paused = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)
|
||||||
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, _message)
|
message.status = paused ? 'paused' : 'success'
|
||||||
|
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, message)
|
||||||
store.dispatch(setGenerating(false))
|
store.dispatch(setGenerating(false))
|
||||||
|
|
||||||
return _message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FetchMessagesSummaryParams {
|
interface FetchMessagesSummaryParams {
|
||||||
|
|||||||
@ -9,5 +9,6 @@ export const EVENT_NAMES = {
|
|||||||
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
||||||
ADD_ASSISTANT: 'ADD_ASSISTANT',
|
ADD_ASSISTANT: 'ADD_ASSISTANT',
|
||||||
EDIT_MESSAGE: 'EDIT_MESSAGE',
|
EDIT_MESSAGE: 'EDIT_MESSAGE',
|
||||||
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE'
|
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE',
|
||||||
|
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export type Message = {
|
|||||||
topicId: string
|
topicId: string
|
||||||
modelId?: string
|
modelId?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
status: 'sending' | 'pending' | 'success' | 'error'
|
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
||||||
usage?: OpenAI.Completions.CompletionUsage
|
usage?: OpenAI.Completions.CompletionUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user