refactor(export): 添加导出菜单选项设置、思维链导出功能 (#4168)
* 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: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
parent
95639df35c
commit
56e9a7371a
35
src/renderer/src/components/DividerWithText.tsx
Normal file
35
src/renderer/src/components/DividerWithText.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface DividerWithTextProps {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DividerWithText: React.FC<DividerWithTextProps> = ({ text }) => {
|
||||||
|
return (
|
||||||
|
<DividerContainer>
|
||||||
|
<DividerText>{text}</DividerText>
|
||||||
|
<DividerLine />
|
||||||
|
</DividerContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -196,6 +196,7 @@
|
|||||||
"topics.export.image": "Export as image",
|
"topics.export.image": "Export as image",
|
||||||
"topics.export.joplin": "Export to Joplin",
|
"topics.export.joplin": "Export to Joplin",
|
||||||
"topics.export.md": "Export as markdown",
|
"topics.export.md": "Export as markdown",
|
||||||
|
"topics.export.md.reason": "Export as Markdown (with reasoning)",
|
||||||
"topics.export.notion": "Export to Notion",
|
"topics.export.notion": "Export to Notion",
|
||||||
"topics.export.obsidian": "Export to Obsidian",
|
"topics.export.obsidian": "Export to Obsidian",
|
||||||
"topics.export.obsidian_vault": "Vault",
|
"topics.export.obsidian_vault": "Vault",
|
||||||
@ -296,7 +297,8 @@
|
|||||||
"select": "Select",
|
"select": "Select",
|
||||||
"topics": "Topics",
|
"topics": "Topics",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"you": "You"
|
"you": "You",
|
||||||
|
"reasoning_content": "Deep reasoning"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "Docs"
|
"title": "Docs"
|
||||||
@ -795,8 +797,24 @@
|
|||||||
"title": "Clear Cache"
|
"title": "Clear Cache"
|
||||||
},
|
},
|
||||||
"data.title": "Data Directory",
|
"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_one": "{{count}} hour",
|
||||||
"hour_interval_other": "{{count}} hours",
|
"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": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
"button": "Check",
|
"button": "Check",
|
||||||
|
|||||||
@ -196,6 +196,7 @@
|
|||||||
"topics.export.image": "画像としてエクスポート",
|
"topics.export.image": "画像としてエクスポート",
|
||||||
"topics.export.joplin": "Joplin にエクスポート",
|
"topics.export.joplin": "Joplin にエクスポート",
|
||||||
"topics.export.md": "Markdownとしてエクスポート",
|
"topics.export.md": "Markdownとしてエクスポート",
|
||||||
|
"topics.export.md.reason": "Markdown としてエクスポート (思考内容を含む)",
|
||||||
"topics.export.notion": "Notion にエクスポート",
|
"topics.export.notion": "Notion にエクスポート",
|
||||||
"topics.export.obsidian": "Obsidian にエクスポート",
|
"topics.export.obsidian": "Obsidian にエクスポート",
|
||||||
"topics.export.obsidian_vault": "保管庫",
|
"topics.export.obsidian_vault": "保管庫",
|
||||||
@ -296,7 +297,8 @@
|
|||||||
"select": "選択",
|
"select": "選択",
|
||||||
"topics": "トピック",
|
"topics": "トピック",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "あなた"
|
"you": "あなた",
|
||||||
|
"reasoning_content": "深く考察済み"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "ドキュメント"
|
"title": "ドキュメント"
|
||||||
@ -795,8 +797,24 @@
|
|||||||
"title": "キャッシュをクリア"
|
"title": "キャッシュをクリア"
|
||||||
},
|
},
|
||||||
"data.title": "データディレクトリ",
|
"data.title": "データディレクトリ",
|
||||||
|
"divider.basic": "基本データ設定",
|
||||||
|
"divider.cloud_storage": "クラウドバックアップ設定",
|
||||||
|
"divider.export_settings": "エクスポート設定",
|
||||||
|
"divider.third_party": "サードパーティー連携",
|
||||||
"hour_interval_one": "{{count}} 時間",
|
"hour_interval_one": "{{count}} 時間",
|
||||||
"hour_interval_other": "{{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": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
"button": "確認",
|
"button": "確認",
|
||||||
|
|||||||
@ -196,6 +196,7 @@
|
|||||||
"topics.export.image": "Экспорт как изображение",
|
"topics.export.image": "Экспорт как изображение",
|
||||||
"topics.export.joplin": "Экспорт в Joplin",
|
"topics.export.joplin": "Экспорт в Joplin",
|
||||||
"topics.export.md": "Экспорт как markdown",
|
"topics.export.md": "Экспорт как markdown",
|
||||||
|
"topics.export.md.reason": "Экспорт в Markdown (с рассуждениями)",
|
||||||
"topics.export.notion": "Экспорт в Notion",
|
"topics.export.notion": "Экспорт в Notion",
|
||||||
"topics.export.obsidian": "Экспорт в Obsidian",
|
"topics.export.obsidian": "Экспорт в Obsidian",
|
||||||
"topics.export.obsidian_vault": "Хранилище",
|
"topics.export.obsidian_vault": "Хранилище",
|
||||||
@ -296,7 +297,8 @@
|
|||||||
"select": "Выбрать",
|
"select": "Выбрать",
|
||||||
"topics": "Топики",
|
"topics": "Топики",
|
||||||
"warning": "Предупреждение",
|
"warning": "Предупреждение",
|
||||||
"you": "Вы"
|
"you": "Вы",
|
||||||
|
"reasoning_content": "Глубокий анализ"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "Документация"
|
"title": "Документация"
|
||||||
@ -795,8 +797,24 @@
|
|||||||
"title": "Очистка кэша"
|
"title": "Очистка кэша"
|
||||||
},
|
},
|
||||||
"data.title": "Каталог данных",
|
"data.title": "Каталог данных",
|
||||||
|
"divider.basic": "Основные настройки данных",
|
||||||
|
"divider.cloud_storage": "Настройки облачного резервирования",
|
||||||
|
"divider.export_settings": "Настройки экспорта",
|
||||||
|
"divider.third_party": "Сторонние подключения",
|
||||||
"hour_interval_one": "{{count}} час",
|
"hour_interval_one": "{{count}} час",
|
||||||
"hour_interval_other": "{{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": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
"button": "Проверить",
|
"button": "Проверить",
|
||||||
|
|||||||
@ -198,6 +198,7 @@
|
|||||||
"topics.export.image": "导出为图片",
|
"topics.export.image": "导出为图片",
|
||||||
"topics.export.joplin": "导出到 Joplin",
|
"topics.export.joplin": "导出到 Joplin",
|
||||||
"topics.export.md": "导出为 Markdown",
|
"topics.export.md": "导出为 Markdown",
|
||||||
|
"topics.export.md.reason": "导出为 Markdown (包含思考)",
|
||||||
"topics.export.notion": "导出到 Notion",
|
"topics.export.notion": "导出到 Notion",
|
||||||
"topics.export.obsidian": "导出到 Obsidian",
|
"topics.export.obsidian": "导出到 Obsidian",
|
||||||
"topics.export.obsidian_vault": "保管库",
|
"topics.export.obsidian_vault": "保管库",
|
||||||
@ -296,7 +297,8 @@
|
|||||||
"select": "选择",
|
"select": "选择",
|
||||||
"topics": "话题",
|
"topics": "话题",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "用户"
|
"you": "用户",
|
||||||
|
"reasoning_content": "已深度思考"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "帮助文档"
|
"title": "帮助文档"
|
||||||
@ -795,8 +797,24 @@
|
|||||||
"title": "清除缓存"
|
"title": "清除缓存"
|
||||||
},
|
},
|
||||||
"data.title": "数据目录",
|
"data.title": "数据目录",
|
||||||
|
"divider.basic": "基础数据设置",
|
||||||
|
"divider.cloud_storage": "云备份设置",
|
||||||
|
"divider.export_settings": "导出设置",
|
||||||
|
"divider.third_party": "第三方连接",
|
||||||
"hour_interval_one": "{{count}} 小时",
|
"hour_interval_one": "{{count}} 小时",
|
||||||
"hour_interval_other": "{{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": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
"button": "检查",
|
"button": "检查",
|
||||||
|
|||||||
@ -196,6 +196,7 @@
|
|||||||
"topics.export.image": "匯出為圖片",
|
"topics.export.image": "匯出為圖片",
|
||||||
"topics.export.joplin": "匯出到 Joplin",
|
"topics.export.joplin": "匯出到 Joplin",
|
||||||
"topics.export.md": "匯出為 Markdown",
|
"topics.export.md": "匯出為 Markdown",
|
||||||
|
"topics.export.md.reason": "匯出為 Markdown (包含思考)",
|
||||||
"topics.export.notion": "匯出到 Notion",
|
"topics.export.notion": "匯出到 Notion",
|
||||||
"topics.export.obsidian": "匯出到 Obsidian",
|
"topics.export.obsidian": "匯出到 Obsidian",
|
||||||
"topics.export.obsidian_vault": "保管庫",
|
"topics.export.obsidian_vault": "保管庫",
|
||||||
@ -296,7 +297,8 @@
|
|||||||
"select": "選擇",
|
"select": "選擇",
|
||||||
"topics": "話題",
|
"topics": "話題",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "您"
|
"you": "您",
|
||||||
|
"reasoning_content": "已深度思考"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "說明文件"
|
"title": "說明文件"
|
||||||
@ -795,8 +797,24 @@
|
|||||||
"title": "清除快取"
|
"title": "清除快取"
|
||||||
},
|
},
|
||||||
"data.title": "資料目錄",
|
"data.title": "資料目錄",
|
||||||
|
"divider.basic": "基礎數據設定",
|
||||||
|
"divider.cloud_storage": "雲備份設定",
|
||||||
|
"divider.export_settings": "匯出設定",
|
||||||
|
"divider.third_party": "第三方連接",
|
||||||
"hour_interval_one": "{{count}} 小時",
|
"hour_interval_one": "{{count}} 小時",
|
||||||
"hour_interval_other": "{{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": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
"button": "檢查",
|
"button": "檢查",
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessag
|
|||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
|
import { RootState } from '@renderer/store'
|
||||||
import type { Message, Model } from '@renderer/types'
|
import type { Message, Model } from '@renderer/types'
|
||||||
import type { Assistant, Topic } from '@renderer/types'
|
import type { Assistant, Topic } from '@renderer/types'
|
||||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils'
|
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||||
@ -38,6 +39,7 @@ import dayjs from 'dayjs'
|
|||||||
import { clone } from 'lodash'
|
import { clone } from 'lodash'
|
||||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -68,6 +70,21 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
|
|
||||||
const isUserMessage = message.role === 'user'
|
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(
|
const onCopy = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -182,7 +199,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
key: 'export',
|
key: 'export',
|
||||||
icon: <UploadOutlined />,
|
icon: <UploadOutlined />,
|
||||||
children: [
|
children: [
|
||||||
{
|
exportMenuOptions.image && {
|
||||||
label: t('chat.topics.copy.image'),
|
label: t('chat.topics.copy.image'),
|
||||||
key: 'img',
|
key: 'img',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -193,7 +210,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.image && {
|
||||||
label: t('chat.topics.export.image'),
|
label: t('chat.topics.export.image'),
|
||||||
key: 'image',
|
key: 'image',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -204,9 +221,17 @@ const MessageMenubar: FC<Props> = (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'),
|
label: t('chat.topics.export.word'),
|
||||||
key: 'word',
|
key: 'word',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -215,7 +240,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
window.api.export.toWord(markdown, title)
|
window.api.export.toWord(markdown, title)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.notion && {
|
||||||
label: t('chat.topics.export.notion'),
|
label: t('chat.topics.export.notion'),
|
||||||
key: 'notion',
|
key: 'notion',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -224,7 +249,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
exportMarkdownToNotion(title, markdown)
|
exportMarkdownToNotion(title, markdown)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.yuque && {
|
||||||
label: t('chat.topics.export.yuque'),
|
label: t('chat.topics.export.yuque'),
|
||||||
key: 'yuque',
|
key: 'yuque',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -233,7 +258,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
exportMarkdownToYuque(title, markdown)
|
exportMarkdownToYuque(title, markdown)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.obsidian && {
|
||||||
label: t('chat.topics.export.obsidian'),
|
label: t('chat.topics.export.obsidian'),
|
||||||
key: 'obsidian',
|
key: 'obsidian',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -242,7 +267,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' })
|
await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.joplin && {
|
||||||
label: t('chat.topics.export.joplin'),
|
label: t('chat.topics.export.joplin'),
|
||||||
key: 'joplin',
|
key: 'joplin',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -251,7 +276,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
exportMarkdownToJoplin(title, markdown)
|
exportMarkdownToJoplin(title, markdown)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.siyuan && {
|
||||||
label: t('chat.topics.export.siyuan'),
|
label: t('chat.topics.export.siyuan'),
|
||||||
key: 'siyuan',
|
key: 'siyuan',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -260,10 +285,10 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
exportMarkdownToSiyuan(title, markdown)
|
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) => {
|
const onRegenerate = async (e: React.MouseEvent | undefined) => {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { TopicManager } from '@renderer/hooks/useTopic'
|
|||||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
|
import { RootState } from '@renderer/store'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||||
@ -35,10 +36,12 @@ import {
|
|||||||
} from '@renderer/utils/export'
|
} from '@renderer/utils/export'
|
||||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||||
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
||||||
|
import { ItemType, MenuItemType } from 'antd/es/menu/interface'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import { FC, startTransition, useCallback, useMemo, useRef, useState } from 'react'
|
import { FC, startTransition, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -153,6 +156,21 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
[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(
|
const getTopicMenuItems = useCallback(
|
||||||
(topic: Topic) => {
|
(topic: Topic) => {
|
||||||
const menus: MenuProps['items'] = [
|
const menus: MenuProps['items'] = [
|
||||||
@ -255,18 +273,22 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
key: 'export',
|
key: 'export',
|
||||||
icon: <UploadOutlined />,
|
icon: <UploadOutlined />,
|
||||||
children: [
|
children: [
|
||||||
{
|
exportMenuOptions.image !== false && {
|
||||||
label: t('chat.topics.export.image'),
|
label: t('chat.topics.export.image'),
|
||||||
key: 'image',
|
key: 'image',
|
||||||
onClick: () => EventEmitter.emit(EVENT_NAMES.EXPORT_TOPIC_IMAGE, topic)
|
onClick: () => EventEmitter.emit(EVENT_NAMES.EXPORT_TOPIC_IMAGE, topic)
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.markdown !== false && {
|
||||||
label: t('chat.topics.export.md'),
|
label: t('chat.topics.export.md'),
|
||||||
key: 'markdown',
|
key: 'markdown',
|
||||||
onClick: () => exportTopicAsMarkdown(topic)
|
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'),
|
label: t('chat.topics.export.word'),
|
||||||
key: 'word',
|
key: 'word',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -274,14 +296,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
window.api.export.toWord(markdown, removeSpecialCharactersForFileName(topic.name))
|
window.api.export.toWord(markdown, removeSpecialCharactersForFileName(topic.name))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.notion !== false && {
|
||||||
label: t('chat.topics.export.notion'),
|
label: t('chat.topics.export.notion'),
|
||||||
key: 'notion',
|
key: 'notion',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
exportTopicToNotion(topic)
|
exportTopicToNotion(topic)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.yuque !== false && {
|
||||||
label: t('chat.topics.export.yuque'),
|
label: t('chat.topics.export.yuque'),
|
||||||
key: 'yuque',
|
key: 'yuque',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -289,7 +311,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
exportMarkdownToYuque(topic.name, markdown)
|
exportMarkdownToYuque(topic.name, markdown)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.obsidian !== false && {
|
||||||
label: t('chat.topics.export.obsidian'),
|
label: t('chat.topics.export.obsidian'),
|
||||||
key: 'obsidian',
|
key: 'obsidian',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -297,7 +319,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' })
|
await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.joplin !== false && {
|
||||||
label: t('chat.topics.export.joplin'),
|
label: t('chat.topics.export.joplin'),
|
||||||
key: 'joplin',
|
key: 'joplin',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -305,7 +327,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
exportMarkdownToJoplin(topic.name, markdown)
|
exportMarkdownToJoplin(topic.name, markdown)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
exportMenuOptions.siyuan !== false && {
|
||||||
label: t('chat.topics.export.siyuan'),
|
label: t('chat.topics.export.siyuan'),
|
||||||
key: 'siyuan',
|
key: 'siyuan',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@ -313,7 +335,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
exportMarkdownToSiyuan(topic.name, markdown)
|
exportMarkdownToSiyuan(topic.name, markdown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
].filter(Boolean) as ItemType<MenuItemType>[]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -345,18 +367,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
|
|
||||||
return menus
|
return menus
|
||||||
},
|
},
|
||||||
[
|
[assistant, assistants, onClearMessages, onDeleteTopic, onPinTopic, onMoveTopic, t, updateTopic, exportMenuOptions]
|
||||||
t,
|
|
||||||
assistants,
|
|
||||||
assistant,
|
|
||||||
updateTopic,
|
|
||||||
activeTopic.id,
|
|
||||||
setActiveTopic,
|
|
||||||
onPinTopic,
|
|
||||||
onClearMessages,
|
|
||||||
onMoveTopic,
|
|
||||||
onDeleteTopic
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import {
|
|||||||
FileMarkdownOutlined,
|
FileMarkdownOutlined,
|
||||||
FileSearchOutlined,
|
FileSearchOutlined,
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
|
MenuOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
YuqueOutlined
|
YuqueOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
import DividerWithText from '@renderer/components/DividerWithText'
|
||||||
import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons'
|
import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import ListItem from '@renderer/components/ListItem'
|
import ListItem from '@renderer/components/ListItem'
|
||||||
@ -23,6 +25,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
import ExportMenuOptions from './ExportMenuSettings'
|
||||||
import JoplinSettings from './JoplinSettings'
|
import JoplinSettings from './JoplinSettings'
|
||||||
import MarkdownExportSettings from './MarkdownExportSettings'
|
import MarkdownExportSettings from './MarkdownExportSettings'
|
||||||
import NotionSettings from './NotionSettings'
|
import NotionSettings from './NotionSettings'
|
||||||
@ -63,14 +66,23 @@ const DataSettings: FC = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
|
{ key: 'divider_0', isDivider: true, text: t('settings.data.divider.basic') },
|
||||||
{ key: 'data', title: 'settings.data.data.title', icon: <DatabaseOutlined style={{ fontSize: 16 }} /> },
|
{ key: 'data', title: 'settings.data.data.title', icon: <DatabaseOutlined style={{ fontSize: 16 }} /> },
|
||||||
|
{ key: 'divider_1', isDivider: true, text: t('settings.data.divider.cloud_storage') },
|
||||||
{ key: 'webdav', title: 'settings.data.webdav.title', icon: <CloudSyncOutlined style={{ fontSize: 16 }} /> },
|
{ key: 'webdav', title: 'settings.data.webdav.title', icon: <CloudSyncOutlined style={{ fontSize: 16 }} /> },
|
||||||
{ key: 'nutstore', title: 'settings.data.nutstore.title', icon: <NutstoreIcon /> },
|
{ key: 'nutstore', title: 'settings.data.nutstore.title', icon: <NutstoreIcon /> },
|
||||||
|
{ key: 'divider_2', isDivider: true, text: t('settings.data.divider.export_settings') },
|
||||||
|
{
|
||||||
|
key: 'export_menu',
|
||||||
|
title: 'settings.data.export_menu.title',
|
||||||
|
icon: <MenuOutlined style={{ fontSize: 16 }} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'markdown_export',
|
key: 'markdown_export',
|
||||||
title: 'settings.data.markdown_export.title',
|
title: 'settings.data.markdown_export.title',
|
||||||
icon: <FileMarkdownOutlined style={{ fontSize: 16 }} />
|
icon: <FileMarkdownOutlined style={{ fontSize: 16 }} />
|
||||||
},
|
},
|
||||||
|
{ key: 'divider_3', isDivider: true, text: t('settings.data.divider.third_party') },
|
||||||
{ key: 'notion', title: 'settings.data.notion.title', icon: <i className="iconfont icon-notion" /> },
|
{ key: 'notion', title: 'settings.data.notion.title', icon: <i className="iconfont icon-notion" /> },
|
||||||
{
|
{
|
||||||
key: 'yuque',
|
key: 'yuque',
|
||||||
@ -80,7 +92,6 @@ const DataSettings: FC = () => {
|
|||||||
{
|
{
|
||||||
key: 'joplin',
|
key: 'joplin',
|
||||||
title: 'settings.data.joplin.title',
|
title: 'settings.data.joplin.title',
|
||||||
//joplin icon needs to be updated into iconfont
|
|
||||||
icon: <JoplinIcon />
|
icon: <JoplinIcon />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,16 +159,20 @@ const DataSettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) =>
|
||||||
|
item.isDivider ? (
|
||||||
|
<DividerWithText key={item.key} text={item.text || ''} /> // 动态传递分隔符文字
|
||||||
|
) : (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
title={t(item.title)}
|
title={t(item.title || '')}
|
||||||
active={menu === item.key}
|
active={menu === item.key}
|
||||||
onClick={() => setMenu(item.key)}
|
onClick={() => setMenu(item.key)}
|
||||||
titleStyle={{ fontWeight: 500 }}
|
titleStyle={{ fontWeight: 500 }}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
<SettingContainer theme={theme} style={{ display: 'flex', flex: 1 }}>
|
<SettingContainer theme={theme} style={{ display: 'flex', flex: 1 }}>
|
||||||
{menu === 'data' && (
|
{menu === 'data' && (
|
||||||
@ -227,6 +242,7 @@ const DataSettings: FC = () => {
|
|||||||
)}
|
)}
|
||||||
{menu === 'webdav' && <WebDavSettings />}
|
{menu === 'webdav' && <WebDavSettings />}
|
||||||
{menu === 'nutstore' && <NutstoreSettings />}
|
{menu === 'nutstore' && <NutstoreSettings />}
|
||||||
|
{menu === 'export_menu' && <ExportMenuOptions />}
|
||||||
{menu === 'markdown_export' && <MarkdownExportSettings />}
|
{menu === 'markdown_export' && <MarkdownExportSettings />}
|
||||||
{menu === 'notion' && <NotionSettings />}
|
{menu === 'notion' && <NotionSettings />}
|
||||||
{menu === 'yuque' && <YuqueSettings />}
|
{menu === 'yuque' && <YuqueSettings />}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<SettingGroup theme={theme}>
|
||||||
|
<SettingTitle>{t('settings.data.export_menu.title')}</SettingTitle>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.image')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.image} onChange={(checked) => handleToggleOption('image', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.markdown')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.markdown} onChange={(checked) => handleToggleOption('markdown', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.markdown_reason')}</SettingRowTitle>
|
||||||
|
<Switch
|
||||||
|
checked={exportMenuOptions.markdown_reason}
|
||||||
|
onChange={(checked) => handleToggleOption('markdown_reason', checked)}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.notion')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.notion} onChange={(checked) => handleToggleOption('notion', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.yuque')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.yuque} onChange={(checked) => handleToggleOption('yuque', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.joplin')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.joplin} onChange={(checked) => handleToggleOption('joplin', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.obsidian')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.obsidian} onChange={(checked) => handleToggleOption('obsidian', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.siyuan')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.siyuan} onChange={(checked) => handleToggleOption('siyuan', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle>
|
||||||
|
<Switch checked={exportMenuOptions.docx} onChange={(checked) => handleToggleOption('docx', checked)} />
|
||||||
|
</SettingRow>
|
||||||
|
</SettingGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExportMenuOptions
|
||||||
@ -110,6 +110,17 @@ export interface SettingsState {
|
|||||||
showOpenedMinappsInSidebar: boolean
|
showOpenedMinappsInSidebar: boolean
|
||||||
// 隐私设置
|
// 隐私设置
|
||||||
enableDataCollection: 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'
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
@ -196,7 +207,18 @@ const initialState: SettingsState = {
|
|||||||
siyuanRootPath: null,
|
siyuanRootPath: null,
|
||||||
maxKeepAliveMinapps: 3,
|
maxKeepAliveMinapps: 3,
|
||||||
showOpenedMinappsInSidebar: true,
|
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({
|
const settingsSlice = createSlice({
|
||||||
@ -451,6 +473,9 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setEnableDataCollection: (state, action: PayloadAction<boolean>) => {
|
setEnableDataCollection: (state, action: PayloadAction<boolean>) => {
|
||||||
state.enableDataCollection = action.payload
|
state.enableDataCollection = action.payload
|
||||||
|
},
|
||||||
|
setExportMenuOptions: (state, action: PayloadAction<typeof initialState.exportMenuOptions>) => {
|
||||||
|
state.exportMenuOptions = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -536,7 +561,8 @@ export const {
|
|||||||
setSiyuanRootPath,
|
setSiyuanRootPath,
|
||||||
setMaxKeepAliveMinapps,
|
setMaxKeepAliveMinapps,
|
||||||
setShowOpenedMinappsInSidebar,
|
setShowOpenedMinappsInSidebar,
|
||||||
setEnableDataCollection
|
setEnableDataCollection,
|
||||||
|
setExportMenuOptions
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Message, Topic } from '@renderer/types'
|
|||||||
import { convertMathFormula, removeSpecialCharactersForFileName } from '@renderer/utils/index'
|
import { convertMathFormula, removeSpecialCharactersForFileName } from '@renderer/utils/index'
|
||||||
import { markdownToBlocks } from '@tryfabric/martian'
|
import { markdownToBlocks } from '@tryfabric/martian'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
//TODO: 添加对思考内容的支持
|
||||||
|
|
||||||
export const messageToMarkdown = (message: Message) => {
|
export const messageToMarkdown = (message: Message) => {
|
||||||
const { forceDollarMathInMarkdown } = store.getState().settings
|
const { forceDollarMathInMarkdown } = store.getState().settings
|
||||||
@ -18,27 +19,63 @@ export const messageToMarkdown = (message: Message) => {
|
|||||||
return [titleSection, '', contentSection].join('\n')
|
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) {
|
||||||
|
// 移除开头的<think>标记和换行符,并将所有换行符替换为<br>
|
||||||
|
let reasoningContent = message.reasoning_content
|
||||||
|
if (reasoningContent.startsWith('<think>\n')) {
|
||||||
|
reasoningContent = reasoningContent.substring(8)
|
||||||
|
} else if (reasoningContent.startsWith('<think>')) {
|
||||||
|
reasoningContent = reasoningContent.substring(7)
|
||||||
|
}
|
||||||
|
reasoningContent = reasoningContent.replace(/\n/g, '<br>')
|
||||||
|
|
||||||
|
// 应用数学公式转换(如果启用)
|
||||||
|
if (forceDollarMathInMarkdown) {
|
||||||
|
reasoningContent = convertMathFormula(reasoningContent)
|
||||||
|
}
|
||||||
|
// 添加思考内容的Markdown格式
|
||||||
|
reasoningSection = `<details style="background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;">
|
||||||
|
<summary>${i18n.t('common.reasoning_content')}</summary><hr>
|
||||||
|
${reasoningContent}
|
||||||
|
</details>`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 topicName = `# ${topic.name}`
|
||||||
const topicMessages = await db.topics.get(topic.id)
|
const topicMessages = await db.topics.get(topic.id)
|
||||||
|
|
||||||
if (topicMessages) {
|
if (topicMessages) {
|
||||||
return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages)
|
return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages, exportReasoning)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const exportTopicAsMarkdown = async (topic: Topic) => {
|
export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: boolean) => {
|
||||||
const { markdownExportPath } = store.getState().settings
|
const { markdownExportPath } = store.getState().settings
|
||||||
if (!markdownExportPath) {
|
if (!markdownExportPath) {
|
||||||
try {
|
try {
|
||||||
const fileName = removeSpecialCharactersForFileName(topic.name) + '.md'
|
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)
|
const result = await window.api.file.save(fileName, markdown)
|
||||||
if (result) {
|
if (result) {
|
||||||
window.message.success({
|
window.message.success({
|
||||||
@ -53,7 +90,7 @@ export const exportTopicAsMarkdown = async (topic: Topic) => {
|
|||||||
try {
|
try {
|
||||||
const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss')
|
const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss')
|
||||||
const fileName = removeSpecialCharactersForFileName(topic.name) + ` ${timestamp}.md`
|
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)
|
await window.api.file.write(markdownExportPath + '/' + fileName, markdown)
|
||||||
window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' })
|
window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' })
|
||||||
} catch (error: any) {
|
} 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
|
const { markdownExportPath } = store.getState().settings
|
||||||
if (!markdownExportPath) {
|
if (!markdownExportPath) {
|
||||||
try {
|
try {
|
||||||
const title = await getMessageTitle(message)
|
const title = await getMessageTitle(message)
|
||||||
const fileName = removeSpecialCharactersForFileName(title) + '.md'
|
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)
|
const result = await window.api.file.save(fileName, markdown)
|
||||||
if (result) {
|
if (result) {
|
||||||
window.message.success({
|
window.message.success({
|
||||||
@ -84,7 +121,7 @@ export const exportMessageAsMarkdown = async (message: Message) => {
|
|||||||
const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss')
|
const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss')
|
||||||
const title = await getMessageTitle(message)
|
const title = await getMessageTitle(message)
|
||||||
const fileName = removeSpecialCharactersForFileName(title) + ` ${timestamp}.md`
|
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)
|
await window.api.file.write(markdownExportPath + '/' + fileName, markdown)
|
||||||
window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' })
|
window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user