diff --git a/src/renderer/src/context/SyntaxHighlighterProvider.tsx b/src/renderer/src/context/SyntaxHighlighterProvider.tsx index 9e94665dbb..ce35f2bba6 100644 --- a/src/renderer/src/context/SyntaxHighlighterProvider.tsx +++ b/src/renderer/src/context/SyntaxHighlighterProvider.tsx @@ -3,22 +3,10 @@ import { useMermaid } from '@renderer/hooks/useMermaid' import { useSettings } from '@renderer/hooks/useSettings' import { CodeCacheService } from '@renderer/services/CodeCacheService' import { type CodeStyleVarious, ThemeMode } from '@renderer/types' +import { getHighlighter, loadLanguageIfNeeded, loadThemeIfNeeded } from '@renderer/utils/highlighter' import type React from 'react' import { createContext, type PropsWithChildren, use, useCallback, useMemo } from 'react' -import { bundledLanguages, bundledThemes, createHighlighter, type Highlighter } from 'shiki' - -let highlighterPromise: Promise | null = null - -async function getHighlighter() { - if (!highlighterPromise) { - highlighterPromise = createHighlighter({ - langs: ['javascript', 'typescript', 'python', 'java', 'markdown'], - themes: ['one-light', 'material-theme-darker'] - }) - } - - return await highlighterPromise -} +import { bundledThemes } from 'shiki' interface SyntaxHighlighterContextType { codeToHtml: (code: string, language: string, enableCache: boolean) => Promise @@ -60,19 +48,8 @@ export const SyntaxHighlighterProvider: React.FC = ({ childre try { const highlighter = await getHighlighter() - if (!highlighter.getLoadedThemes().includes(highlighterTheme)) { - const themeImportFn = bundledThemes[highlighterTheme] - if (themeImportFn) { - await highlighter.loadTheme(await themeImportFn()) - } - } - - if (!highlighter.getLoadedLanguages().includes(mappedLanguage)) { - const languageImportFn = bundledLanguages[mappedLanguage] - if (languageImportFn) { - await highlighter.loadLanguage(await languageImportFn()) - } - } + await loadThemeIfNeeded(highlighter, highlighterTheme) + await loadLanguageIfNeeded(highlighter, mappedLanguage) // 生成高亮HTML const html = highlighter.codeToHtml(code, { diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index b381fc171d..94460abe12 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -39,7 +39,6 @@ const MessageTools: FC = ({ blocks }) => { return 'Invalid Result' } }, [toolResponse]) - const { renderedMarkdown: styledResult } = useShikiWithMarkdownIt(`\`\`\`json\n${resultString}\n\`\`\``) if (!toolResponse) { return null @@ -59,8 +58,6 @@ const MessageTools: FC = ({ blocks }) => { // Format tool responses for collapse items const getCollapseItems = () => { const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = [] - // Add tool responses - // for (const toolResponse of toolResponses) { const { id, tool, status, response } = toolResponse const isInvoking = status === 'invoking' const isDone = status === 'done' @@ -123,11 +120,10 @@ const MessageTools: FC = ({ blocks }) => { ), children: isDone && result && ( -
+ ) }) - // } return items } @@ -140,7 +136,6 @@ const MessageTools: FC = ({ blocks }) => { switch (parsedResult.content[0]?.type) { case 'text': return {parsedResult.content[0].text} - // TODO: support other types default: return {content} } @@ -174,7 +169,6 @@ const MessageTools: FC = ({ blocks }) => { styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}> {expandedResponse && ( - {/* mode swtich tabs */} = ({ blocks }) => { { key: 'raw', label: t('message.tools.raw'), - children:
+ children: ( + + ) } ]} /> @@ -211,6 +214,19 @@ const MessageTools: FC = ({ blocks }) => { ) } +// New component to handle collapsed content +const CollapsedContent: FC<{ isExpanded: boolean; resultString: string }> = ({ isExpanded, resultString }) => { + const { renderedMarkdown: styledResult } = useShikiWithMarkdownIt( + isExpanded ? `\`\`\`json\n${resultString}\n\`\`\`` : '' + ) + + if (!isExpanded) { + return null + } + + return
+} + const CollapseContainer = styled(Collapse)` margin-top: 10px; margin-bottom: 12px; diff --git a/src/renderer/src/pages/settings/MCPSettings/McpDescription.tsx b/src/renderer/src/pages/settings/MCPSettings/McpDescription.tsx index 352b5489f2..9877e0b989 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpDescription.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpDescription.tsx @@ -1,4 +1,5 @@ import { useTheme } from '@renderer/context/ThemeProvider' +import { runAsyncFunction } from '@renderer/utils' import { getShikiInstance } from '@renderer/utils/shiki' import { Card } from 'antd' import MarkdownIt from 'markdown-it' @@ -30,9 +31,11 @@ const MCPDescription = ({ searchKey }: McpDescriptionProps) => { }, [md, searchKey]) useEffect(() => { - const sk = getShikiInstance(theme) - md.current.use(sk) - getMcpInfo() + runAsyncFunction(async () => { + const sk = await getShikiInstance(theme) + md.current.use(sk) + getMcpInfo() + }) }, [getMcpInfo, theme]) return ( diff --git a/src/renderer/src/utils/highlighter.ts b/src/renderer/src/utils/highlighter.ts new file mode 100644 index 0000000000..6417c06104 --- /dev/null +++ b/src/renderer/src/utils/highlighter.ts @@ -0,0 +1,32 @@ +import { bundledLanguages, bundledThemes, createHighlighter, type Highlighter } from 'shiki' + +let highlighterPromise: Promise | null = null + +export async function getHighlighter() { + if (!highlighterPromise) { + highlighterPromise = createHighlighter({ + langs: ['javascript', 'typescript', 'python', 'java', 'markdown', 'json'], + themes: ['one-light', 'material-theme-darker'] + }) + } + + return await highlighterPromise +} + +export async function loadLanguageIfNeeded(highlighter: Highlighter, language: string) { + if (!highlighter.getLoadedLanguages().includes(language)) { + const languageImportFn = bundledLanguages[language] + if (languageImportFn) { + await highlighter.loadLanguage(await languageImportFn()) + } + } +} + +export async function loadThemeIfNeeded(highlighter: Highlighter, theme: string) { + if (!highlighter.getLoadedThemes().includes(theme)) { + const themeImportFn = bundledThemes[theme] + if (themeImportFn) { + await highlighter.loadTheme(await themeImportFn()) + } + } +} diff --git a/src/renderer/src/utils/shiki.ts b/src/renderer/src/utils/shiki.ts index b0b9470c63..753f7a5729 100644 --- a/src/renderer/src/utils/shiki.ts +++ b/src/renderer/src/utils/shiki.ts @@ -1,9 +1,11 @@ import { useTheme } from '@renderer/context/ThemeProvider' import { ThemeMode } from '@renderer/types' -import { MarkdownItShikiOptions, setupMarkdownIt } from '@shikijs/markdown-it' +import { setupMarkdownIt } from '@shikijs/markdown-it' import MarkdownIt from 'markdown-it' import { useEffect, useRef, useState } from 'react' -import { BuiltinLanguage, BuiltinTheme, bundledLanguages, createHighlighter } from 'shiki' + +import { runAsyncFunction } from '.' +import { getHighlighter } from './highlighter' const defaultOptions = { themes: { @@ -13,19 +15,9 @@ const defaultOptions = { defaultColor: 'light' } -const initHighlighter = async (options: MarkdownItShikiOptions) => { - const themeNames = ('themes' in options ? Object.values(options.themes) : [options.theme]).filter( - Boolean - ) as BuiltinTheme[] - return await createHighlighter({ - themes: themeNames, - langs: options.langs || (Object.keys(bundledLanguages) as BuiltinLanguage[]) - }) -} +export async function getShikiInstance(theme: ThemeMode) { + const highlighter = await getHighlighter() -const highlighter = await initHighlighter(defaultOptions) - -export function getShikiInstance(theme: ThemeMode) { const options = { ...defaultOptions, defaultColor: theme @@ -46,9 +38,11 @@ export function useShikiWithMarkdownIt(content: string) { ) const { theme } = useTheme() useEffect(() => { - const sk = getShikiInstance(theme) - md.current.use(sk) - setRenderedMarkdown(md.current.render(content)) + runAsyncFunction(async () => { + const sk = await getShikiInstance(theme) + md.current.use(sk) + setRenderedMarkdown(md.current.render(content)) + }) }, [content, theme]) return { renderedMarkdown