diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 967e7f4105..03fbad0d8f 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -39,7 +39,7 @@ import { Dropdown, MenuProps, Tooltip } from 'antd' import { ItemType, MenuItemType } from 'antd/es/menu/interface' import dayjs from 'dayjs' import { findIndex } from 'lodash' -import { FC, startTransition, useCallback, useMemo, useRef, useState } from 'react' +import { FC, startTransition, useCallback, useDeferredValue, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import styled from 'styled-components' @@ -158,236 +158,240 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) - const getTopicMenuItems = useCallback( - (topic: Topic) => { - const menus: MenuProps['items'] = [ - { - label: t('chat.topics.auto_rename'), - key: 'auto-rename', - icon: , - async onClick() { - const messages = await TopicManager.getTopicMessages(topic.id) - if (messages.length >= 2) { - const summaryText = await fetchMessagesSummary({ messages, assistant }) - if (summaryText) { - updateTopic({ ...topic, name: summaryText, isNameManuallyEdited: false }) - } - } - } - }, - { - label: t('chat.topics.edit.title'), - key: 'rename', - icon: , - async onClick() { - const name = await PromptPopup.show({ - title: t('chat.topics.edit.title'), - message: '', - defaultValue: topic?.name || '' - }) - if (name && topic?.name !== name) { - updateTopic({ ...topic, name, isNameManuallyEdited: true }) - } - } - }, - { - label: t('chat.topics.prompt'), - key: 'topic-prompt', - icon: , - extra: ( - - - - ), - async onClick() { - const prompt = await PromptPopup.show({ - title: t('chat.topics.prompt.edit.title'), - message: '', - defaultValue: topic?.prompt || '', - inputProps: { - rows: 8, - allowClear: true - } - }) + const [_targetTopic, setTargetTopic] = useState(null) + const targetTopic = useDeferredValue(_targetTopic) + const getTopicMenuItems = useMemo(() => { + const topic = targetTopic + if (!topic) return [] - prompt !== null && - (() => { - const updatedTopic = { ...topic, prompt: prompt.trim() } - updateTopic(updatedTopic) - topic.id === activeTopic.id && setActiveTopic(updatedTopic) - })() - } - }, - { - label: topic.pinned ? t('chat.topics.unpinned') : t('chat.topics.pinned'), - key: 'pin', - icon: , - onClick() { - onPinTopic(topic) - } - }, - { - label: t('chat.topics.clear.title'), - key: 'clear-messages', - icon: , - async onClick() { - window.modal.confirm({ - title: t('chat.input.clear.content'), - centered: true, - onOk: () => onClearMessages(topic) - }) - } - }, - { - label: t('chat.topics.copy.title'), - key: 'copy', - icon: , - children: [ - { - label: t('chat.topics.copy.image'), - key: 'img', - onClick: () => EventEmitter.emit(EVENT_NAMES.COPY_TOPIC_IMAGE, topic) - }, - { - label: t('chat.topics.copy.md'), - key: 'md', - onClick: () => copyTopicAsMarkdown(topic) + const menus: MenuProps['items'] = [ + { + label: t('chat.topics.auto_rename'), + key: 'auto-rename', + icon: , + async onClick() { + const messages = await TopicManager.getTopicMessages(topic.id) + if (messages.length >= 2) { + const summaryText = await fetchMessagesSummary({ messages, assistant }) + if (summaryText) { + updateTopic({ ...topic, name: summaryText, isNameManuallyEdited: false }) } - ] - }, - { - label: t('chat.topics.export.title'), - key: 'export', - icon: , - children: [ - exportMenuOptions.image !== false && { - label: t('chat.topics.export.image'), - key: 'image', - onClick: () => EventEmitter.emit(EVENT_NAMES.EXPORT_TOPIC_IMAGE, topic) - }, - exportMenuOptions.markdown !== false && { - label: t('chat.topics.export.md'), - key: 'markdown', - onClick: () => exportTopicAsMarkdown(topic) - }, - exportMenuOptions.markdown_reason !== false && { - label: t('chat.topics.export.md.reason'), - key: 'markdown_reason', - onClick: () => exportTopicAsMarkdown(topic, true) - }, - exportMenuOptions.docx !== false && { - label: t('chat.topics.export.word'), - key: 'word', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - window.api.export.toWord(markdown, removeSpecialCharactersForFileName(topic.name)) - } - }, - exportMenuOptions.notion !== false && { - label: t('chat.topics.export.notion'), - key: 'notion', - onClick: async () => { - exportTopicToNotion(topic) - } - }, - exportMenuOptions.yuque !== false && { - label: t('chat.topics.export.yuque'), - key: 'yuque', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - exportMarkdownToYuque(topic.name, markdown) - } - }, - exportMenuOptions.obsidian !== false && { - label: t('chat.topics.export.obsidian'), - key: 'obsidian', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' }) - } - }, - exportMenuOptions.joplin !== false && { - label: t('chat.topics.export.joplin'), - key: 'joplin', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - exportMarkdownToJoplin(topic.name, markdown) - } - }, - exportMenuOptions.siyuan !== false && { - label: t('chat.topics.export.siyuan'), - key: 'siyuan', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - exportMarkdownToSiyuan(topic.name, markdown) - } - } - ].filter(Boolean) as ItemType[] + } } - ] + }, + { + label: t('chat.topics.edit.title'), + key: 'rename', + icon: , + async onClick() { + const name = await PromptPopup.show({ + title: t('chat.topics.edit.title'), + message: '', + defaultValue: topic?.name || '' + }) + if (name && topic?.name !== name) { + updateTopic({ ...topic, name, isNameManuallyEdited: true }) + } + } + }, + { + label: t('chat.topics.prompt'), + key: 'topic-prompt', + icon: , + extra: ( + + + + ), + async onClick() { + const prompt = await PromptPopup.show({ + title: t('chat.topics.prompt.edit.title'), + message: '', + defaultValue: topic?.prompt || '', + inputProps: { + rows: 8, + allowClear: true + } + }) - if (assistants.length > 1 && assistant.topics.length > 1) { - menus.push({ - label: t('chat.topics.move_to'), - key: 'move', - icon: , - children: assistants - .filter((a) => a.id !== assistant.id) - .map((a) => ({ - label: a.name, - key: a.id, - onClick: () => onMoveTopic(topic, a) - })) - }) + prompt !== null && + (() => { + const updatedTopic = { ...topic, prompt: prompt.trim() } + updateTopic(updatedTopic) + topic.id === activeTopic.id && setActiveTopic(updatedTopic) + })() + } + }, + { + label: topic.pinned ? t('chat.topics.unpinned') : t('chat.topics.pinned'), + key: 'pin', + icon: , + onClick() { + onPinTopic(topic) + } + }, + { + label: t('chat.topics.clear.title'), + key: 'clear-messages', + icon: , + async onClick() { + window.modal.confirm({ + title: t('chat.input.clear.content'), + centered: true, + onOk: () => onClearMessages(topic) + }) + } + }, + { + label: t('chat.topics.copy.title'), + key: 'copy', + icon: , + children: [ + { + label: t('chat.topics.copy.image'), + key: 'img', + onClick: () => EventEmitter.emit(EVENT_NAMES.COPY_TOPIC_IMAGE, topic) + }, + { + label: t('chat.topics.copy.md'), + key: 'md', + onClick: () => copyTopicAsMarkdown(topic) + } + ] + }, + { + label: t('chat.topics.export.title'), + key: 'export', + icon: , + children: [ + exportMenuOptions.image !== false && { + label: t('chat.topics.export.image'), + key: 'image', + onClick: () => EventEmitter.emit(EVENT_NAMES.EXPORT_TOPIC_IMAGE, topic) + }, + exportMenuOptions.markdown !== false && { + label: t('chat.topics.export.md'), + key: 'markdown', + onClick: () => exportTopicAsMarkdown(topic) + }, + exportMenuOptions.markdown_reason !== false && { + label: t('chat.topics.export.md.reason'), + key: 'markdown_reason', + onClick: () => exportTopicAsMarkdown(topic, true) + }, + exportMenuOptions.docx !== false && { + label: t('chat.topics.export.word'), + key: 'word', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + window.api.export.toWord(markdown, removeSpecialCharactersForFileName(topic.name)) + } + }, + exportMenuOptions.notion !== false && { + label: t('chat.topics.export.notion'), + key: 'notion', + onClick: async () => { + exportTopicToNotion(topic) + } + }, + exportMenuOptions.yuque !== false && { + label: t('chat.topics.export.yuque'), + key: 'yuque', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToYuque(topic.name, markdown) + } + }, + exportMenuOptions.obsidian !== false && { + label: t('chat.topics.export.obsidian'), + key: 'obsidian', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' }) + } + }, + exportMenuOptions.joplin !== false && { + label: t('chat.topics.export.joplin'), + key: 'joplin', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToJoplin(topic.name, markdown) + } + }, + exportMenuOptions.siyuan !== false && { + label: t('chat.topics.export.siyuan'), + key: 'siyuan', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToSiyuan(topic.name, markdown) + } + } + ].filter(Boolean) as ItemType[] } - - if (assistant.topics.length > 1 && !topic.pinned) { - menus.push({ type: 'divider' }) - menus.push({ - label: t('common.delete'), - danger: true, - key: 'delete', - icon: , - onClick: () => onDeleteTopic(topic) - }) - } - - return menus - }, - [ - activeTopic.id, - assistant, - assistants, - exportMenuOptions.docx, - exportMenuOptions.image, - exportMenuOptions.joplin, - exportMenuOptions.markdown, - exportMenuOptions.markdown_reason, - exportMenuOptions.notion, - exportMenuOptions.obsidian, - exportMenuOptions.siyuan, - exportMenuOptions.yuque, - onClearMessages, - onDeleteTopic, - onMoveTopic, - onPinTopic, - setActiveTopic, - t, - updateTopic ] - ) + + if (assistants.length > 1 && assistant.topics.length > 1) { + menus.push({ + label: t('chat.topics.move_to'), + key: 'move', + icon: , + children: assistants + .filter((a) => a.id !== assistant.id) + .map((a) => ({ + label: a.name, + key: a.id, + onClick: () => onMoveTopic(topic, a) + })) + }) + } + + if (assistant.topics.length > 1 && !topic.pinned) { + menus.push({ type: 'divider' }) + menus.push({ + label: t('common.delete'), + danger: true, + key: 'delete', + icon: , + onClick: () => onDeleteTopic(topic) + }) + } + + return menus + }, [ + activeTopic.id, + assistant, + assistants, + exportMenuOptions.docx, + exportMenuOptions.image, + exportMenuOptions.joplin, + exportMenuOptions.markdown, + exportMenuOptions.markdown_reason, + exportMenuOptions.notion, + exportMenuOptions.obsidian, + exportMenuOptions.siyuan, + exportMenuOptions.yuque, + onClearMessages, + onDeleteTopic, + onMoveTopic, + onPinTopic, + setActiveTopic, + t, + updateTopic, + targetTopic + ]) return ( - - - {(topic) => { - const isActive = topic.id === activeTopic?.id - const topicName = topic.name.replace('`', '') - const topicPrompt = topic.prompt - const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt - return ( - + + + + {(topic) => { + const isActive = topic.id === activeTopic?.id + const topicName = topic.name.replace('`', '') + const topicPrompt = topic.prompt + const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt + return ( setTargetTopic(topic)} className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)} style={{ borderRadius }}> @@ -435,12 +439,12 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic )} - - ) - }} - -
-
+ ) + }} + +
+ + ) }