mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
refactor(CodeTool): use hook for codeblock tools rather than context (#6273)
* refactor(CodeTool): use hook for codeblock tools rather than context * fix: codeblock overflow behaviour * fix: CodePreview scrollbar * refactor: move margin to CodeHeader * refactor: add min-width to codeblock
This commit is contained in:
parent
a7520169e6
commit
2f312d68a0
@ -321,6 +321,7 @@ mjx-container {
|
||||
|
||||
.cm-lineWrapping * {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,8 @@ body[theme-mode='light'] {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
::-webkit-scrollbar-track,
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@ -30,7 +31,7 @@ body[theme-mode='light'] {
|
||||
}
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
pre:not(.shiki)::-webkit-scrollbar-thumb {
|
||||
border-radius: 0;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
&:hover {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar'
|
||||
import { CodeTool, TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { uuid } from '@renderer/utils'
|
||||
@ -12,6 +12,7 @@ import styled from 'styled-components'
|
||||
interface CodePreviewProps {
|
||||
children: string
|
||||
language: string
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@ -20,7 +21,7 @@ interface CodePreviewProps {
|
||||
* - 通过 shiki tokenizer 处理流式响应
|
||||
* - 为了正确执行语法高亮,必须保证流式响应都依次到达 tokenizer,不能跳过
|
||||
*/
|
||||
const CodePreview = ({ children, language }: CodePreviewProps) => {
|
||||
const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
||||
const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings()
|
||||
const { activeShikiTheme, highlightCodeChunk, cleanupTokenizers } = useCodeStyle()
|
||||
const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
|
||||
@ -35,7 +36,7 @@ const CodePreview = ({ children, language }: CodePreviewProps) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { registerTool, removeTool } = useCodeToolbar()
|
||||
const { registerTool, removeTool } = useCodeTool(setTools)
|
||||
|
||||
// 展开/折叠工具
|
||||
useEffect(() => {
|
||||
@ -171,14 +172,13 @@ const CodePreview = ({ children, language }: CodePreviewProps) => {
|
||||
ref={codeContentRef}
|
||||
$lineNumbers={codeShowLineNumbers}
|
||||
$wrap={codeWrappable && !isUnwrapped}
|
||||
$fadeIn={hasHighlightedCode}
|
||||
style={{
|
||||
fontSize: fontSize - 1,
|
||||
maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none'
|
||||
}}>
|
||||
{hasHighlightedCode ? (
|
||||
<div className="fade-in-effect">
|
||||
<ShikiTokensRenderer language={language} tokenLines={tokenLines} />
|
||||
</div>
|
||||
<ShikiTokensRenderer language={language} tokenLines={tokenLines} />
|
||||
) : (
|
||||
<CodePlaceholder>{children}</CodePlaceholder>
|
||||
)}
|
||||
@ -229,26 +229,22 @@ const ShikiTokensRenderer: React.FC<{ language: string; tokenLines: ThemedToken[
|
||||
const ContentContainer = styled.div<{
|
||||
$lineNumbers: boolean
|
||||
$wrap: boolean
|
||||
$fadeIn: boolean
|
||||
}>`
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 0.5px solid transparent;
|
||||
border-radius: 5px;
|
||||
margin-top: 0;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.shiki {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
padding: 1em;
|
||||
|
||||
code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
.line {
|
||||
display: block;
|
||||
@ -256,7 +252,7 @@ const ContentContainer = styled.div<{
|
||||
padding-left: ${(props) => (props.$lineNumbers ? '2rem' : '0')};
|
||||
|
||||
* {
|
||||
word-wrap: ${(props) => (props.$wrap ? 'break-word' : undefined)};
|
||||
overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
|
||||
white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
|
||||
}
|
||||
}
|
||||
@ -292,18 +288,15 @@ const ContentContainer = styled.div<{
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-effect {
|
||||
animation: contentFadeIn 0.3s ease-in-out forwards;
|
||||
}
|
||||
animation: ${(props) => (props.$fadeIn ? 'contentFadeIn 0.3s ease-in-out forwards' : 'none')};
|
||||
`
|
||||
|
||||
const CodePlaceholder = styled.div`
|
||||
display: block;
|
||||
opacity: 0.1;
|
||||
flex-direction: column;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-x: hidden;
|
||||
display: block;
|
||||
min-height: 1.3rem;
|
||||
`
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import { Flex } from 'antd'
|
||||
import React, { memo, startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -7,9 +7,10 @@ import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
children: string
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
}
|
||||
|
||||
const MermaidPreview: React.FC<Props> = ({ children }) => {
|
||||
const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
||||
const { mermaid, isLoading, error: mermaidError } = useMermaid()
|
||||
const mermaidRef = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@ -25,6 +26,7 @@ const MermaidPreview: React.FC<Props> = ({ children }) => {
|
||||
|
||||
// 使用工具栏
|
||||
usePreviewTools({
|
||||
setTools,
|
||||
handleZoom,
|
||||
handleCopyImage,
|
||||
handleDownload
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import { Spin } from 'antd'
|
||||
import pako from 'pako'
|
||||
import React, { memo, useCallback, useRef, useState } from 'react'
|
||||
@ -134,9 +134,10 @@ const PlantUMLServerImage: React.FC<PlantUMLServerImageProps> = ({ format, diagr
|
||||
|
||||
interface PlantUMLProps {
|
||||
children: string
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
}
|
||||
|
||||
const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children }) => {
|
||||
const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children, setTools }) => {
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@ -165,6 +166,7 @@ const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children }) => {
|
||||
|
||||
// 使用工具栏
|
||||
usePreviewTools({
|
||||
setTools,
|
||||
handleZoom,
|
||||
handleCopyImage,
|
||||
handleDownload: customDownload
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import { memo, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
children: string
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
}
|
||||
|
||||
const SvgPreview: React.FC<Props> = ({ children }) => {
|
||||
const SvgPreview: React.FC<Props> = ({ children, setTools }) => {
|
||||
const svgContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 使用通用图像工具
|
||||
@ -17,6 +18,7 @@ const SvgPreview: React.FC<Props> = ({ children }) => {
|
||||
|
||||
// 使用工具栏
|
||||
usePreviewTools({
|
||||
setTools,
|
||||
handleCopyImage,
|
||||
handleDownload
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { CodeToolbar, CodeToolContext, TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar'
|
||||
import { CodeTool, CodeToolbar, TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { pyodideService } from '@renderer/services/PyodideService'
|
||||
import { extractTitle } from '@renderer/utils/formats'
|
||||
@ -49,6 +49,9 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [output, setOutput] = useState('')
|
||||
|
||||
const [tools, setTools] = useState<CodeTool[]>([])
|
||||
const { registerTool, removeTool } = useCodeTool(setTools)
|
||||
|
||||
const isExecutable = useMemo(() => {
|
||||
return codeExecution.enabled && language === 'python'
|
||||
}, [codeExecution.enabled, language])
|
||||
@ -59,33 +62,17 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
return hasSpecialView && viewMode === 'special'
|
||||
}, [hasSpecialView, viewMode])
|
||||
|
||||
const { updateContext, registerTool, removeTool } = useCodeToolbar()
|
||||
const handleCopySource = useCallback(() => {
|
||||
navigator.clipboard.writeText(children)
|
||||
window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' })
|
||||
}, [children, t])
|
||||
|
||||
useEffect(() => {
|
||||
updateContext({
|
||||
code: children,
|
||||
language
|
||||
})
|
||||
}, [children, language, updateContext])
|
||||
|
||||
const handleCopySource = useCallback(
|
||||
(ctx?: CodeToolContext) => {
|
||||
if (!ctx) return
|
||||
navigator.clipboard.writeText(ctx.code)
|
||||
window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' })
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
const handleDownloadSource = useCallback((ctx?: CodeToolContext) => {
|
||||
if (!ctx) return
|
||||
|
||||
const { code, language } = ctx
|
||||
const handleDownloadSource = useCallback(() => {
|
||||
let fileName = ''
|
||||
|
||||
// 尝试提取标题
|
||||
if (language === 'html' && code.includes('</html>')) {
|
||||
const title = extractTitle(code)
|
||||
if (language === 'html' && children.includes('</html>')) {
|
||||
const title = extractTitle(children)
|
||||
if (title) {
|
||||
fileName = `${title}.html`
|
||||
}
|
||||
@ -96,31 +83,26 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}`
|
||||
}
|
||||
|
||||
window.api.file.save(fileName, code)
|
||||
}, [])
|
||||
window.api.file.save(fileName, children)
|
||||
}, [children, language])
|
||||
|
||||
const handleRunScript = useCallback(
|
||||
(ctx?: CodeToolContext) => {
|
||||
if (!ctx) return
|
||||
const handleRunScript = useCallback(() => {
|
||||
setIsRunning(true)
|
||||
setOutput('')
|
||||
|
||||
setIsRunning(true)
|
||||
setOutput('')
|
||||
|
||||
pyodideService
|
||||
.runScript(ctx.code, {}, codeExecution.timeoutMinutes * 60000)
|
||||
.then((formattedOutput) => {
|
||||
setOutput(formattedOutput)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unexpected error:', error)
|
||||
setOutput(`Unexpected error: ${error.message || 'Unknown error'}`)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRunning(false)
|
||||
})
|
||||
},
|
||||
[codeExecution.timeoutMinutes]
|
||||
)
|
||||
pyodideService
|
||||
.runScript(children, {}, codeExecution.timeoutMinutes * 60000)
|
||||
.then((formattedOutput) => {
|
||||
setOutput(formattedOutput)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unexpected error:', error)
|
||||
setOutput(`Unexpected error: ${error.message || 'Unknown error'}`)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRunning(false)
|
||||
})
|
||||
}, [children, codeExecution.timeoutMinutes])
|
||||
|
||||
useEffect(() => {
|
||||
// 复制按钮
|
||||
@ -191,7 +173,7 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
...TOOL_SPECS.run,
|
||||
icon: isRunning ? <LoadingOutlined /> : <CirclePlay className="icon" />,
|
||||
tooltip: t('code_block.run'),
|
||||
onClick: (ctx) => !isRunning && handleRunScript(ctx)
|
||||
onClick: () => !isRunning && handleRunScript()
|
||||
})
|
||||
|
||||
return () => isExecutable && removeTool(TOOL_SPECS.run.id)
|
||||
@ -200,20 +182,32 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
// 源代码视图组件
|
||||
const sourceView = useMemo(() => {
|
||||
if (codeEditor.enabled) {
|
||||
return <CodeEditor value={children} language={language} onSave={onSave} options={{ stream: true }} />
|
||||
return (
|
||||
<CodeEditor
|
||||
value={children}
|
||||
language={language}
|
||||
onSave={onSave}
|
||||
options={{ stream: true }}
|
||||
setTools={setTools}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return <CodePreview language={language}>{children}</CodePreview>
|
||||
return (
|
||||
<CodePreview language={language} setTools={setTools}>
|
||||
{children}
|
||||
</CodePreview>
|
||||
)
|
||||
}
|
||||
}, [children, codeEditor.enabled, language, onSave])
|
||||
}, [children, codeEditor.enabled, language, onSave, setTools])
|
||||
|
||||
// 特殊视图组件映射
|
||||
const specialView = useMemo(() => {
|
||||
if (language === 'mermaid') {
|
||||
return <MermaidPreview>{children}</MermaidPreview>
|
||||
return <MermaidPreview setTools={setTools}>{children}</MermaidPreview>
|
||||
} else if (language === 'plantuml' && isValidPlantUML(children)) {
|
||||
return <PlantUmlPreview>{children}</PlantUmlPreview>
|
||||
return <PlantUmlPreview setTools={setTools}>{children}</PlantUmlPreview>
|
||||
} else if (language === 'svg') {
|
||||
return <SvgPreview>{children}</SvgPreview>
|
||||
return <SvgPreview setTools={setTools}>{children}</SvgPreview>
|
||||
}
|
||||
return null
|
||||
}, [children, language])
|
||||
@ -246,7 +240,7 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
return (
|
||||
<CodeBlockWrapper className="code-block" $isInSpecialView={isInSpecialView}>
|
||||
{renderHeader}
|
||||
<CodeToolbar />
|
||||
<CodeToolbar tools={tools} />
|
||||
{renderContent}
|
||||
{renderArtifacts}
|
||||
{isExecutable && output && <StatusBar>{output}</StatusBar>}
|
||||
@ -255,10 +249,11 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
}
|
||||
|
||||
const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
|
||||
/* FIXME: 在 bubble style 中撑开一些宽度*/
|
||||
min-width: min(calc(60vw - var(--sidebar-width)), 700px);
|
||||
position: relative;
|
||||
|
||||
.code-toolbar {
|
||||
margin-top: ${(props) => (props.$isInSpecialView ? '20px' : '0')};
|
||||
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
|
||||
border-radius: ${(props) => (props.$isInSpecialView ? '0' : '4px')};
|
||||
opacity: 0;
|
||||
@ -279,13 +274,13 @@ const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
|
||||
const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 0 10px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
margin-top: ${(props) => (props.$isInSpecialView ? '6px' : '0')};
|
||||
height: ${(props) => (props.$isInSpecialView ? '16px' : '34px')};
|
||||
`
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar'
|
||||
import { CodeTool, TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import CodeMirror, { Annotation, BasicSetupOptions, EditorView, Extension, keymap } from '@uiw/react-codemirror'
|
||||
@ -25,6 +25,7 @@ interface Props {
|
||||
language: string
|
||||
onSave?: (newContent: string) => void
|
||||
onChange?: (newContent: string) => void
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
minHeight?: string
|
||||
maxHeight?: string
|
||||
/** 用于覆写编辑器的某些设置 */
|
||||
@ -52,6 +53,7 @@ const CodeEditor = ({
|
||||
language,
|
||||
onSave,
|
||||
onChange,
|
||||
setTools,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
options,
|
||||
@ -88,7 +90,7 @@ const CodeEditor = ({
|
||||
|
||||
const langExtensions = useLanguageExtensions(language, options?.lint)
|
||||
|
||||
const { registerTool, removeTool } = useCodeToolbar()
|
||||
const { registerTool, removeTool } = useCodeTool(setTools)
|
||||
|
||||
// 展开/折叠工具
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
import React, { createContext, use, useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { CodeTool, CodeToolContext } from './types'
|
||||
|
||||
// 定义上下文默认值
|
||||
const defaultContext: CodeToolContext = {
|
||||
code: '',
|
||||
language: ''
|
||||
}
|
||||
|
||||
export interface CodeToolbarContextType {
|
||||
tools: CodeTool[]
|
||||
context: CodeToolContext
|
||||
registerTool: (tool: CodeTool) => void
|
||||
removeTool: (id: string) => void
|
||||
updateContext: (newContext: Partial<CodeToolContext>) => void
|
||||
}
|
||||
|
||||
const defaultCodeToolbarContext: CodeToolbarContextType = {
|
||||
tools: [],
|
||||
context: defaultContext,
|
||||
registerTool: () => {},
|
||||
removeTool: () => {},
|
||||
updateContext: () => {}
|
||||
}
|
||||
|
||||
const CodeToolbarContext = createContext<CodeToolbarContextType>(defaultCodeToolbarContext)
|
||||
|
||||
export const CodeToolbarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [tools, setTools] = useState<CodeTool[]>([])
|
||||
const [context, setContext] = useState<CodeToolContext>(defaultContext)
|
||||
|
||||
// 注册工具,如果已存在同ID工具则替换
|
||||
const registerTool = useCallback((tool: CodeTool) => {
|
||||
setTools((prev) => {
|
||||
const filtered = prev.filter((t) => t.id !== tool.id)
|
||||
return [...filtered, tool].sort((a, b) => b.order - a.order)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 移除工具
|
||||
const removeTool = useCallback((id: string) => {
|
||||
setTools((prev) => prev.filter((tool) => tool.id !== id))
|
||||
}, [])
|
||||
|
||||
// 更新上下文
|
||||
const updateContext = useCallback((newContext: Partial<CodeToolContext>) => {
|
||||
setContext((prev) => ({ ...prev, ...newContext }))
|
||||
}, [])
|
||||
|
||||
const value: CodeToolbarContextType = useMemo(
|
||||
() => ({
|
||||
tools,
|
||||
context,
|
||||
registerTool,
|
||||
removeTool,
|
||||
updateContext
|
||||
}),
|
||||
[tools, context, registerTool, removeTool, updateContext]
|
||||
)
|
||||
|
||||
return <CodeToolbarContext value={value}>{children}</CodeToolbarContext>
|
||||
}
|
||||
|
||||
export const useCodeToolbar = () => {
|
||||
const context = use(CodeToolbarContext)
|
||||
if (!context) {
|
||||
throw new Error('useCodeToolbar must be used within a CodeToolbarProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
26
src/renderer/src/components/CodeToolbar/hook.ts
Normal file
26
src/renderer/src/components/CodeToolbar/hook.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { CodeTool } from './types'
|
||||
|
||||
export const useCodeTool = (setTools?: (value: React.SetStateAction<CodeTool[]>) => void) => {
|
||||
// 注册工具,如果已存在同ID工具则替换
|
||||
const registerTool = useCallback(
|
||||
(tool: CodeTool) => {
|
||||
setTools?.((prev) => {
|
||||
const filtered = prev.filter((t) => t.id !== tool.id)
|
||||
return [...filtered, tool].sort((a, b) => b.order - a.order)
|
||||
})
|
||||
},
|
||||
[setTools]
|
||||
)
|
||||
|
||||
// 移除工具
|
||||
const removeTool = useCallback(
|
||||
(id: string) => {
|
||||
setTools?.((prev) => prev.filter((tool) => tool.id !== id))
|
||||
},
|
||||
[setTools]
|
||||
)
|
||||
|
||||
return { registerTool, removeTool }
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export * from './constants'
|
||||
export * from './context'
|
||||
export * from './hook'
|
||||
export * from './toolbar'
|
||||
export * from './types'
|
||||
export * from './usePreviewTools'
|
||||
|
||||
@ -5,7 +5,6 @@ import React, { memo, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useCodeToolbar } from './context'
|
||||
import { CodeTool } from './types'
|
||||
|
||||
interface CodeToolButtonProps {
|
||||
@ -13,22 +12,19 @@ interface CodeToolButtonProps {
|
||||
}
|
||||
|
||||
const CodeToolButton: React.FC<CodeToolButtonProps> = memo(({ tool }) => {
|
||||
const { context } = useCodeToolbar()
|
||||
|
||||
return (
|
||||
<Tooltip key={`${tool.id}-${tool.tooltip}`} title={tool.tooltip} mouseEnterDelay={0.5}>
|
||||
<ToolWrapper onClick={() => tool.onClick(context)}>{tool.icon}</ToolWrapper>
|
||||
<Tooltip key={tool.id} title={tool.tooltip} mouseEnterDelay={0.5}>
|
||||
<ToolWrapper onClick={() => tool.onClick()}>{tool.icon}</ToolWrapper>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
export const CodeToolbar: React.FC = memo(() => {
|
||||
const { tools, context } = useCodeToolbar()
|
||||
export const CodeToolbar: React.FC<{ tools: CodeTool[] }> = memo(({ tools }) => {
|
||||
const [showQuickTools, setShowQuickTools] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 根据条件显示工具
|
||||
const visibleTools = tools.filter((tool) => !tool.visible || tool.visible(context))
|
||||
const visibleTools = tools.filter((tool) => !tool.visible || tool.visible())
|
||||
|
||||
// 按类型分组
|
||||
const coreTools = visibleTools.filter((tool) => tool.type === 'core')
|
||||
|
||||
@ -20,16 +20,6 @@ export interface CodeToolSpec {
|
||||
export interface CodeTool extends CodeToolSpec {
|
||||
icon: React.ReactNode
|
||||
tooltip: string
|
||||
visible?: (ctx?: CodeToolContext) => boolean
|
||||
onClick: (ctx?: CodeToolContext) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具上下文接口
|
||||
* @param code 代码内容
|
||||
* @param language 语言类型
|
||||
*/
|
||||
export interface CodeToolContext {
|
||||
code: string
|
||||
language: string
|
||||
visible?: () => boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DownloadPngIcon, DownloadSvgIcon } from '../Icons/DownloadIcons'
|
||||
import { TOOL_SPECS } from './constants'
|
||||
import { useCodeToolbar } from './context'
|
||||
import { useCodeTool } from './hook'
|
||||
import { CodeTool } from './types'
|
||||
|
||||
// 预编译正则表达式用于查询位置
|
||||
const TRANSFORM_REGEX = /translate\((-?\d+\.?\d*)px,\s*(-?\d+\.?\d*)px\)/
|
||||
@ -272,6 +273,7 @@ export const usePreviewToolHandlers = (
|
||||
}
|
||||
|
||||
export interface PreviewToolsOptions {
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
handleZoom?: (delta: number) => void
|
||||
handleCopyImage?: () => Promise<void>
|
||||
handleDownload?: (format: 'svg' | 'png') => void
|
||||
@ -280,9 +282,9 @@ export interface PreviewToolsOptions {
|
||||
/**
|
||||
* 提供预览组件通用工具栏功能的自定义Hook
|
||||
*/
|
||||
export const usePreviewTools = ({ handleZoom, handleCopyImage, handleDownload }: PreviewToolsOptions) => {
|
||||
export const usePreviewTools = ({ setTools, handleZoom, handleCopyImage, handleDownload }: PreviewToolsOptions) => {
|
||||
const { t } = useTranslation()
|
||||
const { registerTool, removeTool } = useCodeToolbar()
|
||||
const { registerTool, removeTool } = useCodeTool(setTools)
|
||||
|
||||
useEffect(() => {
|
||||
// 根据提供的功能有选择性地注册工具
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import CodeBlockView from '@renderer/components/CodeBlockView'
|
||||
import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
|
||||
import React, { memo, useCallback } from 'react'
|
||||
|
||||
interface Props {
|
||||
@ -24,11 +23,9 @@ const CodeBlock: React.FC<Props> = ({ children, className, id, onSave }) => {
|
||||
)
|
||||
|
||||
return match ? (
|
||||
<CodeToolbarProvider>
|
||||
<CodeBlockView language={language} onSave={handleSave}>
|
||||
{children}
|
||||
</CodeBlockView>
|
||||
</CodeToolbarProvider>
|
||||
<CodeBlockView language={language} onSave={handleSave}>
|
||||
{children}
|
||||
</CodeBlockView>
|
||||
) : (
|
||||
<code className={className} style={{ textWrap: 'wrap' }}>
|
||||
{children}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setMCPServerActive } from '@renderer/store/mcp'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
@ -157,25 +156,23 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({ visible, onClose, onSuc
|
||||
name="serverConfig"
|
||||
label={t('settings.mcp.addServer.importFrom.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.mcp.addServer.importFrom.placeholder') }]}>
|
||||
<CodeToolbarProvider>
|
||||
<CodeEditor
|
||||
// 如果表單值為空,顯示範例 JSON;否則顯示表單值
|
||||
value={serverConfigValue}
|
||||
placeholder={initialJsonExample}
|
||||
language="json"
|
||||
onChange={handleEditorChange}
|
||||
maxHeight="300px"
|
||||
options={{
|
||||
lint: true,
|
||||
collapsible: true,
|
||||
wrappable: true,
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
keymap: true
|
||||
}}
|
||||
/>
|
||||
</CodeToolbarProvider>
|
||||
<CodeEditor
|
||||
// 如果表單值為空,顯示範例 JSON;否則顯示表單值
|
||||
value={serverConfigValue}
|
||||
placeholder={initialJsonExample}
|
||||
language="json"
|
||||
onChange={handleEditorChange}
|
||||
maxHeight="300px"
|
||||
options={{
|
||||
lint: true,
|
||||
collapsible: true,
|
||||
wrappable: true,
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
keymap: true
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setMCPServers } from '@renderer/store/mcp'
|
||||
@ -121,23 +120,21 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
</div>
|
||||
{jsonConfig && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<CodeToolbarProvider>
|
||||
<CodeEditor
|
||||
value={jsonConfig}
|
||||
language="json"
|
||||
onChange={(value) => setJsonConfig(value)}
|
||||
maxHeight="60vh"
|
||||
options={{
|
||||
lint: true,
|
||||
collapsible: true,
|
||||
wrappable: true,
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
keymap: true
|
||||
}}
|
||||
/>
|
||||
</CodeToolbarProvider>
|
||||
<CodeEditor
|
||||
value={jsonConfig}
|
||||
language="json"
|
||||
onChange={(value) => setJsonConfig(value)}
|
||||
maxHeight="60vh"
|
||||
options={{
|
||||
lint: true,
|
||||
collapsible: true,
|
||||
wrappable: true,
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
keymap: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Typography.Text type="secondary">{t('settings.mcp.jsonModeHint')}</Typography.Text>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user