Merge branch 'main' into feat-knowlege-ocr
This commit is contained in:
+362
-129
@@ -4,135 +4,368 @@ export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||
export const bookExts = ['.epub']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
'.md', // Markdown 文件
|
||||
'.mdx', // Markdown 文件
|
||||
'.html', // HTML 文件
|
||||
'.htm', // HTML 文件的另一种扩展名
|
||||
'.xml', // XML 文件
|
||||
'.json', // JSON 文件
|
||||
'.yaml', // YAML 文件
|
||||
'.yml', // YAML 文件的另一种扩展名
|
||||
'.csv', // 逗号分隔值文件
|
||||
'.tsv', // 制表符分隔值文件
|
||||
'.ini', // 配置文件
|
||||
'.log', // 日志文件
|
||||
'.rtf', // 富文本格式文件
|
||||
'.org', // org-mode 文件
|
||||
'.wiki', // VimWiki 文件
|
||||
'.tex', // LaTeX 文件
|
||||
'.bib', // BibTeX 文件
|
||||
'.srt', // 字幕文件
|
||||
'.xhtml', // XHTML 文件
|
||||
'.nfo', // 信息文件(主要用于场景发布)
|
||||
'.conf', // 配置文件
|
||||
'.config', // 配置文件
|
||||
'.env', // 环境变量文件
|
||||
'.rst', // reStructuredText 文件
|
||||
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||
'.ts', // TypeScript 文件
|
||||
'.jsp', // JavaServer Pages 文件
|
||||
'.aspx', // ASP.NET 文件
|
||||
'.bat', // Windows 批处理文件
|
||||
'.sh', // Unix/Linux Shell 脚本文件
|
||||
'.py', // Python 脚本文件
|
||||
'.ipynb', // Jupyter 笔记本格式
|
||||
'.rb', // Ruby 脚本文件
|
||||
'.pl', // Perl 脚本文件
|
||||
'.sql', // SQL 脚本文件
|
||||
'.css', // Cascading Style Sheets 文件
|
||||
'.less', // Less CSS 预处理器文件
|
||||
'.scss', // Sass CSS 预处理器文件
|
||||
'.sass', // Sass 文件
|
||||
'.styl', // Stylus CSS 预处理器文件
|
||||
'.coffee', // CoffeeScript 文件
|
||||
'.ino', // Arduino 代码文件
|
||||
'.asm', // Assembly 语言文件
|
||||
'.go', // Go 语言文件
|
||||
'.scala', // Scala 语言文件
|
||||
'.swift', // Swift 语言文件
|
||||
'.kt', // Kotlin 语言文件
|
||||
'.rs', // Rust 语言文件
|
||||
'.lua', // Lua 语言文件
|
||||
'.groovy', // Groovy 语言文件
|
||||
'.dart', // Dart 语言文件
|
||||
'.hs', // Haskell 语言文件
|
||||
'.clj', // Clojure 语言文件
|
||||
'.cljs', // ClojureScript 语言文件
|
||||
'.elm', // Elm 语言文件
|
||||
'.erl', // Erlang 语言文件
|
||||
'.ex', // Elixir 语言文件
|
||||
'.exs', // Elixir 脚本文件
|
||||
'.pug', // Pug (formerly Jade) 模板文件
|
||||
'.haml', // Haml 模板文件
|
||||
'.slim', // Slim 模板文件
|
||||
'.tpl', // 模板文件(通用)
|
||||
'.ejs', // Embedded JavaScript 模板文件
|
||||
'.hbs', // Handlebars 模板文件
|
||||
'.mustache', // Mustache 模板文件
|
||||
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||
'.twig', // Twig 模板文件
|
||||
'.blade', // Blade 模板文件 (Laravel)
|
||||
'.vue', // Vue.js 单文件组件
|
||||
'.jsx', // React JSX 文件
|
||||
'.tsx', // React TSX 文件
|
||||
'.graphql', // GraphQL 查询语言文件
|
||||
'.gql', // GraphQL 查询语言文件
|
||||
'.proto', // Protocol Buffers 文件
|
||||
'.thrift', // Thrift 文件
|
||||
'.toml', // TOML 配置文件
|
||||
'.edn', // Clojure 数据表示文件
|
||||
'.cake', // CakePHP 配置文件
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 或 MATLAB 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java', // Java 代码文件
|
||||
'.cs', // C# 代码文件
|
||||
'.cpp', // C++ 代码文件
|
||||
'.c', // C++ 代码文件
|
||||
'.h', // C++ 头文件
|
||||
'.hpp', // C++ 头文件
|
||||
'.cc', // C++ 源文件
|
||||
'.cxx', // C++ 源文件
|
||||
'.cppm', // C++20 模块接口文件
|
||||
'.ipp', // 模板实现文件
|
||||
'.ixx', // C++20 模块实现文件
|
||||
'.f90', // Fortran 90 源文件
|
||||
'.f', // Fortran 固定格式源代码文件
|
||||
'.f03', // Fortran 2003+ 源代码文件
|
||||
'.ahk', // AutoHotKey 语言文件
|
||||
'.tcl', // Tcl 脚本
|
||||
'.do', // Questa 或 Modelsim Tcl 脚本
|
||||
'.v', // Verilog 源文件
|
||||
'.sv', // SystemVerilog 源文件
|
||||
'.svh', // SystemVerilog 头文件
|
||||
'.vhd', // VHDL 源文件
|
||||
'.vhdl', // VHDL 源文件
|
||||
'.lef', // Library Exchange Format
|
||||
'.def', // Design Exchange Format
|
||||
'.edif', // Electronic Design Interchange Format
|
||||
'.sdf', // Standard Delay Format
|
||||
'.sdc', // Synopsys Design Constraints
|
||||
'.xdc', // Xilinx Design Constraints
|
||||
'.rpt', // 报告文件
|
||||
'.lisp', // Lisp 脚本
|
||||
'.il', // Cadence SKILL 脚本
|
||||
'.ils', // Cadence SKILL++ 脚本
|
||||
'.sp', // SPICE netlist 文件
|
||||
'.spi', // SPICE netlist 文件
|
||||
'.cir', // SPICE netlist 文件
|
||||
'.net', // SPICE netlist 文件
|
||||
'.scs', // Spectre netlist 文件
|
||||
'.asc', // LTspice netlist schematic 文件
|
||||
'.tf' // Technology File
|
||||
]
|
||||
const textExtsByCategory = new Map([
|
||||
[
|
||||
'language',
|
||||
[
|
||||
'.js',
|
||||
'.mjs',
|
||||
'.cjs',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx', // JavaScript/TypeScript
|
||||
'.py', // Python
|
||||
'.java', // Java
|
||||
'.cs', // C#
|
||||
'.cpp',
|
||||
'.c',
|
||||
'.h',
|
||||
'.hpp',
|
||||
'.cc',
|
||||
'.cxx',
|
||||
'.cppm',
|
||||
'.ipp',
|
||||
'.ixx', // C/C++
|
||||
'.php', // PHP
|
||||
'.rb', // Ruby
|
||||
'.pl', // Perl
|
||||
'.go', // Go
|
||||
'.rs', // Rust
|
||||
'.swift', // Swift
|
||||
'.kt',
|
||||
'.kts', // Kotlin
|
||||
'.scala', // Scala
|
||||
'.lua', // Lua
|
||||
'.groovy', // Groovy
|
||||
'.dart', // Dart
|
||||
'.hs', // Haskell
|
||||
'.clj',
|
||||
'.cljs', // Clojure
|
||||
'.elm', // Elm
|
||||
'.erl', // Erlang
|
||||
'.ex',
|
||||
'.exs', // Elixir
|
||||
'.ml',
|
||||
'.mli', // OCaml
|
||||
'.fs', // F#
|
||||
'.r',
|
||||
'.R', // R
|
||||
'.sol', // Solidity
|
||||
'.awk', // AWK
|
||||
'.cob', // COBOL
|
||||
'.asm',
|
||||
'.s', // Assembly
|
||||
'.lisp',
|
||||
'.lsp', // Lisp
|
||||
'.coffee', // CoffeeScript
|
||||
'.ino', // Arduino
|
||||
'.jl', // Julia
|
||||
'.nim', // Nim
|
||||
'.zig', // Zig
|
||||
'.d', // D语言
|
||||
'.pas', // Pascal
|
||||
'.vb', // Visual Basic
|
||||
'.rkt', // Racket
|
||||
'.scm', // Scheme
|
||||
'.hx', // Haxe
|
||||
'.as', // ActionScript
|
||||
'.pde', // Processing
|
||||
'.f90',
|
||||
'.f',
|
||||
'.f03',
|
||||
'.for',
|
||||
'.f95', // Fortran
|
||||
'.adb',
|
||||
'.ads', // Ada
|
||||
'.pro', // Prolog
|
||||
'.m',
|
||||
'.mm', // Objective-C/MATLAB
|
||||
'.rpy', // Ren'Py
|
||||
'.ets', // OpenHarmony,
|
||||
'.uniswap', // DeFi
|
||||
'.vy', // Vyper
|
||||
'.shader',
|
||||
'.glsl',
|
||||
'.frag',
|
||||
'.vert',
|
||||
'.gd' // Godot
|
||||
]
|
||||
],
|
||||
[
|
||||
'script',
|
||||
[
|
||||
'.sh', // Shell
|
||||
'.bat',
|
||||
'.cmd', // Windows批处理
|
||||
'.ps1', // PowerShell
|
||||
'.tcl',
|
||||
'.do', // Tcl
|
||||
'.ahk', // AutoHotkey
|
||||
'.zsh', // Zsh
|
||||
'.fish', // Fish shell
|
||||
'.csh', // C shell
|
||||
'.vbs', // VBScript
|
||||
'.applescript', // AppleScript
|
||||
'.au3', // AutoIt
|
||||
'.bash',
|
||||
'.nu'
|
||||
]
|
||||
],
|
||||
[
|
||||
'style',
|
||||
[
|
||||
'.css', // CSS
|
||||
'.less', // Less
|
||||
'.scss',
|
||||
'.sass', // Sass
|
||||
'.styl', // Stylus
|
||||
'.pcss', // PostCSS
|
||||
'.postcss' // PostCSS
|
||||
]
|
||||
],
|
||||
[
|
||||
'template',
|
||||
[
|
||||
'.vue', // Vue.js
|
||||
'.pug',
|
||||
'.jade', // Pug/Jade
|
||||
'.haml', // Haml
|
||||
'.slim', // Slim
|
||||
'.tpl', // 通用模板
|
||||
'.ejs', // EJS
|
||||
'.hbs', // Handlebars
|
||||
'.mustache', // Mustache
|
||||
'.twig', // Twig
|
||||
'.blade', // Blade (Laravel)
|
||||
'.liquid', // Liquid
|
||||
'.jinja',
|
||||
'.jinja2',
|
||||
'.j2', // Jinja
|
||||
'.erb', // ERB
|
||||
'.vm', // Velocity
|
||||
'.ftl', // FreeMarker
|
||||
'.svelte', // Svelte
|
||||
'.astro' // Astro
|
||||
]
|
||||
],
|
||||
[
|
||||
'config',
|
||||
[
|
||||
'.ini', // INI配置
|
||||
'.conf',
|
||||
'.config', // 通用配置
|
||||
'.env', // 环境变量
|
||||
'.toml', // TOML
|
||||
'.cfg', // 通用配置
|
||||
'.properties', // Java属性
|
||||
'.desktop', // Linux桌面文件
|
||||
'.service', // systemd服务
|
||||
'.rc',
|
||||
'.bashrc',
|
||||
'.zshrc', // Shell配置
|
||||
'.fishrc', // Fish shell配置
|
||||
'.vimrc', // Vim配置
|
||||
'.htaccess', // Apache配置
|
||||
'.robots', // robots.txt
|
||||
'.editorconfig', // EditorConfig
|
||||
'.eslintrc', // ESLint
|
||||
'.prettierrc', // Prettier
|
||||
'.babelrc', // Babel
|
||||
'.npmrc', // npm
|
||||
'.dockerignore', // Docker ignore
|
||||
'.npmignore',
|
||||
'.yarnrc',
|
||||
'.prettierignore',
|
||||
'.eslintignore',
|
||||
'.browserslistrc',
|
||||
'.json5',
|
||||
'.tfvars'
|
||||
]
|
||||
],
|
||||
[
|
||||
'document',
|
||||
[
|
||||
'.txt',
|
||||
'.text', // 纯文本
|
||||
'.md',
|
||||
'.mdx', // Markdown
|
||||
'.html',
|
||||
'.htm',
|
||||
'.xhtml', // HTML
|
||||
'.xml', // XML
|
||||
'.org', // Org-mode
|
||||
'.wiki', // Wiki
|
||||
'.tex',
|
||||
'.bib', // LaTeX
|
||||
'.rst', // reStructuredText
|
||||
'.rtf', // 富文本
|
||||
'.nfo', // 信息文件
|
||||
'.adoc',
|
||||
'.asciidoc', // AsciiDoc
|
||||
'.pod', // Perl文档
|
||||
'.1',
|
||||
'.2',
|
||||
'.3',
|
||||
'.4',
|
||||
'.5',
|
||||
'.6',
|
||||
'.7',
|
||||
'.8',
|
||||
'.9', // man页面
|
||||
'.man', // man页面
|
||||
'.texi',
|
||||
'.texinfo', // Texinfo
|
||||
'.readme',
|
||||
'.me', // README
|
||||
'.changelog', // 变更日志
|
||||
'.license', // 许可证
|
||||
'.authors', // 作者文件
|
||||
'.po',
|
||||
'.pot'
|
||||
]
|
||||
],
|
||||
[
|
||||
'data',
|
||||
[
|
||||
'.json', // JSON
|
||||
'.jsonc', // JSON with comments
|
||||
'.yaml',
|
||||
'.yml', // YAML
|
||||
'.csv',
|
||||
'.tsv', // 分隔值文件
|
||||
'.edn', // Clojure数据
|
||||
'.jsonl',
|
||||
'.ndjson', // 换行分隔JSON
|
||||
'.geojson', // GeoJSON
|
||||
'.gpx', // GPS Exchange
|
||||
'.kml', // Keyhole Markup
|
||||
'.rss',
|
||||
'.atom', // Feed格式
|
||||
'.vcf', // vCard
|
||||
'.ics', // iCalendar
|
||||
'.ldif', // LDAP数据交换
|
||||
'.pbtxt',
|
||||
'.map'
|
||||
]
|
||||
],
|
||||
[
|
||||
'build',
|
||||
[
|
||||
'.gradle', // Gradle
|
||||
'.make',
|
||||
'.mk', // Make
|
||||
'.cmake', // CMake
|
||||
'.sbt', // SBT
|
||||
'.rake', // Rake
|
||||
'.spec', // RPM spec
|
||||
'.pom',
|
||||
'.build', // Meson
|
||||
'.bazel' // Bazel
|
||||
]
|
||||
],
|
||||
[
|
||||
'database',
|
||||
[
|
||||
'.sql', // SQL
|
||||
'.ddl',
|
||||
'.dml', // DDL/DML
|
||||
'.plsql', // PL/SQL
|
||||
'.psql', // PostgreSQL
|
||||
'.cypher', // Cypher
|
||||
'.sparql' // SPARQL
|
||||
]
|
||||
],
|
||||
[
|
||||
'web',
|
||||
[
|
||||
'.graphql',
|
||||
'.gql', // GraphQL
|
||||
'.proto', // Protocol Buffers
|
||||
'.thrift', // Thrift
|
||||
'.wsdl', // WSDL
|
||||
'.raml', // RAML
|
||||
'.swagger',
|
||||
'.openapi' // API文档
|
||||
]
|
||||
],
|
||||
[
|
||||
'version',
|
||||
[
|
||||
'.gitignore', // Git ignore
|
||||
'.gitattributes', // Git attributes
|
||||
'.gitconfig', // Git config
|
||||
'.hgignore', // Mercurial ignore
|
||||
'.bzrignore', // Bazaar ignore
|
||||
'.svnignore', // SVN ignore
|
||||
'.githistory' // Git history
|
||||
]
|
||||
],
|
||||
[
|
||||
'subtitle',
|
||||
[
|
||||
'.srt',
|
||||
'.sub',
|
||||
'.ass' // 字幕格式
|
||||
]
|
||||
],
|
||||
[
|
||||
'log',
|
||||
[
|
||||
'.log',
|
||||
'.rpt' // 日志和报告 (移除了.out,因为通常是二进制可执行文件)
|
||||
]
|
||||
],
|
||||
[
|
||||
'eda',
|
||||
[
|
||||
'.v',
|
||||
'.sv',
|
||||
'.svh', // Verilog/SystemVerilog
|
||||
'.vhd',
|
||||
'.vhdl', // VHDL
|
||||
'.lef',
|
||||
'.def', // LEF/DEF
|
||||
'.edif', // EDIF
|
||||
'.sdf', // SDF
|
||||
'.sdc',
|
||||
'.xdc', // 约束文件
|
||||
'.sp',
|
||||
'.spi',
|
||||
'.cir',
|
||||
'.net', // SPICE
|
||||
'.scs', // Spectre
|
||||
'.asc', // LTspice
|
||||
'.tf', // Technology File
|
||||
'.il',
|
||||
'.ils' // SKILL
|
||||
]
|
||||
],
|
||||
[
|
||||
'game',
|
||||
[
|
||||
'.mtl', // Material Template Library
|
||||
'.x3d', // X3D文件
|
||||
'.gltf', // glTF JSON
|
||||
'.prefab', // Unity预制体 (YAML格式)
|
||||
'.meta' // Unity元数据文件 (YAML格式)
|
||||
]
|
||||
],
|
||||
[
|
||||
'other',
|
||||
[
|
||||
'.mcfunction', // Minecraft函数
|
||||
'.jsp', // JSP
|
||||
'.aspx', // ASP.NET
|
||||
'.ipynb', // Jupyter Notebook
|
||||
'.cake',
|
||||
'.ctp', // CakePHP
|
||||
'.cfm',
|
||||
'.cfc' // ColdFusion
|
||||
]
|
||||
]
|
||||
])
|
||||
|
||||
export const textExts = Array.from(textExtsByCategory.values()).flat()
|
||||
|
||||
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]
|
||||
|
||||
|
||||
@@ -304,7 +304,12 @@ export class SelectionService {
|
||||
if (!this.selectionHook) return false
|
||||
|
||||
this.selectionHook.stop()
|
||||
this.selectionHook.cleanup()
|
||||
this.selectionHook.cleanup() //already remove all listeners
|
||||
|
||||
//reset the listener states
|
||||
this.isCtrlkeyListenerActive = false
|
||||
this.isHideByMouseKeyListenerActive = false
|
||||
|
||||
if (this.toolbarWindow) {
|
||||
this.toolbarWindow.close()
|
||||
this.toolbarWindow = null
|
||||
@@ -836,6 +841,8 @@ export class SelectionService {
|
||||
//ctrlkey pressed
|
||||
if (this.lastCtrlkeyDownTime === 0) {
|
||||
this.lastCtrlkeyDownTime = Date.now()
|
||||
//add the mouse-wheel listener, detect if user is zooming in/out
|
||||
this.selectionHook!.on('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -859,9 +866,20 @@ export class SelectionService {
|
||||
*/
|
||||
private handleKeyUpCtrlkeyMode = (data: KeyboardEventData) => {
|
||||
if (!this.isCtrlkey(data.vkCode)) return
|
||||
//remove the mouse-wheel listener
|
||||
this.selectionHook!.off('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
|
||||
this.lastCtrlkeyDownTime = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse wheel events in ctrlkey trigger mode
|
||||
* ignore CtrlKey pressing when mouse wheel is used
|
||||
* because user is zooming in/out
|
||||
*/
|
||||
private handleMouseWheelCtrlkeyMode = () => {
|
||||
this.lastCtrlkeyDownTime = -1
|
||||
}
|
||||
|
||||
//check if the key is ctrl key
|
||||
private isCtrlkey(vkCode: number) {
|
||||
return vkCode === 162 || vkCode === 163
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
},
|
||||
Tooltip: {
|
||||
fontSize: 13
|
||||
},
|
||||
ColorPicker: {
|
||||
fontFamily: 'var(--code-font-family)'
|
||||
}
|
||||
},
|
||||
token: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
setLaunchToTray,
|
||||
setPinTopicsToTop,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setShowTokens,
|
||||
setSidebarIcons,
|
||||
setTargetLanguage,
|
||||
setTheme,
|
||||
@@ -83,6 +84,9 @@ export function useSettings() {
|
||||
},
|
||||
setAssistantIconType(assistantIconType: AssistantIconType) {
|
||||
dispatch(setAssistantIconType(assistantIconType))
|
||||
},
|
||||
setShowTokens(showTokens: boolean) {
|
||||
dispatch(setShowTokens(showTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...",
|
||||
@@ -1112,7 +1113,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",
|
||||
@@ -1121,12 +1124,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",
|
||||
@@ -1140,10 +1145,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",
|
||||
@@ -1506,6 +1510,7 @@
|
||||
"advancedSettings": "Advanced Settings"
|
||||
},
|
||||
"messages.prompt": "Show prompt",
|
||||
"messages.tokens": "Show token usage",
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||
|
||||
@@ -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": "翻訳中...",
|
||||
@@ -1110,7 +1111,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数式に$$を強制使用",
|
||||
@@ -1119,29 +1122,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": "自動バックアップ",
|
||||
@@ -1502,6 +1508,7 @@
|
||||
"advancedSettings": "詳細設定"
|
||||
},
|
||||
"messages.prompt": "プロンプト表示",
|
||||
"messages.tokens": "トークン使用量を表示",
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||
|
||||
@@ -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": "Перевод...",
|
||||
@@ -1110,7 +1111,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",
|
||||
@@ -1119,12 +1122,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",
|
||||
@@ -1138,10 +1143,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": "Автоматическое резервное копирование",
|
||||
@@ -1502,6 +1506,7 @@
|
||||
"advancedSettings": "Расширенные настройки"
|
||||
},
|
||||
"messages.prompt": "Показывать подсказки",
|
||||
"messages.tokens": "Показать использование токенов",
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||
|
||||
@@ -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": "导出到语雀",
|
||||
@@ -662,7 +663,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": "切换模型回答",
|
||||
@@ -1112,7 +1113,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公式",
|
||||
@@ -1121,14 +1124,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",
|
||||
@@ -1142,10 +1147,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": "自动备份",
|
||||
@@ -1506,6 +1510,7 @@
|
||||
"advancedSettings": "高级设置"
|
||||
},
|
||||
"messages.prompt": "显示提示词",
|
||||
"messages.tokens": "显示Token用量",
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
"messages.grid_popover_trigger": "网格详情触发",
|
||||
|
||||
@@ -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": "翻譯中...",
|
||||
@@ -1112,7 +1113,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公式強制使用$$",
|
||||
@@ -1121,12 +1124,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",
|
||||
@@ -1140,10 +1145,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": "自動備份",
|
||||
@@ -1505,6 +1509,7 @@
|
||||
"advancedSettings": "高級設定"
|
||||
},
|
||||
"messages.prompt": "提示詞顯示",
|
||||
"messages.tokens": "Token用量顯示",
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
"messages.grid_popover_trigger": "網格詳細資訊觸發",
|
||||
|
||||
@@ -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 && {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { Popover } from 'antd'
|
||||
@@ -11,6 +12,7 @@ interface MessageTokensProps {
|
||||
}
|
||||
|
||||
const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
||||
const { showTokens } = useSettings()
|
||||
// const { generating } = useRuntime()
|
||||
const locateMessage = () => {
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false)
|
||||
@@ -23,7 +25,7 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
||||
if (message.role === 'user') {
|
||||
return (
|
||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
||||
Tokens: {message?.usage?.total_tokens}
|
||||
{showTokens && `Tokens: ${message?.usage?.total_tokens}`}
|
||||
</MessageMetadata>
|
||||
)
|
||||
}
|
||||
@@ -54,7 +56,7 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
||||
{hasMetrics ? (
|
||||
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
|
||||
{tokensInfo}
|
||||
{showTokens && tokensInfo}
|
||||
</Popover>
|
||||
) : (
|
||||
tokensInfo
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
setShowInputEstimatedTokens,
|
||||
setShowMessageDivider,
|
||||
setShowPrompt,
|
||||
setShowTokens,
|
||||
setShowTranslateConfirm,
|
||||
setThoughtAutoCollapse
|
||||
} from '@renderer/store/settings'
|
||||
@@ -113,7 +114,8 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
messageNavigation,
|
||||
enableQuickPanelTriggers,
|
||||
enableBackspaceDeleteModel,
|
||||
showTranslateConfirm
|
||||
showTranslateConfirm,
|
||||
showTokens
|
||||
} = useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||
@@ -336,6 +338,11 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.tokens')}</SettingRowTitleSmall>
|
||||
<Switch size="small" checked={showTokens} onChange={(checked) => dispatch(setShowTokens(checked))} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
|
||||
@@ -174,7 +174,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...topic, name: summaryText, isNameManuallyEdited: false })
|
||||
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
|
||||
updateTopic(updatedTopic)
|
||||
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
||||
} else {
|
||||
window.message?.error(t('message.error.fetchTopicName'))
|
||||
}
|
||||
@@ -192,7 +194,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
updateTopic({ ...topic, name, isNameManuallyEdited: true })
|
||||
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
||||
updateTopic(updatedTopic)
|
||||
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -308,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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Center, VStack } from '@renderer/components/Layout'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
|
||||
import { Alert, Button } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
@@ -26,7 +26,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const checkBinaries = async () => {
|
||||
const checkBinaries = useCallback(async () => {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
const bunExists = await window.api.isBinaryExist('bun')
|
||||
const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo()
|
||||
@@ -36,7 +36,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
setUvPath(uvPath)
|
||||
setBunPath(bunPath)
|
||||
setBinariesDir(dir)
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
const installUV = async () => {
|
||||
try {
|
||||
@@ -69,7 +69,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
|
||||
useEffect(() => {
|
||||
checkBinaries()
|
||||
}, [])
|
||||
}, [checkBinaries])
|
||||
|
||||
if (mini) {
|
||||
const installed = isUvInstalled && isBunInstalled
|
||||
|
||||
@@ -445,6 +445,14 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
)
|
||||
}
|
||||
|
||||
if (thinking_content) {
|
||||
onChunk({
|
||||
type: ChunkType.THINKING_COMPLETE,
|
||||
text: thinking_content,
|
||||
thinking_millsec: new Date().getTime() - time_first_token_millsec
|
||||
})
|
||||
}
|
||||
|
||||
userMessages.push({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
@@ -464,18 +472,31 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
|
||||
finalUsage.prompt_tokens += message.usage?.input_tokens || 0
|
||||
finalUsage.completion_tokens += message.usage?.output_tokens || 0
|
||||
finalUsage.total_tokens += finalUsage.prompt_tokens + finalUsage.completion_tokens
|
||||
finalMetrics.completion_tokens = finalUsage.completion_tokens
|
||||
finalMetrics.time_completion_millsec += new Date().getTime() - start_time_millsec
|
||||
finalMetrics.time_first_token_millsec = time_first_token_millsec - start_time_millsec
|
||||
// 直接修改finalUsage对象会报错,TypeError: Cannot assign to read only property 'prompt_tokens' of object '#<Object>'
|
||||
// 暂未找到原因
|
||||
const updatedUsage: Usage = {
|
||||
...finalUsage,
|
||||
prompt_tokens: finalUsage.prompt_tokens + (message.usage?.input_tokens || 0),
|
||||
completion_tokens: finalUsage.completion_tokens + (message.usage?.output_tokens || 0)
|
||||
}
|
||||
updatedUsage.total_tokens = updatedUsage.prompt_tokens + updatedUsage.completion_tokens
|
||||
|
||||
const updatedMetrics: Metrics = {
|
||||
...finalMetrics,
|
||||
completion_tokens: updatedUsage.completion_tokens,
|
||||
time_completion_millsec:
|
||||
finalMetrics.time_completion_millsec + (new Date().getTime() - start_time_millsec),
|
||||
time_first_token_millsec: time_first_token_millsec - start_time_millsec
|
||||
}
|
||||
|
||||
Object.assign(finalUsage, updatedUsage)
|
||||
Object.assign(finalMetrics, updatedMetrics)
|
||||
|
||||
onChunk({
|
||||
type: ChunkType.BLOCK_COMPLETE,
|
||||
response: {
|
||||
usage: finalUsage,
|
||||
metrics: finalMetrics
|
||||
usage: updatedUsage,
|
||||
metrics: updatedMetrics
|
||||
}
|
||||
})
|
||||
resolve()
|
||||
@@ -488,7 +509,9 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
}
|
||||
onChunk({ type: ChunkType.LLM_RESPONSE_CREATED })
|
||||
const start_time_millsec = new Date().getTime()
|
||||
await processStream(body, 0).finally(cleanup)
|
||||
await processStream(body, 0).finally(() => {
|
||||
cleanup()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -571,6 +571,16 @@ export abstract class BaseOpenAIProvider extends BaseProvider {
|
||||
if (time_first_token_millsec === 0) {
|
||||
time_first_token_millsec = new Date().getTime()
|
||||
}
|
||||
// Insert separation between summary parts
|
||||
if (thinkContent.length > 0) {
|
||||
const separator = '\n\n'
|
||||
onChunk({
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: separator,
|
||||
thinking_millsec: new Date().getTime() - time_first_token_millsec
|
||||
})
|
||||
thinkContent += separator
|
||||
}
|
||||
break
|
||||
case 'response.reasoning_summary_text.delta':
|
||||
onChunk({
|
||||
@@ -944,28 +954,32 @@ export abstract class BaseOpenAIProvider extends BaseProvider {
|
||||
if (!model) {
|
||||
return { valid: false, error: new Error('No model found') }
|
||||
}
|
||||
if (stream) {
|
||||
const response = await this.sdk.responses.create({
|
||||
model: model.id,
|
||||
input: [{ role: 'user', content: 'hi' }],
|
||||
stream: true
|
||||
})
|
||||
for await (const chunk of response) {
|
||||
if (chunk.type === 'response.output_text.delta') {
|
||||
return { valid: true, error: null }
|
||||
try {
|
||||
if (stream) {
|
||||
const response = await this.sdk.responses.create({
|
||||
model: model.id,
|
||||
input: [{ role: 'user', content: 'hi' }],
|
||||
stream: true
|
||||
})
|
||||
for await (const chunk of response) {
|
||||
if (chunk.type === 'response.output_text.delta') {
|
||||
return { valid: true, error: null }
|
||||
}
|
||||
}
|
||||
return { valid: false, error: new Error('No streaming response') }
|
||||
} else {
|
||||
const response = await this.sdk.responses.create({
|
||||
model: model.id,
|
||||
input: [{ role: 'user', content: 'hi' }],
|
||||
stream: false
|
||||
})
|
||||
if (!response.output_text) {
|
||||
return { valid: false, error: new Error('No response') }
|
||||
}
|
||||
return { valid: true, error: null }
|
||||
}
|
||||
throw new Error('Empty streaming response')
|
||||
} else {
|
||||
const response = await this.sdk.responses.create({
|
||||
model: model.id,
|
||||
input: [{ role: 'user', content: 'hi' }],
|
||||
stream: false
|
||||
})
|
||||
if (!response.output_text) {
|
||||
throw new Error('Empty response')
|
||||
}
|
||||
return { valid: true, error: null }
|
||||
} catch (error: any) {
|
||||
return { valid: false, error: error }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -471,7 +471,7 @@ export function checkApiProvider(provider: Provider): {
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkApi(provider: Provider, model: Model) {
|
||||
export async function checkApi(provider: Provider, model: Model): Promise<{ valid: boolean; error: Error | null }> {
|
||||
const validation = checkApiProvider(provider)
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
@@ -484,9 +484,16 @@ export async function checkApi(provider: Provider, model: Model) {
|
||||
|
||||
// Try streaming check first
|
||||
const result = await ai.check(model, true)
|
||||
|
||||
if (result.valid && !result.error) {
|
||||
return result
|
||||
}
|
||||
|
||||
return ai.check(model, false)
|
||||
// 不应该假设错误由流式引发。多次发起检测请求可能触发429,掩盖了真正的问题。
|
||||
// 但这里错误类型做的很粗糙,暂时先这样
|
||||
if (result.error && result.error.message.includes('stream')) {
|
||||
return ai.check(model, false)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1521,6 +1521,7 @@ const migrateConfig = {
|
||||
provider.type = 'mistral'
|
||||
}
|
||||
})
|
||||
state.settings.showTokens = true
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface SettingsState {
|
||||
proxyUrl?: string
|
||||
userName: string
|
||||
showPrompt: boolean
|
||||
showTokens: boolean
|
||||
showMessageDivider: boolean
|
||||
messageFont: 'system' | 'serif'
|
||||
showInputEstimatedTokens: boolean
|
||||
@@ -128,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
|
||||
// 思源笔记配置
|
||||
@@ -191,6 +194,7 @@ export const initialState: SettingsState = {
|
||||
proxyUrl: undefined,
|
||||
userName: '',
|
||||
showPrompt: true,
|
||||
showTokens: true,
|
||||
showMessageDivider: true,
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false,
|
||||
@@ -269,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,
|
||||
@@ -355,6 +361,9 @@ const settingsSlice = createSlice({
|
||||
setShowPrompt: (state, action: PayloadAction<boolean>) => {
|
||||
state.showPrompt = action.payload
|
||||
},
|
||||
setShowTokens: (state, action: PayloadAction<boolean>) => {
|
||||
state.showTokens = action.payload
|
||||
},
|
||||
setShowMessageDivider: (state, action: PayloadAction<boolean>) => {
|
||||
state.showMessageDivider = action.payload
|
||||
},
|
||||
@@ -575,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
|
||||
@@ -599,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
|
||||
},
|
||||
@@ -660,6 +675,8 @@ const settingsSlice = createSlice({
|
||||
})
|
||||
|
||||
export const {
|
||||
setShowModelNameInMarkdown,
|
||||
setShowModelProviderInMarkdown,
|
||||
setShowAssistants,
|
||||
toggleShowAssistants,
|
||||
setShowTopics,
|
||||
@@ -672,6 +689,7 @@ export const {
|
||||
setProxyUrl,
|
||||
setUserName,
|
||||
setShowPrompt,
|
||||
setShowTokens,
|
||||
setShowMessageDivider,
|
||||
setMessageFont,
|
||||
setShowInputEstimatedTokens,
|
||||
@@ -731,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')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user