From 56e9a7371a8e518c2402ef5f9627dc4b4aeb4706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Sun, 6 Apr 2025 08:43:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(export):=20=E6=B7=BB=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E8=8F=9C=E5=8D=95=E9=80=89=E9=A1=B9=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E3=80=81=E6=80=9D=E7=BB=B4=E9=93=BE=E5=AF=BC=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20(#4168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(settings): Add export menu setting & optimize data settings page * feat: add dynamic export menu options from Redux state in MessageMenubar and TopicsTab * feat(export): Add export to markdown with reasoning method * feat(export): optimize reasoning style * feat(export): Add export to markdown with reasoning to export menu * feat(i18n): Update i18n for new export options --------- Co-authored-by: 亢奋猫 --- .../src/components/DividerWithText.tsx | 35 ++++++ src/renderer/src/i18n/locales/en-us.json | 20 +++- src/renderer/src/i18n/locales/ja-jp.json | 20 +++- src/renderer/src/i18n/locales/ru-ru.json | 20 +++- src/renderer/src/i18n/locales/zh-cn.json | 20 +++- src/renderer/src/i18n/locales/zh-tw.json | 20 +++- .../pages/home/Messages/MessageMenubar.tsx | 49 +++++++-- .../src/pages/home/Tabs/TopicsTab.tsx | 55 +++++---- .../settings/DataSettings/DataSettings.tsx | 38 +++++-- .../DataSettings/ExportMenuSettings.tsx | 104 ++++++++++++++++++ src/renderer/src/store/settings.ts | 30 ++++- src/renderer/src/utils/export.ts | 57 ++++++++-- 12 files changed, 406 insertions(+), 62 deletions(-) create mode 100644 src/renderer/src/components/DividerWithText.tsx create mode 100644 src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx diff --git a/src/renderer/src/components/DividerWithText.tsx b/src/renderer/src/components/DividerWithText.tsx new file mode 100644 index 00000000..0a160894 --- /dev/null +++ b/src/renderer/src/components/DividerWithText.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import styled from 'styled-components' + +interface DividerWithTextProps { + text: string +} + +const DividerWithText: React.FC = ({ text }) => { + return ( + + {text} + + + ) +} + +const DividerContainer = styled.div` + display: flex; + align-items: center; + margin: 0px 0; +` + +const DividerText = styled.span` + font-size: 12px; + color: var(--color-text-2); + margin-right: 8px; +` + +const DividerLine = styled.div` + flex: 1; + height: 1px; + background-color: var(--color-border); +` + +export default DividerWithText diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 9493e039..7d408a7c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -196,6 +196,7 @@ "topics.export.image": "Export as image", "topics.export.joplin": "Export to Joplin", "topics.export.md": "Export as markdown", + "topics.export.md.reason": "Export as Markdown (with reasoning)", "topics.export.notion": "Export to Notion", "topics.export.obsidian": "Export to Obsidian", "topics.export.obsidian_vault": "Vault", @@ -296,7 +297,8 @@ "select": "Select", "topics": "Topics", "warning": "Warning", - "you": "You" + "you": "You", + "reasoning_content": "Deep reasoning" }, "docs": { "title": "Docs" @@ -795,8 +797,24 @@ "title": "Clear Cache" }, "data.title": "Data Directory", + "divider.basic": "Basic Data Settings", + "divider.cloud_storage": "Cloud Backup Settings", + "divider.export_settings": "Export Settings", + "divider.third_party": "Third-party Connections", "hour_interval_one": "{{count}} hour", "hour_interval_other": "{{count}} hours", + "export_menu": { + "title": "Export Menu Settings", + "image": "Export as Image", + "markdown": "Export as Markdown", + "markdown_reason": "Export as Markdown (with reasoning)", + "notion": "Export to Notion", + "yuque": "Export to Yuque", + "obsidian": "Export to Obsidian", + "siyuan": "Export to SiYuan Note", + "joplin": "Export to Joplin", + "docx": "Export as Word" + }, "joplin": { "check": { "button": "Check", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 141a8fb6..959d9381 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -196,6 +196,7 @@ "topics.export.image": "画像としてエクスポート", "topics.export.joplin": "Joplin にエクスポート", "topics.export.md": "Markdownとしてエクスポート", + "topics.export.md.reason": "Markdown としてエクスポート (思考内容を含む)", "topics.export.notion": "Notion にエクスポート", "topics.export.obsidian": "Obsidian にエクスポート", "topics.export.obsidian_vault": "保管庫", @@ -296,7 +297,8 @@ "select": "選択", "topics": "トピック", "warning": "警告", - "you": "あなた" + "you": "あなた", + "reasoning_content": "深く考察済み" }, "docs": { "title": "ドキュメント" @@ -795,8 +797,24 @@ "title": "キャッシュをクリア" }, "data.title": "データディレクトリ", + "divider.basic": "基本データ設定", + "divider.cloud_storage": "クラウドバックアップ設定", + "divider.export_settings": "エクスポート設定", + "divider.third_party": "サードパーティー連携", "hour_interval_one": "{{count}} 時間", "hour_interval_other": "{{count}} 時間", + "export_menu": { + "title": "エクスポートメニュー設定", + "image": "画像としてエクスポート", + "markdown": "Markdownとしてエクスポート", + "markdown_reason": "Markdownとしてエクスポート(思考内容を含む)", + "notion": "Notionにエクスポート", + "yuque": "語雀にエクスポート", + "obsidian": "Obsidianにエクスポート", + "siyuan": "思源ノートにエクスポート", + "joplin": "Joplinにエクスポート", + "docx": "Wordとしてエクスポート" + }, "joplin": { "check": { "button": "確認", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b142ba26..5bb1f71d 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -196,6 +196,7 @@ "topics.export.image": "Экспорт как изображение", "topics.export.joplin": "Экспорт в Joplin", "topics.export.md": "Экспорт как markdown", + "topics.export.md.reason": "Экспорт в Markdown (с рассуждениями)", "topics.export.notion": "Экспорт в Notion", "topics.export.obsidian": "Экспорт в Obsidian", "topics.export.obsidian_vault": "Хранилище", @@ -296,7 +297,8 @@ "select": "Выбрать", "topics": "Топики", "warning": "Предупреждение", - "you": "Вы" + "you": "Вы", + "reasoning_content": "Глубокий анализ" }, "docs": { "title": "Документация" @@ -795,8 +797,24 @@ "title": "Очистка кэша" }, "data.title": "Каталог данных", + "divider.basic": "Основные настройки данных", + "divider.cloud_storage": "Настройки облачного резервирования", + "divider.export_settings": "Настройки экспорта", + "divider.third_party": "Сторонние подключения", "hour_interval_one": "{{count}} час", "hour_interval_other": "{{count}} часов", + "export_menu": { + "title": "Настройки меню экспорта", + "image": "Экспорт как изображение", + "markdown": "Экспорт в Markdown", + "markdown_reason": "Экспорт в Markdown (с рассуждениями)", + "notion": "Экспорт в Notion", + "yuque": "Экспорт в Yuque", + "obsidian": "Экспорт в Obsidian", + "siyuan": "Экспорт в SiYuan Note", + "joplin": "Экспорт в Joplin", + "docx": "Экспорт в Word" + }, "joplin": { "check": { "button": "Проверить", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 098e22de..3b7189ac 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -198,6 +198,7 @@ "topics.export.image": "导出为图片", "topics.export.joplin": "导出到 Joplin", "topics.export.md": "导出为 Markdown", + "topics.export.md.reason": "导出为 Markdown (包含思考)", "topics.export.notion": "导出到 Notion", "topics.export.obsidian": "导出到 Obsidian", "topics.export.obsidian_vault": "保管库", @@ -296,7 +297,8 @@ "select": "选择", "topics": "话题", "warning": "警告", - "you": "用户" + "you": "用户", + "reasoning_content": "已深度思考" }, "docs": { "title": "帮助文档" @@ -795,8 +797,24 @@ "title": "清除缓存" }, "data.title": "数据目录", + "divider.basic": "基础数据设置", + "divider.cloud_storage": "云备份设置", + "divider.export_settings": "导出设置", + "divider.third_party": "第三方连接", "hour_interval_one": "{{count}} 小时", "hour_interval_other": "{{count}} 小时", + "export_menu": { + "title": "导出菜单设置", + "image": "导出为图片", + "markdown": "导出为Markdown", + "markdown_reason": "导出为Markdown(包含思考)", + "notion": "导出到Notion", + "yuque": "导出到语雀", + "obsidian": "导出到Obsidian", + "siyuan": "导出到思源笔记", + "joplin": "导出到Joplin", + "docx": "导出为Word" + }, "joplin": { "check": { "button": "检查", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index ea0ac90a..62bf5b8f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -196,6 +196,7 @@ "topics.export.image": "匯出為圖片", "topics.export.joplin": "匯出到 Joplin", "topics.export.md": "匯出為 Markdown", + "topics.export.md.reason": "匯出為 Markdown (包含思考)", "topics.export.notion": "匯出到 Notion", "topics.export.obsidian": "匯出到 Obsidian", "topics.export.obsidian_vault": "保管庫", @@ -296,7 +297,8 @@ "select": "選擇", "topics": "話題", "warning": "警告", - "you": "您" + "you": "您", + "reasoning_content": "已深度思考" }, "docs": { "title": "說明文件" @@ -795,8 +797,24 @@ "title": "清除快取" }, "data.title": "資料目錄", + "divider.basic": "基礎數據設定", + "divider.cloud_storage": "雲備份設定", + "divider.export_settings": "匯出設定", + "divider.third_party": "第三方連接", "hour_interval_one": "{{count}} 小時", "hour_interval_other": "{{count}} 小時", + "export_menu": { + "title": "匯出選單設定", + "image": "匯出為圖片", + "markdown": "匯出為Markdown", + "markdown_reason": "匯出為Markdown(包含思考)", + "notion": "匯出到Notion", + "yuque": "匯出到語雀", + "obsidian": "匯出到Obsidian", + "siyuan": "匯出到思源筆記", + "joplin": "匯出到Joplin", + "docx": "匯出為Word" + }, "joplin": { "check": { "button": "檢查", diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index f5fa7403..31e86977 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -21,6 +21,7 @@ import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessag import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService' import { translateText } from '@renderer/services/TranslateService' +import { RootState } from '@renderer/store' import type { Message, Model } from '@renderer/types' import type { Assistant, Topic } from '@renderer/types' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils' @@ -38,6 +39,7 @@ import dayjs from 'dayjs' import { clone } from 'lodash' import { FC, memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import styled from 'styled-components' interface Props { @@ -68,6 +70,21 @@ const MessageMenubar: FC = (props) => { const isUserMessage = message.role === 'user' + const exportMenuOptions = useSelector( + (state: RootState) => + state.settings.exportMenuOptions || { + image: true, + markdown: true, + markdown_reason: true, + notion: true, + yuque: true, + joplin: true, + obsidian: true, + siyuan: true, + docx: true + } + ) + const onCopy = useCallback( (e: React.MouseEvent) => { e.stopPropagation() @@ -182,7 +199,7 @@ const MessageMenubar: FC = (props) => { key: 'export', icon: , children: [ - { + exportMenuOptions.image && { label: t('chat.topics.copy.image'), key: 'img', onClick: async () => { @@ -193,7 +210,7 @@ const MessageMenubar: FC = (props) => { }) } }, - { + exportMenuOptions.image && { label: t('chat.topics.export.image'), key: 'image', onClick: async () => { @@ -204,9 +221,17 @@ const MessageMenubar: FC = (props) => { } } }, - { label: t('chat.topics.export.md'), key: 'markdown', onClick: () => exportMessageAsMarkdown(message) }, - - { + exportMenuOptions.markdown && { + label: t('chat.topics.export.md'), + key: 'markdown', + onClick: () => exportMessageAsMarkdown(message) + }, + exportMenuOptions.markdown_reason && { + label: t('chat.topics.export.md.reason'), + key: 'markdown_reason', + onClick: () => exportMessageAsMarkdown(message, true) + }, + exportMenuOptions.docx && { label: t('chat.topics.export.word'), key: 'word', onClick: async () => { @@ -215,7 +240,7 @@ const MessageMenubar: FC = (props) => { window.api.export.toWord(markdown, title) } }, - { + exportMenuOptions.notion && { label: t('chat.topics.export.notion'), key: 'notion', onClick: async () => { @@ -224,7 +249,7 @@ const MessageMenubar: FC = (props) => { exportMarkdownToNotion(title, markdown) } }, - { + exportMenuOptions.yuque && { label: t('chat.topics.export.yuque'), key: 'yuque', onClick: async () => { @@ -233,7 +258,7 @@ const MessageMenubar: FC = (props) => { exportMarkdownToYuque(title, markdown) } }, - { + exportMenuOptions.obsidian && { label: t('chat.topics.export.obsidian'), key: 'obsidian', onClick: async () => { @@ -242,7 +267,7 @@ const MessageMenubar: FC = (props) => { await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' }) } }, - { + exportMenuOptions.joplin && { label: t('chat.topics.export.joplin'), key: 'joplin', onClick: async () => { @@ -251,7 +276,7 @@ const MessageMenubar: FC = (props) => { exportMarkdownToJoplin(title, markdown) } }, - { + exportMenuOptions.siyuan && { label: t('chat.topics.export.siyuan'), key: 'siyuan', onClick: async () => { @@ -260,10 +285,10 @@ const MessageMenubar: FC = (props) => { exportMarkdownToSiyuan(title, markdown) } } - ] + ].filter(Boolean) } ], - [message, messageContainerRef, onEdit, onNewBranch, t, topic.name] + [message, messageContainerRef, onEdit, onNewBranch, t, topic.name, exportMenuOptions] ) const onRegenerate = async (e: React.MouseEvent | undefined) => { diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 0773fd97..b259890d 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -21,6 +21,7 @@ import { TopicManager } from '@renderer/hooks/useTopic' import { fetchMessagesSummary } from '@renderer/services/ApiService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import store from '@renderer/store' +import { RootState } from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' import { Assistant, Topic } from '@renderer/types' import { removeSpecialCharactersForFileName } from '@renderer/utils' @@ -35,10 +36,12 @@ import { } from '@renderer/utils/export' import { hasTopicPendingRequests } from '@renderer/utils/queue' 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 { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import styled from 'styled-components' interface Props { @@ -153,6 +156,21 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic [setActiveTopic] ) + const exportMenuOptions = useSelector( + (state: RootState) => + state.settings.exportMenuOptions || { + image: true, + markdown: true, + markdown_reason: true, + notion: true, + yuque: true, + joplin: true, + obsidian: true, + siyuan: true, + docx: true + } + ) + const getTopicMenuItems = useCallback( (topic: Topic) => { const menus: MenuProps['items'] = [ @@ -255,18 +273,22 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic 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 () => { @@ -274,14 +296,14 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic 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 () => { @@ -289,7 +311,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic exportMarkdownToYuque(topic.name, markdown) } }, - { + exportMenuOptions.obsidian !== false && { label: t('chat.topics.export.obsidian'), key: 'obsidian', onClick: async () => { @@ -297,7 +319,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' }) } }, - { + exportMenuOptions.joplin !== false && { label: t('chat.topics.export.joplin'), key: 'joplin', onClick: async () => { @@ -305,7 +327,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic exportMarkdownToJoplin(topic.name, markdown) } }, - { + exportMenuOptions.siyuan !== false && { label: t('chat.topics.export.siyuan'), key: 'siyuan', onClick: async () => { @@ -313,7 +335,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic exportMarkdownToSiyuan(topic.name, markdown) } } - ] + ].filter(Boolean) as ItemType[] } ] @@ -345,18 +367,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic return menus }, - [ - t, - assistants, - assistant, - updateTopic, - activeTopic.id, - setActiveTopic, - onPinTopic, - onClearMessages, - onMoveTopic, - onDeleteTopic - ] + [assistant, assistants, onClearMessages, onDeleteTopic, onPinTopic, onMoveTopic, t, updateTopic, exportMenuOptions] ) return ( diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index c9ade999..250e86ec 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -4,9 +4,11 @@ import { FileMarkdownOutlined, FileSearchOutlined, FolderOpenOutlined, + MenuOutlined, SaveOutlined, YuqueOutlined } from '@ant-design/icons' +import DividerWithText from '@renderer/components/DividerWithText' import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons' import { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' @@ -23,6 +25,7 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import ExportMenuOptions from './ExportMenuSettings' import JoplinSettings from './JoplinSettings' import MarkdownExportSettings from './MarkdownExportSettings' import NotionSettings from './NotionSettings' @@ -63,14 +66,23 @@ const DataSettings: FC = () => { ) const menuItems = [ + { key: 'divider_0', isDivider: true, text: t('settings.data.divider.basic') }, { key: 'data', title: 'settings.data.data.title', icon: }, + { key: 'divider_1', isDivider: true, text: t('settings.data.divider.cloud_storage') }, { key: 'webdav', title: 'settings.data.webdav.title', icon: }, { key: 'nutstore', title: 'settings.data.nutstore.title', icon: }, + { key: 'divider_2', isDivider: true, text: t('settings.data.divider.export_settings') }, + { + key: 'export_menu', + title: 'settings.data.export_menu.title', + icon: + }, { key: 'markdown_export', title: 'settings.data.markdown_export.title', icon: }, + { key: 'divider_3', isDivider: true, text: t('settings.data.divider.third_party') }, { key: 'notion', title: 'settings.data.notion.title', icon: }, { key: 'yuque', @@ -80,7 +92,6 @@ const DataSettings: FC = () => { { key: 'joplin', title: 'settings.data.joplin.title', - //joplin icon needs to be updated into iconfont icon: }, { @@ -148,16 +159,20 @@ const DataSettings: FC = () => { return ( - {menuItems.map((item) => ( - setMenu(item.key)} - titleStyle={{ fontWeight: 500 }} - icon={item.icon} - /> - ))} + {menuItems.map((item) => + item.isDivider ? ( + // 动态传递分隔符文字 + ) : ( + setMenu(item.key)} + titleStyle={{ fontWeight: 500 }} + icon={item.icon} + /> + ) + )} {menu === 'data' && ( @@ -227,6 +242,7 @@ const DataSettings: FC = () => { )} {menu === 'webdav' && } {menu === 'nutstore' && } + {menu === 'export_menu' && } {menu === 'markdown_export' && } {menu === 'notion' && } {menu === 'yuque' && } diff --git a/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx b/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx new file mode 100644 index 00000000..847f05c1 --- /dev/null +++ b/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx @@ -0,0 +1,104 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import { RootState, useAppDispatch } from '@renderer/store' +import { setExportMenuOptions } from '@renderer/store/settings' +import { Switch } from 'antd' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const ExportMenuOptions: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + + const exportMenuOptions = useSelector( + (state: RootState) => + state.settings.exportMenuOptions || { + image: true, + markdown: true, + markdown_reason: true, + notion: true, + yuque: true, + joplin: true, + obsidian: true, + siyuan: true, + docx: true + } + ) + + const handleToggleOption = (option: string, checked: boolean) => { + dispatch( + setExportMenuOptions({ + ...exportMenuOptions, + [option]: checked + }) + ) + } + + return ( + + {t('settings.data.export_menu.title')} + + + + {t('settings.data.export_menu.image')} + handleToggleOption('image', checked)} /> + + + + + {t('settings.data.export_menu.markdown')} + handleToggleOption('markdown', checked)} /> + + + + + {t('settings.data.export_menu.markdown_reason')} + handleToggleOption('markdown_reason', checked)} + /> + + + + + {t('settings.data.export_menu.notion')} + handleToggleOption('notion', checked)} /> + + + + + {t('settings.data.export_menu.yuque')} + handleToggleOption('yuque', checked)} /> + + + + + {t('settings.data.export_menu.joplin')} + handleToggleOption('joplin', checked)} /> + + + + + {t('settings.data.export_menu.obsidian')} + handleToggleOption('obsidian', checked)} /> + + + + + {t('settings.data.export_menu.siyuan')} + handleToggleOption('siyuan', checked)} /> + + + + + {t('settings.data.export_menu.docx')} + handleToggleOption('docx', checked)} /> + + + ) +} + +export default ExportMenuOptions diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 2f3439f8..18c503ad 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -110,6 +110,17 @@ export interface SettingsState { showOpenedMinappsInSidebar: boolean // 隐私设置 enableDataCollection: boolean + exportMenuOptions: { + image: boolean + markdown: boolean + markdown_reason: boolean + notion: boolean + yuque: boolean + joplin: boolean + obsidian: boolean + siyuan: boolean + docx: boolean + } } export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid' @@ -196,7 +207,18 @@ const initialState: SettingsState = { siyuanRootPath: null, maxKeepAliveMinapps: 3, showOpenedMinappsInSidebar: true, - enableDataCollection: false + enableDataCollection: false, + exportMenuOptions: { + image: true, + markdown: true, + markdown_reason: true, + notion: true, + yuque: true, + joplin: true, + obsidian: true, + siyuan: true, + docx: true + } } const settingsSlice = createSlice({ @@ -451,6 +473,9 @@ const settingsSlice = createSlice({ }, setEnableDataCollection: (state, action: PayloadAction) => { state.enableDataCollection = action.payload + }, + setExportMenuOptions: (state, action: PayloadAction) => { + state.exportMenuOptions = action.payload } } }) @@ -536,7 +561,8 @@ export const { setSiyuanRootPath, setMaxKeepAliveMinapps, setShowOpenedMinappsInSidebar, - setEnableDataCollection + setEnableDataCollection, + setExportMenuOptions } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index 188205c8..dc056372 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -8,6 +8,7 @@ import { Message, Topic } from '@renderer/types' import { convertMathFormula, removeSpecialCharactersForFileName } from '@renderer/utils/index' import { markdownToBlocks } from '@tryfabric/martian' import dayjs from 'dayjs' +//TODO: 添加对思考内容的支持 export const messageToMarkdown = (message: Message) => { const { forceDollarMathInMarkdown } = store.getState().settings @@ -18,27 +19,63 @@ export const messageToMarkdown = (message: Message) => { return [titleSection, '', contentSection].join('\n') } -export const messagesToMarkdown = (messages: Message[]) => { - return messages.map((message) => messageToMarkdown(message)).join('\n\n---\n\n') +// 保留接口用于其它导出方法使用 +export const messageToMarkdownWithReasoning = (message: Message) => { + const { forceDollarMathInMarkdown } = store.getState().settings + const roleText = message.role === 'user' ? '🧑‍💻 User' : '🤖 Assistant' + const titleSection = `### ${roleText}` + + // 处理思考内容 + let reasoningSection = '' + if (message.reasoning_content) { + // 移除开头的标记和换行符,并将所有换行符替换为
+ let reasoningContent = message.reasoning_content + if (reasoningContent.startsWith('\n')) { + reasoningContent = reasoningContent.substring(8) + } else if (reasoningContent.startsWith('')) { + reasoningContent = reasoningContent.substring(7) + } + reasoningContent = reasoningContent.replace(/\n/g, '
') + + // 应用数学公式转换(如果启用) + if (forceDollarMathInMarkdown) { + reasoningContent = convertMathFormula(reasoningContent) + } + // 添加思考内容的Markdown格式 + reasoningSection = `
+ ${i18n.t('common.reasoning_content')}
+ ${reasoningContent} +
` + } + + const contentSection = forceDollarMathInMarkdown ? convertMathFormula(message.content) : message.content + + return [titleSection, '', reasoningSection + contentSection].join('\n') } -export const topicToMarkdown = async (topic: Topic) => { +export const messagesToMarkdown = (messages: Message[], exportReasoning?: boolean) => { + return messages + .map((message) => (exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message))) + .join('\n\n---\n\n') +} + +export const topicToMarkdown = async (topic: Topic, exportReasoning?: boolean) => { const topicName = `# ${topic.name}` const topicMessages = await db.topics.get(topic.id) if (topicMessages) { - return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages) + return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages, exportReasoning) } return '' } -export const exportTopicAsMarkdown = async (topic: Topic) => { +export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: boolean) => { const { markdownExportPath } = store.getState().settings if (!markdownExportPath) { try { const fileName = removeSpecialCharactersForFileName(topic.name) + '.md' - const markdown = await topicToMarkdown(topic) + const markdown = await topicToMarkdown(topic, exportReasoning) const result = await window.api.file.save(fileName, markdown) if (result) { window.message.success({ @@ -53,7 +90,7 @@ export const exportTopicAsMarkdown = async (topic: Topic) => { try { const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss') const fileName = removeSpecialCharactersForFileName(topic.name) + ` ${timestamp}.md` - const markdown = await topicToMarkdown(topic) + const markdown = await topicToMarkdown(topic, exportReasoning) await window.api.file.write(markdownExportPath + '/' + fileName, markdown) window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) } catch (error: any) { @@ -62,13 +99,13 @@ export const exportTopicAsMarkdown = async (topic: Topic) => { } } -export const exportMessageAsMarkdown = async (message: Message) => { +export const exportMessageAsMarkdown = async (message: Message, exportReasoning?: boolean) => { const { markdownExportPath } = store.getState().settings if (!markdownExportPath) { try { const title = await getMessageTitle(message) const fileName = removeSpecialCharactersForFileName(title) + '.md' - const markdown = messageToMarkdown(message) + const markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message) const result = await window.api.file.save(fileName, markdown) if (result) { window.message.success({ @@ -84,7 +121,7 @@ export const exportMessageAsMarkdown = async (message: Message) => { const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss') const title = await getMessageTitle(message) const fileName = removeSpecialCharactersForFileName(title) + ` ${timestamp}.md` - const markdown = messageToMarkdown(message) + const markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message) await window.api.file.write(markdownExportPath + '/' + fileName, markdown) window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) } catch (error: any) {