feat(Topic): Implement auto-renaming topic with enhanced logic

- Add `autoRenameTopic` function in useTopic hook
- Support automatic topic naming with or without AI summary
- Integrate with store and event system for dynamic topic renaming
- Remove local implementation of auto-rename in Messages component
This commit is contained in:
kangfenmao 2025-03-11 20:44:06 +08:00
parent 74567d5e17
commit 151a08d0dd
3 changed files with 44 additions and 47 deletions

View File

@ -1,20 +1,25 @@
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { deleteMessageFiles } from '@renderer/services/MessagesService' import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store' import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { prepareTopicMessages } from '@renderer/store/messages' import { prepareTopicMessages } from '@renderer/store/messages'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash' import { find, isEmpty } from 'lodash'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant' import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
let _activeTopic: Topic let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
export function useActiveTopic(_assistant: Assistant, topic?: Topic) { export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
const { assistant } = useAssistant(_assistant.id) const { assistant } = useAssistant(_assistant.id)
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0]) const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
_activeTopic = activeTopic _activeTopic = activeTopic
_setActiveTopic = setActiveTopic
useEffect(() => { useEffect(() => {
if (activeTopic) { if (activeTopic) {
@ -48,6 +53,35 @@ export async function getTopicById(topicId: string) {
return { ...topic, messages } as Topic return { ...topic, messages } as Topic
} }
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
const topic = await getTopicById(topicId)
const enableTopicNaming = getStoreSetting('enableTopicNaming')
if (isEmpty(topic.messages)) {
return
}
if (!enableTopicNaming) {
const topicName = topic.messages[0]?.content.substring(0, 50)
if (topicName) {
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
return
}
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
}
}
// Convert class to object with functions since class only has static methods // Convert class to object with functions since class only has static methods
// 只有静态方法,没必要用class可以export {} // 只有静态方法,没必要用class可以export {}
export const TopicManager = { export const TopicManager = {

View File

@ -5,8 +5,7 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { getTopic } from '@renderer/hooks/useTopic' import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { getDefaultTopic } from '@renderer/services/AssistantService' import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService' import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService'
@ -19,7 +18,7 @@ import {
removeSpecialCharactersForFileName, removeSpecialCharactersForFileName,
runAsyncFunction runAsyncFunction
} from '@renderer/utils' } from '@renderer/utils'
import { flatten, isEmpty, last, take } from 'lodash' import { flatten, last, take } from 'lodash'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from 'react-infinite-scroll-component'
@ -39,7 +38,7 @@ interface MessagesProps {
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => { const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings() const { showTopics, topicPosition, showAssistants } = useSettings()
const { updateTopic, addTopic } = useAssistant(assistant.id) const { updateTopic, addTopic } = useAssistant(assistant.id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
@ -67,36 +66,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
setTimeout(() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }), 50) setTimeout(() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }), 50)
}, []) }, [])
const autoRenameTopic = useCallback(async () => {
const _topic = getTopic(assistant, topic.id)
if (isEmpty(messages)) {
return
}
const filteredMessages = messages.filter((m) => m.status === 'success')
if (!enableTopicNaming) {
const topicName = filteredMessages[0]?.content.substring(0, 50)
if (topicName) {
const data = { ..._topic, name: topicName } as Topic
setActiveTopic(data)
updateTopic(data)
}
return
}
if (_topic && _topic.name === t('chat.default.topic.name') && filteredMessages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages: filteredMessages, assistant })
if (summaryText) {
const data = { ..._topic, name: summaryText }
setActiveTopic(data)
updateTopic(data)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assistant, topic.id, enableTopicNaming, t, setActiveTopic, updateTopic, messages])
useEffect(() => { useEffect(() => {
const unsubscribes = [ const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, () => { EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, () => {
@ -157,7 +126,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
await db.topics.add({ id: newTopic.id, messages: branchMessages }) await db.topics.add({ id: newTopic.id, messages: branchMessages })
addTopic(newTopic) addTopic(newTopic)
setActiveTopic(newTopic) setActiveTopic(newTopic)
autoRenameTopic() autoRenameTopic(assistant, newTopic.id)
// 由于复制了消息,消息中附带的文件的总数变了,需要更新 // 由于复制了消息,消息中附带的文件的总数变了,需要更新
const filesArr = branchMessages.map((m) => m.files) const filesArr = branchMessages.map((m) => m.files)
@ -171,17 +140,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
] ]
return () => unsubscribes.forEach((unsub) => unsub()) return () => unsubscribes.forEach((unsub) => unsub())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assistant, dispatch, scrollToBottom, topic, updateTopic]) }, [assistant, dispatch, scrollToBottom, topic, updateTopic])
useEffect(() => {
const unsubscribes = [EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic)]
return () => {
for (const unsub of unsubscribes) {
unsub()
}
}
}, [autoRenameTopic])
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, { EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, {

View File

@ -1,6 +1,6 @@
import { createAsyncThunk, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit' import { createAsyncThunk, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'
import db from '@renderer/databases' import db from '@renderer/databases'
import { TopicManager } from '@renderer/hooks/useTopic' import { autoRenameTopic, TopicManager } from '@renderer/hooks/useTopic'
import { fetchChatCompletion } from '@renderer/services/ApiService' import { fetchChatCompletion } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService' import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService'
@ -205,6 +205,7 @@ export const {
} = messagesSlice.actions } = messagesSlice.actions
const handleResponseMessageUpdate = ( const handleResponseMessageUpdate = (
assistant: Assistant,
message: Message, message: Message,
topicId: string, topicId: string,
dispatch: AppDispatch, dispatch: AppDispatch,
@ -214,7 +215,7 @@ const handleResponseMessageUpdate = (
if (message.status !== 'pending') { if (message.status !== 'pending') {
// When message is complete, commit to messages and sync with DB // When message is complete, commit to messages and sync with DB
if (message.status === 'success') { if (message.status === 'success') {
EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME) autoRenameTopic(assistant, topicId)
} }
if (message.status !== 'sending') { if (message.status !== 'sending') {
dispatch(commitStreamMessage({ topicId, messageId: message.id })) dispatch(commitStreamMessage({ topicId, messageId: message.id }))
@ -347,6 +348,7 @@ export const sendMessage =
// 创建节流函数限制Redux更新频率 // 创建节流函数限制Redux更新频率
// 使用节流函数更新Redux // 使用节流函数更新Redux
throttledDispatch( throttledDispatch(
assistant,
{ {
...assistantMessage, ...assistantMessage,
...updateMessage ...updateMessage