mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 20:00:00 +08:00
feat: @model regenerate message
This commit is contained in:
parent
de41199f7e
commit
b0a3d705ff
@ -144,9 +144,11 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a,
|
||||||
|
.link {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@ -59,10 +59,9 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
const onRegenerate = useCallback(
|
const onRegenerate = useCallback(
|
||||||
(model: Model) => {
|
(model: Model) => {
|
||||||
setModel(model)
|
setModel(model)
|
||||||
onDeleteMessage?.(message)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
|
|
||||||
},
|
},
|
||||||
[message, onDeleteMessage, setModel]
|
[setModel]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getUserName = useCallback(() => {
|
const getUserName = useCallback(() => {
|
||||||
@ -137,6 +136,15 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{canRegenerate && (
|
||||||
|
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
|
||||||
|
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||||
|
<ActionButton>
|
||||||
|
<SyncOutlined />
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
</SelectModelDropdown>
|
||||||
|
)}
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('message.message.delete.content')}
|
title={t('message.message.delete.content')}
|
||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
@ -148,15 +156,6 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
{canRegenerate && (
|
|
||||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
|
|
||||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
|
||||||
<ActionButton>
|
|
||||||
<SyncOutlined />
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
</SelectModelDropdown>
|
|
||||||
)}
|
|
||||||
{!isUserMessage && (
|
{!isUserMessage && (
|
||||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||||
<ActionButton>
|
<ActionButton>
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
|
|||||||
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
||||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
|
import { filterAtMessages } from '@renderer/services/message'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
import LocalStorage from '@renderer/services/storage'
|
||||||
import { Assistant, Message, Topic } from '@renderer/types'
|
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||||
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
|
import { estimateHistoryTokenCount, getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { debounce, reverse } from 'lodash'
|
import { debounce, last, reverse } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -75,8 +76,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
onSendMessage(msg)
|
onSendMessage(msg)
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async () => {
|
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
|
||||||
fetchChatCompletion({ assistant, messages: messages, topic, onResponse: setLastMessage })
|
const lastUserMessage = last(filterAtMessages(messages).filter((m) => m.role === 'user'))
|
||||||
|
if (lastUserMessage) {
|
||||||
|
const content = `[@${model.name}](#) ${getBriefInfo(lastUserMessage.content)}`
|
||||||
|
onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', content })
|
||||||
|
fetchChatCompletion({
|
||||||
|
assistant,
|
||||||
|
topic,
|
||||||
|
messages: [...messages, lastUserMessage],
|
||||||
|
onResponse: setLastMessage
|
||||||
|
})
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
|
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
|
||||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const Link: React.FC = (props) => {
|
const Link: React.FC = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
|
if (props.href?.startsWith('#')) {
|
||||||
|
return <span className="link">{props.children}</span>
|
||||||
|
}
|
||||||
|
|
||||||
return <a {...omit(props, 'node')} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} />
|
return <a {...omit(props, 'node')} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
getTranslateModel
|
getTranslateModel
|
||||||
} from './assistant'
|
} from './assistant'
|
||||||
import { EVENT_NAMES, EventEmitter } from './event'
|
import { EVENT_NAMES, EventEmitter } from './event'
|
||||||
|
import { filterAtMessages } from './message'
|
||||||
import ProviderSDK from './ProviderSDK'
|
import ProviderSDK from './ProviderSDK'
|
||||||
|
|
||||||
export async function fetchChatCompletion({
|
export async function fetchChatCompletion({
|
||||||
@ -50,7 +51,7 @@ export async function fetchChatCompletion({
|
|||||||
onResponse({ ...message })
|
onResponse({ ...message })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await providerSdk.completions(messages, assistant, ({ text, usage }) => {
|
await providerSdk.completions(filterAtMessages(messages), assistant, ({ text, usage }) => {
|
||||||
message.content = message.content + text || ''
|
message.content = message.content + text || ''
|
||||||
message.usage = usage
|
message.usage = usage
|
||||||
onResponse({ ...message, status: 'pending' })
|
onResponse({ ...message, status: 'pending' })
|
||||||
|
|||||||
5
src/renderer/src/services/message.ts
Normal file
5
src/renderer/src/services/message.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Message } from '@renderer/types'
|
||||||
|
|
||||||
|
export const filterAtMessages = (messages: Message[]) => {
|
||||||
|
return messages.filter((message) => message.type !== '@')
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@ export type Message = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
||||||
usage?: OpenAI.Completions.CompletionUsage
|
usage?: OpenAI.Completions.CompletionUsage
|
||||||
|
type?: 'text' | '@'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Topic = {
|
export type Topic = {
|
||||||
|
|||||||
@ -230,3 +230,24 @@ export function convertMathFormula(input) {
|
|||||||
// 使用正则表达式匹配并替换公式格式
|
// 使用正则表达式匹配并替换公式格式
|
||||||
return input.replaceAll(/\\\[/g, '$$$$').replaceAll(/\\\]/g, '$$$$')
|
return input.replaceAll(/\\\[/g, '$$$$').replaceAll(/\\\]/g, '$$$$')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBriefInfo(text: string, maxLength: number = 50): string {
|
||||||
|
// 去除空行
|
||||||
|
const noEmptyLinesText = text.replace(/\n\s*\n/g, '\n')
|
||||||
|
|
||||||
|
// 检查文本是否超过最大长度
|
||||||
|
if (noEmptyLinesText.length <= maxLength) {
|
||||||
|
return noEmptyLinesText
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到最近的单词边界
|
||||||
|
let truncatedText = noEmptyLinesText.slice(0, maxLength)
|
||||||
|
const lastSpaceIndex = truncatedText.lastIndexOf(' ')
|
||||||
|
|
||||||
|
if (lastSpaceIndex !== -1) {
|
||||||
|
truncatedText = truncatedText.slice(0, lastSpaceIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截取前面的内容,并在末尾添加 "..."
|
||||||
|
return truncatedText + '...'
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user