diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index 3dd3d95f..0972e0d2 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -144,9 +144,11 @@ height: auto; } - a { + a, + .link { color: var(--color-link); text-decoration: none; + cursor: pointer; &:hover { text-decoration: underline; diff --git a/src/renderer/src/pages/home/components/Message.tsx b/src/renderer/src/pages/home/components/Message.tsx index 44fcb4e6..f2f9bcff 100644 --- a/src/renderer/src/pages/home/components/Message.tsx +++ b/src/renderer/src/pages/home/components/Message.tsx @@ -59,10 +59,9 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = const onRegenerate = useCallback( (model: Model) => { setModel(model) - onDeleteMessage?.(message) - setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100) }, - [message, onDeleteMessage, setModel] + [setModel] ) const getUserName = useCallback(() => { @@ -137,6 +136,15 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = {copied && } + {canRegenerate && ( + + + + + + + + )} = ({ message, index, showMenu, onDeleteMessage }) = - {canRegenerate && ( - - - - - - - - )} {!isUserMessage && ( diff --git a/src/renderer/src/pages/home/components/Messages.tsx b/src/renderer/src/pages/home/components/Messages.tsx index 35903588..c9c2e48d 100644 --- a/src/renderer/src/pages/home/components/Messages.tsx +++ b/src/renderer/src/pages/home/components/Messages.tsx @@ -2,12 +2,13 @@ import { useAssistant } from '@renderer/hooks/useAssistant' import { useProviderByAssistant } from '@renderer/hooks/useProvider' import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' +import { filterAtMessages } from '@renderer/services/message' import LocalStorage from '@renderer/services/storage' -import { Assistant, Message, Topic } from '@renderer/types' -import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils' +import { Assistant, Message, Model, Topic } from '@renderer/types' +import { estimateHistoryTokenCount, getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils' import { t } from 'i18next' 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 styled from 'styled-components' @@ -75,8 +76,18 @@ const Messages: FC = ({ assistant, topic }) => { onSendMessage(msg) setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100) }), - EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async () => { - fetchChatCompletion({ assistant, messages: messages, topic, onResponse: setLastMessage }) + EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => { + 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.CLEAR_MESSAGES, () => { diff --git a/src/renderer/src/pages/home/components/markdown/Link.tsx b/src/renderer/src/pages/home/components/markdown/Link.tsx index 1ceeb471..25ebc170 100644 --- a/src/renderer/src/pages/home/components/markdown/Link.tsx +++ b/src/renderer/src/pages/home/components/markdown/Link.tsx @@ -1,7 +1,11 @@ import { omit } from 'lodash' import React from 'react' -const Link: React.FC = (props) => { +const Link: React.FC = (props: React.AnchorHTMLAttributes) => { + if (props.href?.startsWith('#')) { + return {props.children} + } + return e.stopPropagation()} /> } diff --git a/src/renderer/src/services/api.ts b/src/renderer/src/services/api.ts index 6f080058..4d6b78f7 100644 --- a/src/renderer/src/services/api.ts +++ b/src/renderer/src/services/api.ts @@ -14,6 +14,7 @@ import { getTranslateModel } from './assistant' import { EVENT_NAMES, EventEmitter } from './event' +import { filterAtMessages } from './message' import ProviderSDK from './ProviderSDK' export async function fetchChatCompletion({ @@ -50,7 +51,7 @@ export async function fetchChatCompletion({ onResponse({ ...message }) try { - await providerSdk.completions(messages, assistant, ({ text, usage }) => { + await providerSdk.completions(filterAtMessages(messages), assistant, ({ text, usage }) => { message.content = message.content + text || '' message.usage = usage onResponse({ ...message, status: 'pending' }) diff --git a/src/renderer/src/services/message.ts b/src/renderer/src/services/message.ts new file mode 100644 index 00000000..237b7f23 --- /dev/null +++ b/src/renderer/src/services/message.ts @@ -0,0 +1,5 @@ +import { Message } from '@renderer/types' + +export const filterAtMessages = (messages: Message[]) => { + return messages.filter((message) => message.type !== '@') +} diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 51796924..fbfe4852 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -25,6 +25,7 @@ export type Message = { createdAt: string status: 'sending' | 'pending' | 'success' | 'paused' | 'error' usage?: OpenAI.Completions.CompletionUsage + type?: 'text' | '@' } export type Topic = { diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index d495c59b..c5078c41 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -230,3 +230,24 @@ export function convertMathFormula(input) { // 使用正则表达式匹配并替换公式格式 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 + '...' +}