mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 11:20:07 +08:00
refactor: enhance export functions (#5854)
* feat(markdown-export): add option to show model name in export * refactor(export): Refactor the Obsidian export modal to Ant Design style * refactor(obsidian-export): export to obsidian using markdown interface & support COT * feat(markdown-export): optimize COT export style, support export model & provider name Add a new setting to toggle displaying the model provider alongside the model name in markdown exports. Update the export logic to include the provider name when enabled, improving context and clarity of exported messages. Also fix invalid filename character removal regex for Mac. * feat(export): add option to export reasoning in Joplin notes Introduce a new setting to toggle exporting reasoning details when exporting topics or messages to Joplin. Update the export function to handle raw messages and convert them to markdown with or without reasoning based on the setting. This improves the export feature by allowing users to include more detailed context in their Joplin notes. * feat(export): update i18n for new export options * fix(settings): remove duplicate showModelNameInMarkdown state * feat(export): add CoT export for notion & optmize notion export * feat(export): update Notion settings i18n * fix(utils): correct citation markdown formatting Swap citation title and URL positions in markdown links to ensure the link text displays the title (or URL if title is missing) and the link points to the correct URL. This improves citation clarity.
This commit is contained in:
parent
19ff35b779
commit
cb128f51cf
@ -1,26 +1,36 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Spin, TreeSelect } from 'antd'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import {
|
||||
exportMarkdownToObsidian,
|
||||
messagesToMarkdown,
|
||||
messageToMarkdown,
|
||||
messageToMarkdownWithReasoning,
|
||||
topicToMarkdown
|
||||
} from '@renderer/utils/export'
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Spin, Switch, TreeSelect } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface ObsidianExportDialogProps {
|
||||
title: string
|
||||
markdown: string
|
||||
open: boolean
|
||||
onClose: (success: boolean) => void
|
||||
obsidianTags: string | null
|
||||
processingMethod: string | '3' //默认新增(存在就覆盖)
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string
|
||||
type: 'folder' | 'markdown'
|
||||
name: string
|
||||
}
|
||||
|
||||
interface PopupContainerProps {
|
||||
title: string
|
||||
obsidianTags: string | null
|
||||
processingMethod: string | '3'
|
||||
open: boolean
|
||||
resolve: (success: boolean) => void
|
||||
message?: Message
|
||||
messages?: Message[]
|
||||
topic?: Topic
|
||||
}
|
||||
|
||||
// 转换文件信息数组为树形结构
|
||||
const convertToTreeData = (files: FileInfo[]) => {
|
||||
const treeData: any[] = [
|
||||
@ -113,13 +123,15 @@ const convertToTreeData = (files: FileInfo[]) => {
|
||||
return treeData
|
||||
}
|
||||
|
||||
const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
const PopupContainer: React.FC<PopupContainerProps> = ({
|
||||
title,
|
||||
markdown,
|
||||
open,
|
||||
onClose,
|
||||
obsidianTags,
|
||||
processingMethod
|
||||
processingMethod,
|
||||
open,
|
||||
resolve,
|
||||
message,
|
||||
messages,
|
||||
topic
|
||||
}) => {
|
||||
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
|
||||
const [state, setState] = useState({
|
||||
@ -130,8 +142,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
processingMethod: processingMethod,
|
||||
folder: ''
|
||||
})
|
||||
|
||||
// 是否手动编辑过标题
|
||||
const [hasTitleBeenManuallyEdited, setHasTitleBeenManuallyEdited] = useState(false)
|
||||
const [vaults, setVaults] = useState<Array<{ path: string; name: string }>>([])
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
@ -139,8 +149,8 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
const [selectedVault, setSelectedVault] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [exportReasoning, setExportReasoning] = useState(false)
|
||||
|
||||
// 处理文件数据转为树形结构
|
||||
useEffect(() => {
|
||||
if (files.length > 0) {
|
||||
const treeData = convertToTreeData(files)
|
||||
@ -157,28 +167,21 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
}
|
||||
}, [files])
|
||||
|
||||
// 组件加载时获取Vault列表
|
||||
useEffect(() => {
|
||||
const fetchVaults = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const vaultsData = await window.obsidian.getVaults()
|
||||
|
||||
if (vaultsData.length === 0) {
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vaults'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setVaults(vaultsData)
|
||||
|
||||
// 如果没有选择的vault,使用默认值或第一个
|
||||
const vaultToUse = defaultObsidianVault || vaultsData[0]?.name
|
||||
if (vaultToUse) {
|
||||
setSelectedVault(vaultToUse)
|
||||
|
||||
// 获取选中vault的文件和文件夹
|
||||
const filesData = await window.obsidian.getFiles(vaultToUse)
|
||||
setFiles(filesData)
|
||||
}
|
||||
@ -189,11 +192,9 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchVaults()
|
||||
}, [defaultObsidianVault])
|
||||
|
||||
// 当选择的vault变化时,获取其文件和文件夹
|
||||
useEffect(() => {
|
||||
if (selectedVault) {
|
||||
const fetchFiles = async () => {
|
||||
@ -209,7 +210,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchFiles()
|
||||
}
|
||||
}, [selectedVault])
|
||||
@ -219,82 +219,71 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vault_selected'))
|
||||
return
|
||||
}
|
||||
|
||||
//构建content 并复制到粘贴板
|
||||
let markdown = ''
|
||||
if (topic) {
|
||||
markdown = await topicToMarkdown(topic, exportReasoning)
|
||||
} else if (messages && messages.length > 0) {
|
||||
markdown = messagesToMarkdown(messages, exportReasoning)
|
||||
} else if (message) {
|
||||
markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message)
|
||||
} else {
|
||||
markdown = ''
|
||||
}
|
||||
let content = ''
|
||||
if (state.processingMethod !== '3') {
|
||||
content = `\n---\n${markdown}`
|
||||
} else {
|
||||
content = `---
|
||||
\ntitle: ${state.title}
|
||||
\ncreated: ${state.createdAt}
|
||||
\nsource: ${state.source}
|
||||
\ntags: ${state.tags}
|
||||
\n---\n${markdown}`
|
||||
content = `---\n\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}`
|
||||
}
|
||||
if (content === '') {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||
return
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(content)
|
||||
|
||||
// 导出到Obsidian
|
||||
exportMarkdownToObsidian({
|
||||
...state,
|
||||
folder: state.folder,
|
||||
vault: selectedVault
|
||||
})
|
||||
|
||||
onClose(true)
|
||||
setOpen(false)
|
||||
resolve(true)
|
||||
}
|
||||
|
||||
const [openState, setOpen] = useState(open)
|
||||
useEffect(() => {
|
||||
setOpen(open)
|
||||
}, [open])
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose(false)
|
||||
setOpen(false)
|
||||
resolve(false)
|
||||
}
|
||||
|
||||
const handleChange = (key: string, value: any) => {
|
||||
setState((prevState) => ({ ...prevState, [key]: value }))
|
||||
}
|
||||
|
||||
// 处理title输入变化
|
||||
const handleTitleInputChange = (newTitle: string) => {
|
||||
handleChange('title', newTitle)
|
||||
setHasTitleBeenManuallyEdited(true)
|
||||
}
|
||||
|
||||
const handleVaultChange = (value: string) => {
|
||||
setSelectedVault(value)
|
||||
// 文件夹会通过useEffect自动获取
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
folder: ''
|
||||
}))
|
||||
setState((prevState) => ({ ...prevState, folder: '' }))
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (value: string) => {
|
||||
// 更新folder值
|
||||
handleChange('folder', value)
|
||||
|
||||
// 检查是否选中md文件
|
||||
if (value) {
|
||||
const selectedFile = files.find((file) => file.path === value)
|
||||
if (selectedFile) {
|
||||
if (selectedFile.type === 'markdown') {
|
||||
// 如果是md文件,自动设置标题为文件名并设置处理方式为1(追加)
|
||||
const fileName = selectedFile.name
|
||||
const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName
|
||||
handleChange('title', titleWithoutExt)
|
||||
// 重置手动编辑标记,因为这是非用户设置的title
|
||||
setHasTitleBeenManuallyEdited(false)
|
||||
handleChange('processingMethod', '1')
|
||||
} else {
|
||||
// 如果是文件夹,自动设置标题为话题名并设置处理方式为3(新建)
|
||||
handleChange('processingMethod', '3')
|
||||
// 仅当用户未手动编辑过 title 时,才将其重置为 props.title
|
||||
if (!hasTitleBeenManuallyEdited) {
|
||||
// title 是 props.title
|
||||
handleChange('title', title)
|
||||
}
|
||||
}
|
||||
@ -305,7 +294,7 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
return (
|
||||
<Modal
|
||||
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||
open={open}
|
||||
open={openState}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
@ -317,9 +306,9 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
type: 'primary',
|
||||
disabled: vaults.length === 0 || loading || !!error
|
||||
}}
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}>
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}
|
||||
afterClose={() => setOpen(open)}>
|
||||
{error && <Alert message={error} type="error" showIcon style={{ marginBottom: 16 }} />}
|
||||
|
||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
|
||||
<Input
|
||||
@ -328,7 +317,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_vault')}>
|
||||
{vaults.length > 0 ? (
|
||||
<Select
|
||||
@ -354,7 +342,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_path')}>
|
||||
<Spin spinning={loading}>
|
||||
{selectedVault ? (
|
||||
@ -376,7 +363,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
)}
|
||||
</Spin>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
|
||||
<Input
|
||||
value={state.tags}
|
||||
@ -398,7 +384,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}>
|
||||
<Select
|
||||
value={state.processingMethod}
|
||||
@ -410,9 +395,12 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
<Option value="3">{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
|
||||
<Switch checked={exportReasoning} onChange={setExportReasoning} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ObsidianExportDialog
|
||||
export { PopupContainer }
|
||||
|
||||
@ -1,44 +1,38 @@
|
||||
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { PopupContainer } from '@renderer/components/ObsidianExportDialog'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
|
||||
interface ObsidianExportOptions {
|
||||
title: string
|
||||
markdown: string
|
||||
processingMethod: string | '3' // 默认新增(存在就覆盖)
|
||||
processingMethod: string | '3'
|
||||
topic?: Topic
|
||||
message?: Message
|
||||
messages?: Message[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置Obsidian 笔记属性弹窗
|
||||
* @param options.title 标题
|
||||
* @param options.markdown markdown内容
|
||||
* @param options.processingMethod 处理方式
|
||||
* @returns
|
||||
*/
|
||||
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
const root = createRoot(div)
|
||||
|
||||
const handleClose = (success: boolean) => {
|
||||
root.unmount()
|
||||
document.body.removeChild(div)
|
||||
resolve(success)
|
||||
}
|
||||
// 不再从store中获取tag配置
|
||||
root.render(
|
||||
<ObsidianExportDialog
|
||||
title={options.title}
|
||||
markdown={options.markdown}
|
||||
obsidianTags=""
|
||||
processingMethod={options.processingMethod}
|
||||
open={true}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
show: showObsidianExportDialog
|
||||
export default class ObsidianExportPopup {
|
||||
static hide() {
|
||||
TopView.hide('ObsidianExportPopup')
|
||||
}
|
||||
static show(options: ObsidianExportOptions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
title={options.title}
|
||||
processingMethod={options.processingMethod}
|
||||
topic={options.topic}
|
||||
message={options.message}
|
||||
messages={options.messages}
|
||||
obsidianTags={''}
|
||||
open={true}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
ObsidianExportPopup.hide()
|
||||
}}
|
||||
/>,
|
||||
'ObsidianExportPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,6 +322,7 @@
|
||||
"translate": "Translate",
|
||||
"topics.export.siyuan": "Export to Siyuan Note",
|
||||
"topics.export.wait_for_title_naming": "Generating title...",
|
||||
"topics.export.obsidian_reasoning": "Include Reasoning Chain",
|
||||
"topics.export.title_naming_success": "Title generated successfully",
|
||||
"topics.export.title_naming_failed": "Failed to generate title, using default title",
|
||||
"input.translating": "Translating...",
|
||||
@ -1104,7 +1105,9 @@
|
||||
"token": "Joplin Authorization Token",
|
||||
"token_placeholder": "Joplin Authorization Token",
|
||||
"url": "Joplin Web Clipper Service URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "Include Reasoning Chain in Export",
|
||||
"export_reasoning.help": "When enabled, the exported content will include the reasoning chain (thought process) generated by the assistant."
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "When enabled, $$ will be forcibly used to mark LaTeX formulas when exporting to Markdown. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.",
|
||||
"markdown_export.force_dollar_math.title": "Force $$ for LaTeX formulas",
|
||||
@ -1113,12 +1116,14 @@
|
||||
"markdown_export.path_placeholder": "Export Path",
|
||||
"markdown_export.select": "Select",
|
||||
"markdown_export.title": "Markdown Export",
|
||||
"markdown_export.show_model_name.title": "Use Model Name on Export",
|
||||
"markdown_export.show_model_name.help": "When enabled, the topic-naming model will be used to create titles for exported messages. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.",
|
||||
"markdown_export.show_model_provider.title": "Show Model Provider",
|
||||
"markdown_export.show_model_provider.help": "Display the model provider (e.g., OpenAI, Gemini) when exporting to Markdown",
|
||||
"minute_interval_one": "{{count}} minute",
|
||||
"minute_interval_other": "{{count}} minutes",
|
||||
"notion.api_key": "Notion API Key",
|
||||
"notion.api_key_placeholder": "Enter Notion API Key",
|
||||
"notion.auto_split": "Auto split when exporting",
|
||||
"notion.auto_split_tip": "Automatically split pages when exporting long topics to Notion",
|
||||
"notion.check": {
|
||||
"button": "Check",
|
||||
"empty_api_key": "API key is not configured",
|
||||
@ -1132,10 +1137,9 @@
|
||||
"notion.help": "Notion Configuration Documentation",
|
||||
"notion.page_name_key": "Page Title Field Name",
|
||||
"notion.page_name_key_placeholder": "Enter page title field name, default is Name",
|
||||
"notion.split_size": "Split size",
|
||||
"notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90",
|
||||
"notion.split_size_placeholder": "Enter block limit per page (default 90)",
|
||||
"notion.title": "Notion Configuration",
|
||||
"notion.title": "Notion Settings",
|
||||
"notion.export_reasoning.title": "Include Reasoning Chain in Export",
|
||||
"notion.export_reasoning.help": "When enabled, exported content will include reasoning chain (thought process).",
|
||||
"title": "Data Settings",
|
||||
"webdav": {
|
||||
"autoSync": "Auto Backup",
|
||||
|
||||
@ -322,6 +322,7 @@
|
||||
"translate": "翻訳",
|
||||
"topics.export.siyuan": "思源笔记にエクスポート",
|
||||
"topics.export.wait_for_title_naming": "タイトルを生成中...",
|
||||
"topics.export.obsidian_reasoning": "思考過程を含める",
|
||||
"topics.export.title_naming_success": "タイトルの生成に成功しました",
|
||||
"topics.export.title_naming_failed": "タイトルの生成に失敗しました。デフォルトのタイトルを使用します",
|
||||
"input.translating": "翻訳中...",
|
||||
@ -1102,7 +1103,9 @@
|
||||
"token": "Joplin 認証トークン",
|
||||
"token_placeholder": "Joplin 認証トークンを入力してください",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "エクスポート時に思考過程を含める",
|
||||
"export_reasoning.help": "有効にすると、エクスポートされる内容にアシスタントが生成した思考過程(リースニングチェーン)が含まれます。"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "有効にすると、Markdownにエクスポートする際にLaTeX数式を$$で強制的にマークします。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX数式に$$を強制使用",
|
||||
@ -1111,29 +1114,32 @@
|
||||
"markdown_export.path_placeholder": "エクスポートパス",
|
||||
"markdown_export.select": "選択",
|
||||
"markdown_export.title": "Markdown エクスポート",
|
||||
"markdown_export.show_model_name.title": "エクスポート時にモデル名を使用",
|
||||
"markdown_export.show_model_name.help": "有効にすると、トピック命名モデルがエクスポートされたメッセージのタイトル作成に使用されます。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。",
|
||||
"markdown_export.show_model_provider.title": "モデルプロバイダーを表示",
|
||||
"markdown_export.show_model_provider.help": "Markdownエクスポート時にモデルプロバイダー(例:OpenAI、Geminiなど)を表示します。",
|
||||
"minute_interval_one": "{{count}} 分",
|
||||
"minute_interval_other": "{{count}} 分",
|
||||
"notion.api_key": "Notion APIキー",
|
||||
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||
"notion.auto_split": "ダイアログをエクスポートすると自動ページ分割",
|
||||
"notion.auto_split_tip": "ダイアログが長い場合、Notionに自動的にページ分割してエクスポートします",
|
||||
"notion.check": {
|
||||
"button": "確認",
|
||||
"empty_api_key": "Api_keyが設定されていません",
|
||||
"empty_database_id": "Database_idが設定されていません",
|
||||
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"success": "接続に成功しました。"
|
||||
"notion": {
|
||||
"api_key": "Notion APIキー",
|
||||
"api_key_placeholder": "Notion APIキーを入力してください",
|
||||
"check": {
|
||||
"button": "確認",
|
||||
"empty_api_key": "Api_keyが設定されていません",
|
||||
"empty_database_id": "Database_idが設定されていません",
|
||||
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"success": "接続に成功しました。"
|
||||
},
|
||||
"database_id": "Notion データベースID",
|
||||
"database_id_placeholder": "Notion データベースIDを入力してください",
|
||||
"help": "Notion 設定ドキュメント",
|
||||
"page_name_key": "ページタイトルフィールド名",
|
||||
"page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||
"title": "Notion 設定",
|
||||
"export_reasoning.title": "エクスポート時に思考チェーンを含める",
|
||||
"export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。"
|
||||
},
|
||||
"notion.database_id": "Notion データベースID",
|
||||
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
|
||||
"notion.help": "Notion 設定ドキュメント",
|
||||
"notion.page_name_key": "ページタイトルフィールド名",
|
||||
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||
"notion.split_size": "自動ページ分割サイズ",
|
||||
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
|
||||
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"title": "データ設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動バックアップ",
|
||||
|
||||
@ -322,6 +322,7 @@
|
||||
"translate": "Перевести",
|
||||
"topics.export.siyuan": "Экспорт в Siyuan Note",
|
||||
"topics.export.wait_for_title_naming": "Создание заголовка...",
|
||||
"topics.export.obsidian_reasoning": "Включить цепочку рассуждений",
|
||||
"topics.export.title_naming_success": "Заголовок успешно создан",
|
||||
"topics.export.title_naming_failed": "Не удалось создать заголовок, используется заголовок по умолчанию",
|
||||
"input.translating": "Перевод...",
|
||||
@ -1102,7 +1103,9 @@
|
||||
"token": "Токен Joplin",
|
||||
"token_placeholder": "Введите токен Joplin",
|
||||
"url": "URL Joplin",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "Включить цепочку рассуждений при экспорте",
|
||||
"export_reasoning.help": "Если включено, экспортируемый контент будет содержать цепочку рассуждений, сгенерированную ассистентом."
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "Если включено, при экспорте в Markdown для обозначения формул LaTeX будет принудительно использоваться $$. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.",
|
||||
"markdown_export.force_dollar_math.title": "Принудительно использовать $$ для формул LaTeX",
|
||||
@ -1111,12 +1114,14 @@
|
||||
"markdown_export.path_placeholder": "Путь экспорта",
|
||||
"markdown_export.select": "Выбрать",
|
||||
"markdown_export.title": "Экспорт в Markdown",
|
||||
"markdown_export.show_model_name.title": "Использовать имя модели при экспорте",
|
||||
"markdown_export.show_model_name.help": "Если включено, для создания заголовков экспортируемых сообщений будет использоваться модель именования темы. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.",
|
||||
"markdown_export.show_model_provider.title": "Показать поставщика модели",
|
||||
"markdown_export.show_model_provider.help": "Показывать поставщика модели (например, OpenAI, Gemini) при экспорте в Markdown",
|
||||
"minute_interval_one": "{{count}} минута",
|
||||
"minute_interval_other": "{{count}} минут",
|
||||
"notion.api_key": "Ключ API Notion",
|
||||
"notion.api_key_placeholder": "Введите ключ API Notion",
|
||||
"notion.auto_split": "Автоматическое разбиение на страницы при экспорте диалога",
|
||||
"notion.auto_split_tip": "Автоматическое разбиение на страницы при экспорте в Notion, если тема слишком длинная",
|
||||
"notion.check": {
|
||||
"button": "Проверить",
|
||||
"empty_api_key": "Не настроен API key",
|
||||
@ -1130,10 +1135,9 @@
|
||||
"notion.help": "Документация по настройке Notion",
|
||||
"notion.page_name_key": "Название поля заголовка страницы",
|
||||
"notion.page_name_key_placeholder": "Введите название поля заголовка страницы, по умолчанию Name",
|
||||
"notion.split_size": "Размер автоматического разбиения",
|
||||
"notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90",
|
||||
"notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)",
|
||||
"notion.title": "Настройки Notion",
|
||||
"notion.export_reasoning.title": "Включить цепочку рассуждений при экспорте",
|
||||
"notion.export_reasoning.help": "При включении, содержимое цепочки рассуждений будет включено при экспорте в Notion.",
|
||||
"title": "Настройки данных",
|
||||
"webdav": {
|
||||
"autoSync": "Автоматическое резервное копирование",
|
||||
|
||||
@ -325,6 +325,7 @@
|
||||
"topics.export.obsidian_no_vault_selected": "请先选择一个保管库",
|
||||
"topics.export.obsidian_select_vault_first": "请先选择保管库",
|
||||
"topics.export.obsidian_root_directory": "根目录",
|
||||
"topics.export.obsidian_reasoning": "导出思维链",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"topics.export.yuque": "导出到语雀",
|
||||
@ -654,7 +655,7 @@
|
||||
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
|
||||
"group.delete.title": "删除分组消息",
|
||||
"ignore.knowledge.base": "联网模式开启,忽略知识库",
|
||||
"info.notion.block_reach_limit": "对话过长,正在分页导出到Notion",
|
||||
"info.notion.block_reach_limit": "对话过长,正在分段导出到Notion",
|
||||
"loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "正在准备导出到Notion...",
|
||||
"mention.title": "切换模型回答",
|
||||
@ -1104,7 +1105,9 @@
|
||||
"token": "Joplin 授权令牌",
|
||||
"token_placeholder": "请输入 Joplin 授权令牌",
|
||||
"url": "Joplin 剪裁服务监听 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "导出时包含思维链",
|
||||
"export_reasoning.help": "开启后,导出到Joplin时会包含思维链内容。"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等",
|
||||
"markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式",
|
||||
@ -1113,14 +1116,16 @@
|
||||
"markdown_export.path_placeholder": "导出路径",
|
||||
"markdown_export.select": "选择",
|
||||
"markdown_export.title": "Markdown 导出",
|
||||
"markdown_export.show_model_name.title": "导出时使用模型名称",
|
||||
"markdown_export.show_model_name.help": "开启后,使用话题命名模型为导出的消息创建标题。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。",
|
||||
"markdown_export.show_model_provider.title": "显示模型供应商",
|
||||
"markdown_export.show_model_provider.help": "在导出Markdown时显示模型供应商,如OpenAI、Gemini等",
|
||||
"message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题",
|
||||
"message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式",
|
||||
"minute_interval_one": "{{count}} 分钟",
|
||||
"minute_interval_other": "{{count}} 分钟",
|
||||
"notion.api_key": "Notion 密钥",
|
||||
"notion.api_key_placeholder": "请输入Notion 密钥",
|
||||
"notion.auto_split": "导出对话时自动分页",
|
||||
"notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion",
|
||||
"notion.check": {
|
||||
"button": "检测",
|
||||
"empty_api_key": "未配置 API key",
|
||||
@ -1134,10 +1139,9 @@
|
||||
"notion.help": "Notion 配置文档",
|
||||
"notion.page_name_key": "页面标题字段名",
|
||||
"notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name",
|
||||
"notion.split_size": "自动分页大小",
|
||||
"notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90",
|
||||
"notion.split_size_placeholder": "请输入每页块数限制(默认90)",
|
||||
"notion.title": "Notion 配置",
|
||||
"notion.title": "Notion 设置",
|
||||
"notion.export_reasoning.title": "导出时包含思维链",
|
||||
"notion.export_reasoning.help": "开启后,导出到Notion时会包含思维链内容。",
|
||||
"title": "数据设置",
|
||||
"webdav": {
|
||||
"autoSync": "自动备份",
|
||||
|
||||
@ -322,6 +322,7 @@
|
||||
"translate": "翻譯",
|
||||
"topics.export.siyuan": "匯出到思源筆記",
|
||||
"topics.export.wait_for_title_naming": "正在生成標題...",
|
||||
"topics.export.obsidian_reasoning": "包含思維鏈",
|
||||
"topics.export.title_naming_success": "標題生成成功",
|
||||
"topics.export.title_naming_failed": "標題生成失敗,使用預設標題",
|
||||
"input.translating": "翻譯中...",
|
||||
@ -1104,7 +1105,9 @@
|
||||
"token": "Joplin 授權Token",
|
||||
"token_placeholder": "請輸入 Joplin 授權Token",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "匯出時包含思維鏈",
|
||||
"export_reasoning.help": "啟用後,匯出內容將包含助手生成的思維鏈(思考過程)。"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$",
|
||||
@ -1113,12 +1116,14 @@
|
||||
"markdown_export.path_placeholder": "匯出路徑",
|
||||
"markdown_export.select": "選擇",
|
||||
"markdown_export.title": "Markdown 匯出",
|
||||
"markdown_export.show_model_name.title": "匯出時使用模型名稱",
|
||||
"markdown_export.show_model_name.help": "啟用後,將以主題命名模型為匯出的訊息建立標題。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等。",
|
||||
"markdown_export.show_model_provider.title": "顯示模型供應商",
|
||||
"markdown_export.show_model_provider.help": "在匯出Markdown時顯示模型供應商,如OpenAI、Gemini等",
|
||||
"minute_interval_one": "{{count}} 分鐘",
|
||||
"minute_interval_other": "{{count}} 分鐘",
|
||||
"notion.api_key": "Notion 金鑰",
|
||||
"notion.api_key_placeholder": "請輸入 Notion 金鑰",
|
||||
"notion.auto_split": "匯出對話時自動分頁",
|
||||
"notion.auto_split_tip": "當要匯出的話題過長時自動分頁匯出到 Notion",
|
||||
"notion.check": {
|
||||
"button": "檢查",
|
||||
"empty_api_key": "未設定 API key",
|
||||
@ -1132,10 +1137,9 @@
|
||||
"notion.help": "Notion 設定文件",
|
||||
"notion.page_name_key": "頁面標題欄位名稱",
|
||||
"notion.page_name_key_placeholder": "請輸入頁面標題欄位名稱,預設為 Name",
|
||||
"notion.split_size": "自動分頁大小",
|
||||
"notion.split_size_help": "Notion 免費版使用者建議設定為 90,進階版使用者建議設定為 24990,預設值為 90",
|
||||
"notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"notion.export_reasoning.title": "匯出時包含思維鏈",
|
||||
"notion.export_reasoning.help": "啟用後,匯出到Notion時會包含思維鏈內容。",
|
||||
"title": "資料設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動備份",
|
||||
|
||||
@ -16,10 +16,10 @@ import type { Message } from '@renderer/types/newMessage'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
|
||||
import {
|
||||
exportMarkdownToJoplin,
|
||||
exportMarkdownToNotion,
|
||||
exportMarkdownToSiyuan,
|
||||
exportMarkdownToYuque,
|
||||
exportMessageAsMarkdown,
|
||||
exportMessageToNotion,
|
||||
messageToMarkdown
|
||||
} from '@renderer/utils/export'
|
||||
// import { withMessageThought } from '@renderer/utils/formats'
|
||||
@ -244,7 +244,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
onClick: async () => {
|
||||
const title = await getMessageTitle(message)
|
||||
const markdown = messageToMarkdown(message)
|
||||
exportMarkdownToNotion(title, markdown)
|
||||
exportMessageToNotion(title, markdown, message)
|
||||
}
|
||||
},
|
||||
exportMenuOptions.yuque && {
|
||||
@ -260,9 +260,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
label: t('chat.topics.export.obsidian'),
|
||||
key: 'obsidian',
|
||||
onClick: async () => {
|
||||
const markdown = messageToMarkdown(message)
|
||||
const title = topic.name?.replace(/\//g, '_') || 'Untitled'
|
||||
await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' })
|
||||
await ObsidianExportPopup.show({ title, message, processingMethod: '1' })
|
||||
}
|
||||
},
|
||||
exportMenuOptions.joplin && {
|
||||
@ -270,8 +269,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
key: 'joplin',
|
||||
onClick: async () => {
|
||||
const title = await getMessageTitle(message)
|
||||
const markdown = messageToMarkdown(message)
|
||||
exportMarkdownToJoplin(title, markdown)
|
||||
exportMarkdownToJoplin(title, message)
|
||||
}
|
||||
},
|
||||
exportMenuOptions.siyuan && {
|
||||
|
||||
@ -312,16 +312,15 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
label: t('chat.topics.export.obsidian'),
|
||||
key: 'obsidian',
|
||||
onClick: async () => {
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' })
|
||||
await ObsidianExportPopup.show({ title: topic.name, topic, processingMethod: '3' })
|
||||
}
|
||||
},
|
||||
exportMenuOptions.joplin && {
|
||||
label: t('chat.topics.export.joplin'),
|
||||
key: 'joplin',
|
||||
onClick: async () => {
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
exportMarkdownToJoplin(topic.name, markdown)
|
||||
const topicMessages = await TopicManager.getTopicMessages(topic.id)
|
||||
exportMarkdownToJoplin(topic.name, topicMessages)
|
||||
}
|
||||
},
|
||||
exportMenuOptions.siyuan && {
|
||||
|
||||
@ -3,14 +3,14 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { setJoplinExportReasoning, setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
|
||||
import { Button, Switch, Tooltip } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const JoplinSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -20,6 +20,7 @@ const JoplinSettings: FC = () => {
|
||||
|
||||
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
|
||||
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
|
||||
const joplinExportReasoning = useSelector((state: RootState) => state.settings.joplinExportReasoning)
|
||||
|
||||
const handleJoplinTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setJoplinToken(e.target.value))
|
||||
@ -72,6 +73,10 @@ const JoplinSettings: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggleJoplinExportReasoning = (checked: boolean) => {
|
||||
dispatch(setJoplinExportReasoning(checked))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.joplin.title')}</SettingTitle>
|
||||
@ -111,6 +116,14 @@ const JoplinSettings: FC = () => {
|
||||
<Button onClick={handleJoplinConnectionCheck}>{t('settings.data.joplin.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.joplin.export_reasoning.title')}</SettingRowTitle>
|
||||
<Switch checked={joplinExportReasoning} onChange={handleToggleJoplinExportReasoning} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.joplin.export_reasoning.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setForceDollarMathInMarkdown,
|
||||
setmarkdownExportPath,
|
||||
setShowModelNameInMarkdown,
|
||||
setShowModelProviderInMarkdown,
|
||||
setUseTopicNamingForMessageTitle
|
||||
} from '@renderer/store/settings'
|
||||
import { Button, Switch } from 'antd'
|
||||
@ -23,6 +25,8 @@ const MarkdownExportSettings: FC = () => {
|
||||
const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath)
|
||||
const forceDollarMathInMarkdown = useSelector((state: RootState) => state.settings.forceDollarMathInMarkdown)
|
||||
const useTopicNamingForMessageTitle = useSelector((state: RootState) => state.settings.useTopicNamingForMessageTitle)
|
||||
const showModelNameInExport = useSelector((state: RootState) => state.settings.showModelNameInMarkdown)
|
||||
const showModelProviderInMarkdown = useSelector((state: RootState) => state.settings.showModelProviderInMarkdown)
|
||||
|
||||
const handleSelectFolder = async () => {
|
||||
const path = await window.api.file.selectFolder()
|
||||
@ -43,6 +47,14 @@ const MarkdownExportSettings: FC = () => {
|
||||
dispatch(setUseTopicNamingForMessageTitle(checked))
|
||||
}
|
||||
|
||||
const handleToggleShowModelName = (checked: boolean) => {
|
||||
dispatch(setShowModelNameInMarkdown(checked))
|
||||
}
|
||||
|
||||
const handleToggleShowModelProvider = (checked: boolean) => {
|
||||
dispatch(setShowModelProviderInMarkdown(checked))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.markdown_export.title')}</SettingTitle>
|
||||
@ -86,6 +98,22 @@ const MarkdownExportSettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.message_title.use_topic_naming.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.show_model_name.title')}</SettingRowTitle>
|
||||
<Switch checked={showModelNameInExport} onChange={handleToggleShowModelName} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.show_model_name.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.show_model_provider.title')}</SettingRowTitle>
|
||||
<Switch checked={showModelProviderInMarkdown} onChange={handleToggleShowModelProvider} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.show_model_provider.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,12 +6,11 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setNotionApiKey,
|
||||
setNotionAutoSplit,
|
||||
setNotionDatabaseID,
|
||||
setNotionPageNameKey,
|
||||
setNotionSplitSize
|
||||
setNotionExportReasoning,
|
||||
setNotionPageNameKey
|
||||
} from '@renderer/store/settings'
|
||||
import { Button, InputNumber, Switch, Tooltip } from 'antd'
|
||||
import { Button, Switch, Tooltip } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -27,8 +26,7 @@ const NotionSettings: FC = () => {
|
||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||
const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey)
|
||||
const notionAutoSplit = useSelector((state: RootState) => state.settings.notionAutoSplit)
|
||||
const notionSplitSize = useSelector((state: RootState) => state.settings.notionSplitSize)
|
||||
const notionExportReasoning = useSelector((state: RootState) => state.settings.notionExportReasoning)
|
||||
|
||||
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionApiKey(e.target.value))
|
||||
@ -76,14 +74,8 @@ const NotionSettings: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotionAutoSplitChange = (checked: boolean) => {
|
||||
dispatch(setNotionAutoSplit(checked))
|
||||
}
|
||||
|
||||
const handleNotionSplitSizeChange = (value: number | null) => {
|
||||
if (value !== null) {
|
||||
dispatch(setNotionSplitSize(value))
|
||||
}
|
||||
const handleNotionExportReasoningChange = (checked: boolean) => {
|
||||
dispatch(setNotionExportReasoning(checked))
|
||||
}
|
||||
|
||||
return (
|
||||
@ -140,38 +132,14 @@ const NotionSettings: FC = () => {
|
||||
<Button onClick={handleNotionConnectionCheck}>{t('settings.data.notion.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider /> {/* 添加分割线 */}
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Tooltip title={t('settings.data.notion.auto_split_tip')} placement="right">
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{t('settings.data.notion.auto_split')}
|
||||
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={notionAutoSplit} onChange={handleNotionAutoSplitChange} />
|
||||
<SettingRowTitle>{t('settings.data.notion.export_reasoning.title')}</SettingRowTitle>
|
||||
<Switch checked={notionExportReasoning} onChange={handleNotionExportReasoningChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.notion.export_reasoning.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
{notionAutoSplit && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.split_size')}</SettingRowTitle>
|
||||
<InputNumber
|
||||
min={30}
|
||||
max={25000}
|
||||
value={notionSplitSize}
|
||||
onChange={handleNotionSplitSizeChange}
|
||||
keyboard={true}
|
||||
controls={true}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText style={{ marginLeft: 10 }}>{t('settings.data.notion.split_size_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@ -129,14 +129,16 @@ export interface SettingsState {
|
||||
markdownExportPath: string | null
|
||||
forceDollarMathInMarkdown: boolean
|
||||
useTopicNamingForMessageTitle: boolean
|
||||
showModelNameInMarkdown: boolean
|
||||
showModelProviderInMarkdown: boolean
|
||||
thoughtAutoCollapse: boolean
|
||||
notionAutoSplit: boolean
|
||||
notionSplitSize: number
|
||||
notionExportReasoning: boolean
|
||||
yuqueToken: string | null
|
||||
yuqueUrl: string | null
|
||||
yuqueRepoId: string | null
|
||||
joplinToken: string | null
|
||||
joplinUrl: string | null
|
||||
joplinExportReasoning: boolean
|
||||
defaultObsidianVault: string | null
|
||||
defaultAgent: string | null
|
||||
// 思源笔记配置
|
||||
@ -271,14 +273,16 @@ export const initialState: SettingsState = {
|
||||
markdownExportPath: null,
|
||||
forceDollarMathInMarkdown: false,
|
||||
useTopicNamingForMessageTitle: false,
|
||||
showModelNameInMarkdown: false,
|
||||
showModelProviderInMarkdown: false,
|
||||
thoughtAutoCollapse: true,
|
||||
notionAutoSplit: false,
|
||||
notionSplitSize: 90,
|
||||
notionExportReasoning: false,
|
||||
yuqueToken: '',
|
||||
yuqueUrl: '',
|
||||
yuqueRepoId: '',
|
||||
joplinToken: '',
|
||||
joplinUrl: '',
|
||||
joplinExportReasoning: false,
|
||||
defaultObsidianVault: null,
|
||||
defaultAgent: null,
|
||||
siyuanApiUrl: null,
|
||||
@ -580,14 +584,17 @@ const settingsSlice = createSlice({
|
||||
setUseTopicNamingForMessageTitle: (state, action: PayloadAction<boolean>) => {
|
||||
state.useTopicNamingForMessageTitle = action.payload
|
||||
},
|
||||
setShowModelNameInMarkdown: (state, action: PayloadAction<boolean>) => {
|
||||
state.showModelNameInMarkdown = action.payload
|
||||
},
|
||||
setShowModelProviderInMarkdown: (state, action: PayloadAction<boolean>) => {
|
||||
state.showModelProviderInMarkdown = action.payload
|
||||
},
|
||||
setThoughtAutoCollapse: (state, action: PayloadAction<boolean>) => {
|
||||
state.thoughtAutoCollapse = action.payload
|
||||
},
|
||||
setNotionAutoSplit: (state, action: PayloadAction<boolean>) => {
|
||||
state.notionAutoSplit = action.payload
|
||||
},
|
||||
setNotionSplitSize: (state, action: PayloadAction<number>) => {
|
||||
state.notionSplitSize = action.payload
|
||||
setNotionExportReasoning: (state, action: PayloadAction<boolean>) => {
|
||||
state.notionExportReasoning = action.payload
|
||||
},
|
||||
setYuqueToken: (state, action: PayloadAction<string>) => {
|
||||
state.yuqueToken = action.payload
|
||||
@ -604,6 +611,9 @@ const settingsSlice = createSlice({
|
||||
setJoplinUrl: (state, action: PayloadAction<string>) => {
|
||||
state.joplinUrl = action.payload
|
||||
},
|
||||
setJoplinExportReasoning: (state, action: PayloadAction<boolean>) => {
|
||||
state.joplinExportReasoning = action.payload
|
||||
},
|
||||
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
|
||||
state.messageNavigation = action.payload
|
||||
},
|
||||
@ -665,6 +675,8 @@ const settingsSlice = createSlice({
|
||||
})
|
||||
|
||||
export const {
|
||||
setShowModelNameInMarkdown,
|
||||
setShowModelProviderInMarkdown,
|
||||
setShowAssistants,
|
||||
toggleShowAssistants,
|
||||
setShowTopics,
|
||||
@ -737,13 +749,13 @@ export const {
|
||||
setForceDollarMathInMarkdown,
|
||||
setUseTopicNamingForMessageTitle,
|
||||
setThoughtAutoCollapse,
|
||||
setNotionAutoSplit,
|
||||
setNotionSplitSize,
|
||||
setNotionExportReasoning,
|
||||
setYuqueToken,
|
||||
setYuqueRepoId,
|
||||
setYuqueUrl,
|
||||
setJoplinToken,
|
||||
setJoplinUrl,
|
||||
setJoplinExportReasoning,
|
||||
setMessageNavigation,
|
||||
setDefaultObsidianVault,
|
||||
setDefaultAgent,
|
||||
|
||||
@ -11,7 +11,6 @@ import { convertMathFormula } from '@renderer/utils/markdown'
|
||||
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
|
||||
import { markdownToBlocks } from '@tryfabric/martian'
|
||||
import dayjs from 'dayjs'
|
||||
//TODO: 添加对思考内容的支持
|
||||
|
||||
/**
|
||||
* 从消息内容中提取标题,限制长度并处理换行和标点符号。用于导出功能。
|
||||
@ -43,9 +42,35 @@ export function getTitleFromString(str: string, length: number = 80) {
|
||||
return title
|
||||
}
|
||||
|
||||
const getRoleText = (role: string, modelName?: string, modelProvider?: string) => {
|
||||
const { showModelNameInMarkdown, showModelProviderInMarkdown } = store.getState().settings
|
||||
|
||||
if (role === 'user') {
|
||||
return '🧑💻 User'
|
||||
} else if (role === 'system') {
|
||||
return '🤖 System'
|
||||
} else {
|
||||
let assistantText = '🤖 '
|
||||
if (showModelNameInMarkdown && modelName) {
|
||||
assistantText += `${modelName}`
|
||||
if (showModelProviderInMarkdown && modelProvider) {
|
||||
const providerDisplayName = i18n.t(`provider.${modelProvider}`, { defaultValue: modelProvider })
|
||||
assistantText += ` | ${providerDisplayName}`
|
||||
return assistantText
|
||||
}
|
||||
return assistantText
|
||||
} else if (showModelProviderInMarkdown && modelProvider) {
|
||||
const providerDisplayName = i18n.t(`provider.${modelProvider}`, { defaultValue: modelProvider })
|
||||
assistantText += `Assistant | ${providerDisplayName}`
|
||||
return assistantText
|
||||
}
|
||||
return assistantText + 'Assistant'
|
||||
}
|
||||
}
|
||||
|
||||
const createBaseMarkdown = (message: Message, includeReasoning: boolean = false) => {
|
||||
const { forceDollarMathInMarkdown } = store.getState().settings
|
||||
const roleText = message.role === 'user' ? '🧑💻 User' : '🤖 Assistant'
|
||||
const roleText = getRoleText(message.role, message.model?.name, message.model?.provider)
|
||||
const titleSection = `### ${roleText}`
|
||||
let reasoningSection = ''
|
||||
|
||||
@ -57,15 +82,22 @@ const createBaseMarkdown = (message: Message, includeReasoning: boolean = false)
|
||||
} else if (reasoningContent.startsWith('<think>')) {
|
||||
reasoningContent = reasoningContent.substring(7)
|
||||
}
|
||||
reasoningContent = reasoningContent.replace(/\n/g, '<br>')
|
||||
|
||||
reasoningContent = reasoningContent
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n/g, '<br>')
|
||||
if (forceDollarMathInMarkdown) {
|
||||
reasoningContent = convertMathFormula(reasoningContent)
|
||||
}
|
||||
reasoningSection = `<details style="background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<summary>${i18n.t('common.reasoning_content')}</summary><hr>
|
||||
reasoningSection = `<div style="border: 2px solid #dddddd; border-radius: 10px;">
|
||||
<details style="padding: 5px;">
|
||||
<summary>${i18n.t('common.reasoning_content')}</summary>
|
||||
${reasoningContent}
|
||||
</details>`
|
||||
</details>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +113,6 @@ export const messageToMarkdown = (message: Message) => {
|
||||
return [titleSection, '', contentSection, citation].join('\n\n')
|
||||
}
|
||||
|
||||
// 保留接口用于其它导出方法使用
|
||||
export const messageToMarkdownWithReasoning = (message: Message) => {
|
||||
const { titleSection, reasoningSection, contentSection, citation } = createBaseMarkdown(message, true)
|
||||
return [titleSection, '', reasoningSection + contentSection, citation].join('\n\n')
|
||||
@ -167,14 +198,10 @@ export const exportMessageAsMarkdown = async (message: Message, exportReasoning?
|
||||
const convertMarkdownToNotionBlocks = async (markdown: string) => {
|
||||
return markdownToBlocks(markdown)
|
||||
}
|
||||
// 修改 splitNotionBlocks 函数
|
||||
const splitNotionBlocks = (blocks: any[]) => {
|
||||
const { notionAutoSplit, notionSplitSize } = store.getState().settings
|
||||
|
||||
// 如果未开启自动分页,返回单页
|
||||
if (!notionAutoSplit) {
|
||||
return [blocks]
|
||||
}
|
||||
const splitNotionBlocks = (blocks: any[]) => {
|
||||
// Notion API限制单次传输100块
|
||||
const notionSplitSize = 95
|
||||
|
||||
const pages: any[][] = []
|
||||
let currentPage: any[] = []
|
||||
@ -195,25 +222,68 @@ const splitNotionBlocks = (blocks: any[]) => {
|
||||
return pages
|
||||
}
|
||||
|
||||
export const exportTopicToNotion = async (topic: Topic) => {
|
||||
const convertThinkingToNotionBlocks = async (thinkingContent: string): Promise<any[]> => {
|
||||
if (!thinkingContent.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
const thinkingBlocks = [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'toggle',
|
||||
toggle: {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: '🤔 ' + i18n.t('common.reasoning_content')
|
||||
},
|
||||
annotations: {
|
||||
bold: true
|
||||
}
|
||||
}
|
||||
],
|
||||
children: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: thinkingContent
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return thinkingBlocks
|
||||
}
|
||||
|
||||
const executeNotionExport = async (title: string, allBlocks: any[]): Promise<any> => {
|
||||
const { isExporting } = store.getState().runtime.export
|
||||
if (isExporting) {
|
||||
window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' })
|
||||
return
|
||||
return null
|
||||
}
|
||||
setExportState({
|
||||
isExporting: true
|
||||
})
|
||||
|
||||
setExportState({ isExporting: true })
|
||||
|
||||
const { notionDatabaseID, notionApiKey } = store.getState().settings
|
||||
if (!notionApiKey || !notionDatabaseID) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
|
||||
return
|
||||
setExportState({ isExporting: false })
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const notion = new Client({ auth: notionApiKey })
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
const allBlocks = await convertMarkdownToNotionBlocks(markdown)
|
||||
const blockPages = splitNotionBlocks(allBlocks)
|
||||
|
||||
if (blockPages.length === 0) {
|
||||
@ -223,25 +293,33 @@ export const exportTopicToNotion = async (topic: Topic) => {
|
||||
// 创建主页面和子页面
|
||||
let mainPageResponse: any = null
|
||||
let parentBlockId: string | null = null
|
||||
|
||||
for (let i = 0; i < blockPages.length; i++) {
|
||||
const pageTitle = topic.name
|
||||
const pageBlocks = blockPages[i]
|
||||
|
||||
// 导出进度提示
|
||||
window.message.loading({
|
||||
content: i18n.t('message.loading.notion.exporting_progress', {
|
||||
current: i + 1,
|
||||
total: blockPages.length
|
||||
}),
|
||||
key: 'notion-export-progress'
|
||||
})
|
||||
if (blockPages.length > 1) {
|
||||
window.message.loading({
|
||||
content: i18n.t('message.loading.notion.exporting_progress', {
|
||||
current: i + 1,
|
||||
total: blockPages.length
|
||||
}),
|
||||
key: 'notion-export-progress'
|
||||
})
|
||||
} else {
|
||||
window.message.loading({
|
||||
content: i18n.t('message.loading.notion.preparing'),
|
||||
key: 'notion-export-progress'
|
||||
})
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
// 创建主页面
|
||||
const response = await notion.pages.create({
|
||||
parent: { database_id: notionDatabaseID },
|
||||
properties: {
|
||||
[store.getState().settings.notionPageNameKey || 'Name']: {
|
||||
title: [{ text: { content: pageTitle } }]
|
||||
title: [{ text: { content: title } }]
|
||||
}
|
||||
},
|
||||
children: pageBlocks
|
||||
@ -249,6 +327,7 @@ export const exportTopicToNotion = async (topic: Topic) => {
|
||||
mainPageResponse = response
|
||||
parentBlockId = response.id
|
||||
} else {
|
||||
// 追加后续页面的块到主页面
|
||||
if (!parentBlockId) {
|
||||
throw new Error('Parent block ID is null')
|
||||
}
|
||||
@ -259,63 +338,71 @@ export const exportTopicToNotion = async (topic: Topic) => {
|
||||
}
|
||||
}
|
||||
|
||||
window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-export-progress' })
|
||||
const messageKey = blockPages.length > 1 ? 'notion-export-progress' : 'notion-success'
|
||||
window.message.success({ content: i18n.t('message.success.notion.export'), key: messageKey })
|
||||
return mainPageResponse
|
||||
} catch (error: any) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-export-progress' })
|
||||
return null
|
||||
} finally {
|
||||
setExportState({
|
||||
isExporting: false
|
||||
})
|
||||
setExportState({ isExporting: false })
|
||||
}
|
||||
}
|
||||
|
||||
export const exportMarkdownToNotion = async (title: string, content: string) => {
|
||||
const { isExporting } = store.getState().runtime.export
|
||||
export const exportMessageToNotion = async (title: string, content: string, message?: Message) => {
|
||||
const { notionExportReasoning } = store.getState().settings
|
||||
|
||||
if (isExporting) {
|
||||
window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' })
|
||||
return
|
||||
const notionBlocks = await convertMarkdownToNotionBlocks(content)
|
||||
|
||||
if (notionExportReasoning && message) {
|
||||
const thinkingContent = getThinkingContent(message)
|
||||
if (thinkingContent) {
|
||||
const thinkingBlocks = await convertThinkingToNotionBlocks(thinkingContent)
|
||||
if (notionBlocks.length > 0) {
|
||||
notionBlocks.splice(1, 0, ...thinkingBlocks)
|
||||
} else {
|
||||
notionBlocks.push(...thinkingBlocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setExportState({ isExporting: true })
|
||||
return executeNotionExport(title, notionBlocks)
|
||||
}
|
||||
|
||||
const { notionDatabaseID, notionApiKey } = store.getState().settings
|
||||
export const exportTopicToNotion = async (topic: Topic) => {
|
||||
const { notionExportReasoning } = store.getState().settings
|
||||
|
||||
if (!notionApiKey || !notionDatabaseID) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
|
||||
return
|
||||
}
|
||||
// 获取话题消息
|
||||
const topicRecord = await db.topics.get(topic.id)
|
||||
const topicMessages = topicRecord?.messages || []
|
||||
|
||||
try {
|
||||
const notion = new Client({ auth: notionApiKey })
|
||||
const notionBlocks = await convertMarkdownToNotionBlocks(content)
|
||||
// 创建话题标题块
|
||||
const titleBlocks = await convertMarkdownToNotionBlocks(`# ${topic.name}`)
|
||||
|
||||
if (notionBlocks.length === 0) {
|
||||
throw new Error('No content to export')
|
||||
// 为每个消息创建blocks
|
||||
const allBlocks: any[] = [...titleBlocks]
|
||||
|
||||
for (const message of topicMessages) {
|
||||
// 将单个消息转换为markdown
|
||||
const messageMarkdown = messageToMarkdown(message)
|
||||
const messageBlocks = await convertMarkdownToNotionBlocks(messageMarkdown)
|
||||
|
||||
if (notionExportReasoning) {
|
||||
const thinkingContent = getThinkingContent(message)
|
||||
if (thinkingContent) {
|
||||
const thinkingBlocks = await convertThinkingToNotionBlocks(thinkingContent)
|
||||
if (messageBlocks.length > 0) {
|
||||
messageBlocks.splice(1, 0, ...thinkingBlocks)
|
||||
} else {
|
||||
messageBlocks.push(...thinkingBlocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await notion.pages.create({
|
||||
parent: { database_id: notionDatabaseID },
|
||||
properties: {
|
||||
[store.getState().settings.notionPageNameKey || 'Name']: {
|
||||
title: [{ text: { content: title } }]
|
||||
}
|
||||
},
|
||||
children: notionBlocks as any[]
|
||||
})
|
||||
|
||||
window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' })
|
||||
return response
|
||||
} catch (error: any) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-error' })
|
||||
return null
|
||||
} finally {
|
||||
setExportState({
|
||||
isExporting: false
|
||||
})
|
||||
allBlocks.push(...messageBlocks)
|
||||
}
|
||||
|
||||
return executeNotionExport(topic.name, allBlocks)
|
||||
}
|
||||
|
||||
export const exportMarkdownToYuque = async (title: string, content: string) => {
|
||||
@ -464,7 +551,6 @@ export const exportMarkdownToObsidian = async (attributes: any) => {
|
||||
* @param fileName
|
||||
* @returns
|
||||
*/
|
||||
|
||||
function transformObsidianFileName(fileName: string): string {
|
||||
const platform = window.navigator.userAgent
|
||||
const isWindows = /win/i.test(platform)
|
||||
@ -482,7 +568,7 @@ function transformObsidianFileName(fileName: string): string {
|
||||
} else if (isMac) {
|
||||
// Mac 的清理
|
||||
sanitized = sanitized
|
||||
.replace(/[/:\u0020-\u007E]/g, '') // 移除无效字符
|
||||
.replace(/[<>:"\\/\\|?*]/g, '') // 移除无效字符
|
||||
.replace(/^\./, '_') // 避免以句点开头
|
||||
} else {
|
||||
// Linux 或其他系统
|
||||
@ -504,14 +590,27 @@ function transformObsidianFileName(fileName: string): string {
|
||||
|
||||
return sanitized
|
||||
}
|
||||
export const exportMarkdownToJoplin = async (title: string, content: string) => {
|
||||
const { joplinUrl, joplinToken } = store.getState().settings
|
||||
|
||||
export const exportMarkdownToJoplin = async (title: string, contentOrMessages: string | Message | Message[]) => {
|
||||
const { joplinUrl, joplinToken, joplinExportReasoning } = store.getState().settings
|
||||
|
||||
if (!joplinUrl || !joplinToken) {
|
||||
window.message.error(i18n.t('message.error.joplin.no_config'))
|
||||
return
|
||||
}
|
||||
|
||||
let content: string
|
||||
if (typeof contentOrMessages === 'string') {
|
||||
content = contentOrMessages
|
||||
} else if (Array.isArray(contentOrMessages)) {
|
||||
content = messagesToMarkdown(contentOrMessages, joplinExportReasoning)
|
||||
} else {
|
||||
// 单条Message
|
||||
content = joplinExportReasoning
|
||||
? messageToMarkdownWithReasoning(contentOrMessages)
|
||||
: messageToMarkdown(contentOrMessages)
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = joplinUrl.endsWith('/') ? joplinUrl : `${joplinUrl}/`
|
||||
const response = await fetch(`${baseUrl}notes?token=${joplinToken}`, {
|
||||
|
||||
@ -133,7 +133,7 @@ export const getCitationContent = (message: Message): string => {
|
||||
return citationBlocks
|
||||
.map((block) => formatCitationsFromBlock(block))
|
||||
.flat()
|
||||
.map((citation) => `[${citation.number}] [${citation.url}](${citation.title || citation.url})`)
|
||||
.map((citation) => `[${citation.number}] [${citation.title || citation.url}](${citation.url})`)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user