fix: expand code syntax highlighting options (#307)
* added locale for context-menu * fix: expand code syntax highlighting options * fix: type for theme --------- Co-authored-by: injurka <ikornilov.ext@prosebya.ru~>
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
@@ -37,4 +37,4 @@
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+20
-17
@@ -8,6 +8,7 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
@@ -23,23 +24,25 @@ function App(): JSX.Element {
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/messages/*" element={<HistoryPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
<SyntaxHighlighterProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/messages/*" element={<HistoryPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</SyntaxHighlighterProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||
import { loadScript } from '@renderer/utils'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
BundledLanguage,
|
||||
bundledLanguages,
|
||||
BundledTheme,
|
||||
bundledThemes,
|
||||
createHighlighter,
|
||||
HighlighterGeneric
|
||||
} from 'shiki'
|
||||
|
||||
interface SyntaxHighlighterContextType {
|
||||
codeToHtml: (code: string, language: string) => string
|
||||
}
|
||||
|
||||
const SyntaxHighlighterContext = createContext<SyntaxHighlighterContextType | undefined>(undefined)
|
||||
|
||||
export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
const [highlighter, setHighlighter] = useState<HighlighterGeneric<BundledLanguage, BundledTheme> | null>(null)
|
||||
const { codeStyle } = useSettings()
|
||||
|
||||
const highlighterTheme = useMemo(() => {
|
||||
if (codeStyle === 'auto') return theme === ThemeMode.light ? 'one-light' : 'material-theme-darker'
|
||||
else return codeStyle
|
||||
}, [theme, codeStyle])
|
||||
|
||||
useEffect(() => {
|
||||
const initMermaid = async () => {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
initMermaid()
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
const initHighlighter = async () => {
|
||||
const hl = await createHighlighter({
|
||||
themes: Object.keys(bundledThemes),
|
||||
langs: Object.keys(bundledLanguages)
|
||||
})
|
||||
setHighlighter(hl)
|
||||
}
|
||||
|
||||
initHighlighter()
|
||||
}, [])
|
||||
|
||||
const codeToHtml = (code: string, language: string) => {
|
||||
if (!highlighter) return ''
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: language,
|
||||
theme: highlighterTheme,
|
||||
transformers: [
|
||||
{
|
||||
preprocess(code) {
|
||||
if (code.endsWith('\n')) code = code.slice(0, -1)
|
||||
return code
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider>
|
||||
}
|
||||
|
||||
export const useSyntaxHighlighter = () => {
|
||||
const context = useContext(SyntaxHighlighterContext)
|
||||
if (!context) {
|
||||
throw new Error('useSyntaxHighlighter must be used within a SyntaxHighlighterProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const codeThemes = ['auto', ...Object.keys(bundledThemes)] as CodeStyleVarious[]
|
||||
@@ -67,6 +67,7 @@
|
||||
"upgrade.success.button": "Restart",
|
||||
"topic.added": "New topic added",
|
||||
"save.success.title": "Saved successfully",
|
||||
"message.code_style": "Code style",
|
||||
"message.style": "Message Style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain"
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"upgrade.success.button": "重启",
|
||||
"topic.added": "话题添加成功",
|
||||
"save.success.title": "保存成功",
|
||||
"message.code_style": "代码风格",
|
||||
"message.style": "消息样式",
|
||||
"message.style.bubble": "气泡",
|
||||
"message.style.plain": "简洁"
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"topic.added": "新話題已添加",
|
||||
"save.success.title": "保存成功",
|
||||
"message.code_style": "程式碼風格",
|
||||
"message.style": "消息樣式",
|
||||
"message.style.bubble": "氣泡",
|
||||
"message.style.plain": "簡潔"
|
||||
|
||||
@@ -2,20 +2,6 @@ import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import localforage from 'localforage'
|
||||
|
||||
import { APP_NAME } from './config/env'
|
||||
import { ThemeMode } from './types'
|
||||
import { loadScript } from './utils'
|
||||
|
||||
export async function initMermaid(theme: ThemeMode) {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
localforage.config({
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { CheckOutlined } from '@ant-design/icons'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { initMermaid } from '@renderer/init'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import React, { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Artifacts from './Artifacts'
|
||||
@@ -23,11 +19,13 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const showFooterCopyButton = children && children.length > 500
|
||||
const { codeShowLineNumbers, fontSize } = useSettings()
|
||||
const { theme } = useTheme()
|
||||
const language = match?.[1]
|
||||
const language = match?.[1] ?? 'text'
|
||||
|
||||
const { codeToHtml } = useSyntaxHighlighter()
|
||||
|
||||
const html = codeToHtml(children, language)
|
||||
|
||||
if (language === 'mermaid') {
|
||||
initMermaid(theme)
|
||||
return <Mermaid chart={children} />
|
||||
}
|
||||
|
||||
@@ -37,20 +35,17 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
<CopyButton text={children} />
|
||||
</CodeHeader>
|
||||
<SyntaxHighlighter
|
||||
language={match[1]}
|
||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||
wrapLongLines={false}
|
||||
showLineNumbers={codeShowLineNumbers}
|
||||
customStyle={{
|
||||
<CodeContent
|
||||
isShowLineNumbers={codeShowLineNumbers}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{
|
||||
border: '0.5px solid var(--color-code-background)',
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
marginTop: 0,
|
||||
fontSize
|
||||
}}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
}}
|
||||
/>
|
||||
{showFooterCopyButton && (
|
||||
<CodeFooter>
|
||||
<CopyButton text={children} style={{ marginTop: -40, marginRight: 10 }} />
|
||||
@@ -81,6 +76,31 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
|
||||
)
|
||||
}
|
||||
|
||||
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
|
||||
.shiki {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.isShowLineNumbers &&
|
||||
`
|
||||
code {
|
||||
counter-reset: step;
|
||||
counter-increment: step 0;
|
||||
}
|
||||
|
||||
code .line::before {
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
width: 1rem;
|
||||
margin-right: 1rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
opacity: 0.35;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const CodeHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,12 +2,14 @@ import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-desi
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setCodeShowLineNumbers,
|
||||
setCodeStyle,
|
||||
setFontSize,
|
||||
setMathEngine,
|
||||
setMessageFont,
|
||||
@@ -29,14 +31,14 @@ interface Props {
|
||||
|
||||
const SettingsTab: FC<Props> = (props) => {
|
||||
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
|
||||
const { fontSize } = useSettings()
|
||||
const { messageStyle, codeStyle, fontSize } = useSettings()
|
||||
|
||||
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
||||
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
||||
const { messageStyle } = useSettings()
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -216,6 +218,21 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||
<Select
|
||||
value={codeStyle}
|
||||
onChange={(value) => dispatch(setCodeStyle(value))}
|
||||
style={{ width: 160 }}
|
||||
size="small">
|
||||
{codeThemes.map((theme) => (
|
||||
<Select.Option key={theme} value={theme}>
|
||||
{theme}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
|
||||
<Select
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { LanguageVarious, ThemeMode } from '@renderer/types'
|
||||
import { CodeStyleVarious, LanguageVarious, ThemeMode } from '@renderer/types'
|
||||
|
||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface SettingsState {
|
||||
codeShowLineNumbers: boolean
|
||||
mathEngine: 'MathJax' | 'KaTeX'
|
||||
messageStyle: 'plain' | 'bubble'
|
||||
codeStyle: CodeStyleVarious
|
||||
// webdav 配置 host, user, pass, path
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
@@ -54,6 +55,7 @@ const initialState: SettingsState = {
|
||||
codeShowLineNumbers: false,
|
||||
mathEngine: 'MathJax',
|
||||
messageStyle: 'plain',
|
||||
codeStyle: 'auto',
|
||||
webdavHost: '',
|
||||
webdavUser: '',
|
||||
webdavPass: '',
|
||||
@@ -145,6 +147,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
|
||||
state.messageStyle = action.payload
|
||||
},
|
||||
setCodeStyle: (state, action: PayloadAction<CodeStyleVarious>) => {
|
||||
state.codeStyle = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -176,7 +181,8 @@ export const {
|
||||
setWebdavPath,
|
||||
setCodeShowLineNumbers,
|
||||
setMathEngine,
|
||||
setMessageStyle
|
||||
setMessageStyle,
|
||||
setCodeStyle
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import OpenAI from 'openai'
|
||||
import { BuiltinTheme } from 'shiki'
|
||||
|
||||
export type Assistant = {
|
||||
id: string
|
||||
@@ -137,6 +138,7 @@ export enum ThemeMode {
|
||||
auto = 'auto'
|
||||
}
|
||||
export type LanguageVarious = 'zh-CN' | 'zh-TW' | 'en-US'
|
||||
export type CodeStyleVarious = BuiltinTheme | 'auto'
|
||||
|
||||
export type WebDavConfig = {
|
||||
webdavHost: string
|
||||
|
||||
Reference in New Issue
Block a user