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