diff --git a/dashboard/package.json b/dashboard/package.json index 00224a0f5..cb5807cd7 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -14,7 +14,6 @@ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" }, "dependencies": { - "qrcode": "^1.5.4", "@guolao/vue-monaco-editor": "^1.5.4", "@tiptap/starter-kit": "2.1.7", "@tiptap/vue-3": "2.1.7", @@ -25,7 +24,6 @@ "date-fns": "2.30.0", "dompurify": "^3.3.2", "event-source-polyfill": "^1.0.31", - "highlight.js": "^11.11.1", "js-md5": "^0.8.3", "katex": "^0.16.27", "lodash": "4.17.23", @@ -35,6 +33,7 @@ "monaco-editor": "^0.52.2", "pinia": "2.1.6", "pinyin-pro": "^3.26.0", + "qrcode": "^1.5.4", "shiki": "^3.20.0", "stream-markdown": "^0.0.13", "vee-validate": "4.11.3", diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 775b52a2f..ae65710eb 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -42,9 +42,6 @@ importers: event-source-polyfill: specifier: ^1.0.31 version: 1.0.31 - highlight.js: - specifier: ^11.11.1 - version: 11.11.1 js-md5: specifier: ^0.8.3 version: 0.8.3 @@ -540,79 +537,66 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1919,10 +1903,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} - engines: {node: '>=12.0.0'} - hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -4933,8 +4913,6 @@ snapshots: he@1.2.0: {} - highlight.js@11.11.1: {} - hookified@1.15.1: {} html-void-elements@3.0.0: {} diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 22b4a41ef..54785df23 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 248 icons */ +/* Auto-generated MDI subset – 247 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -236,10 +236,6 @@ content: "\F0167"; } -.mdi-code-braces::before { - content: "\F0169"; -} - .mdi-code-json::before { content: "\F0626"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 5ae667944..b5c8cefd7 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 index 55241b83b..74e05e545 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 2c667afb1..b24aeed04 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -678,7 +678,7 @@ import { import { useRoute, useRouter } from "vue-router"; import { useDisplay } from "vuetify"; import axios from "axios"; -import { MarkdownCodeBlockNode, setCustomComponents } from "markstream-vue"; +import { setCustomComponents } from "markstream-vue"; import "markstream-vue/index.css"; import StyledMenu from "@/components/shared/StyledMenu.vue"; import ProviderConfigDialog from "@/components/chat/ProviderConfigDialog.vue"; @@ -696,6 +696,7 @@ import RefsSidebar from "@/components/chat/message_list_comps/RefsSidebar.vue"; import RefNode from "@/components/chat/message_list_comps/RefNode.vue"; import ActionRef from "@/components/chat/message_list_comps/ActionRef.vue"; import MarkdownMessagePart from "@/components/chat/message_list_comps/MarkdownMessagePart.vue"; +import ThemeAwareMarkdownCodeBlock from "@/components/shared/ThemeAwareMarkdownCodeBlock.vue"; import { useSessions, type Session } from "@/composables/useSessions"; import { useMessages, @@ -720,7 +721,7 @@ const props = withDefaults(defineProps<{ chatboxMode?: boolean }>(), { setCustomComponents("chat-message", { ref: RefNode, - code_block: MarkdownCodeBlockNode, + code_block: ThemeAwareMarkdownCodeBlock, }); const route = useRoute(); diff --git a/dashboard/src/components/chat/MessageList.vue b/dashboard/src/components/chat/MessageList.vue index b83fdeef9..d3070f73e 100644 --- a/dashboard/src/components/chat/MessageList.vue +++ b/dashboard/src/components/chat/MessageList.vue @@ -237,7 +237,7 @@ diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 4ac9e1d6e..bfd501ccc 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -9,7 +9,6 @@ import { useCommonStore } from '@/stores/common'; import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue'; import 'markstream-vue/index.css'; import 'katex/dist/katex.min.css'; -import 'highlight.js/styles/github.css'; import { useI18n } from '@/i18n/composables'; import { router } from '@/router'; import { useRoute } from 'vue-router'; diff --git a/dashboard/src/scss/components/_CodeBlockDark.scss b/dashboard/src/scss/components/_CodeBlockDark.scss new file mode 100644 index 000000000..afc4f7ab7 --- /dev/null +++ b/dashboard/src/scss/components/_CodeBlockDark.scss @@ -0,0 +1,19 @@ +.v-theme--PurpleThemeDark { + .shiki.shiki-themes, + .shiki.shiki-themes span { + color: var(--shiki-dark) !important; + } + + .shiki.shiki-themes { + background-color: var(--shiki-dark-bg) !important; + } + + .markstream-vue { + --border: 217.2 32.6% 17.5%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --secondary: 217.2 32.6% 17.5%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + } +} diff --git a/dashboard/src/scss/components/_HljsDark.scss b/dashboard/src/scss/components/_HljsDark.scss deleted file mode 100644 index 1b084ad58..000000000 --- a/dashboard/src/scss/components/_HljsDark.scss +++ /dev/null @@ -1,107 +0,0 @@ -// highlight.js dark mode overrides -// Scoped to the dark Vuetify theme so the default github.css light styles work in light mode. -.v-theme--PurpleThemeDark { - .hljs { - background: transparent; - color: #adbac7; - } - - .hljs-doctag, - .hljs-keyword, - .hljs-meta .hljs-keyword, - .hljs-template-tag, - .hljs-template-variable, - .hljs-type, - .hljs-variable.language_ { - color: #f47067; - } - - .hljs-title, - .hljs-title.class_, - .hljs-title.class_.inherited__, - .hljs-title.function_ { - color: #dcbdfb; - } - - .hljs-attr, - .hljs-attribute, - .hljs-literal, - .hljs-meta, - .hljs-number, - .hljs-operator, - .hljs-selector-attr, - .hljs-selector-class, - .hljs-selector-id, - .hljs-variable { - color: #6cb6ff; - } - - .hljs-meta .hljs-string, - .hljs-regexp, - .hljs-string { - color: #96d0ff; - } - - .hljs-built_in, - .hljs-symbol { - color: #f69d50; - } - - .hljs-code, - .hljs-comment, - .hljs-formula { - color: #768390; - } - - .hljs-name, - .hljs-quote, - .hljs-selector-pseudo, - .hljs-selector-tag { - color: #8ddb8c; - } - - .hljs-subst { - color: #adbac7; - } - - .hljs-section { - color: #316dca; - font-weight: bold; - } - - .hljs-bullet { - color: #eac55f; - } - - .hljs-emphasis { - color: #adbac7; - font-style: italic; - } - - .hljs-strong { - color: #adbac7; - font-weight: bold; - } - - .hljs-addition { - color: #b4f1b4; - background-color: #1b4721; - } - - .hljs-deletion { - color: #ffd8d3; - background-color: #78191b; - } - - // markstream-vue dark mode variables override - // markstream-vue expects a `.dark` ancestor to activate its dark palette, - // but Vuetify uses `.v-theme--PurpleThemeDark` instead. - .markstream-vue { - --border: 217.2 32.6% 17.5%; - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --secondary: 217.2 32.6% 17.5%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - } -} diff --git a/dashboard/src/scss/style.scss b/dashboard/src/scss/style.scss index 6a572c8a3..6e02010ba 100644 --- a/dashboard/src/scss/style.scss +++ b/dashboard/src/scss/style.scss @@ -13,7 +13,7 @@ @import './components/VTextField'; @import './components/VTabs'; @import './components/VScrollbar'; -@import './components/HljsDark'; +@import './components/CodeBlockDark'; @import './pages/dashboards'; diff --git a/dashboard/src/utils/shiki.js b/dashboard/src/utils/shiki.js new file mode 100644 index 000000000..cb9eac828 --- /dev/null +++ b/dashboard/src/utils/shiki.js @@ -0,0 +1,91 @@ +import { getSingletonHighlighter } from "shiki"; + +export const SHIKI_THEMES = { + light: "github-light", + dark: "github-dark", +}; + +let highlighterPromise; + +function normalizeLanguage(language) { + const normalized = (language || "text").trim().split(/\s+/, 1)[0].toLowerCase(); + return normalized || "text"; +} + +export function escapeHtml(value = "") { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +export async function getShikiHighlighter() { + if (!highlighterPromise) { + highlighterPromise = getSingletonHighlighter({ + themes: Object.values(SHIKI_THEMES), + langs: ["text"], + }); + } + + return highlighterPromise; +} + +export async function ensureShikiLanguages(languages = []) { + const highlighter = await getShikiHighlighter(); + const languagesToLoad = [...new Set(languages.map(normalizeLanguage))].filter( + (language) => language !== "text", + ); + + await Promise.all( + languagesToLoad.map((language) => + highlighter.loadLanguage(language).catch((err) => { + console.warn(`Failed to load Shiki language "${language}".`, err); + }), + ), + ); + + return highlighter; +} + +export function renderShikiCode(highlighter, code, language, colorMode = "auto") { + const normalizedLanguage = normalizeLanguage(language); + const options = + colorMode === "dark" + ? { lang: normalizedLanguage, theme: SHIKI_THEMES.dark } + : colorMode === "light" + ? { lang: normalizedLanguage, theme: SHIKI_THEMES.light } + : { lang: normalizedLanguage, themes: SHIKI_THEMES }; + + try { + return highlighter.codeToHtml(code, options); + } catch (err) { + console.warn( + `Failed to render code with Shiki language "${normalizedLanguage}". Falling back to plain text.`, + err, + ); + + const fallbackOptions = + colorMode === "dark" + ? { lang: "text", theme: SHIKI_THEMES.dark } + : colorMode === "light" + ? { lang: "text", theme: SHIKI_THEMES.light } + : { lang: "text", themes: SHIKI_THEMES }; + + return highlighter.codeToHtml(code, fallbackOptions); + } +} + +export function collectMarkdownFenceLanguages(markdownIt, markdown) { + if (!markdown) return []; + + return markdownIt + .parse(markdown, {}) + .filter((token) => token.type === "fence") + .map((token) => normalizeLanguage(token.info)); +} + +export function normalizeShikiLanguage(language) { + return normalizeLanguage(language); +} diff --git a/dashboard/src/views/WelcomePage.vue b/dashboard/src/views/WelcomePage.vue index c6c497819..ce724f92b 100644 --- a/dashboard/src/views/WelcomePage.vue +++ b/dashboard/src/views/WelcomePage.vue @@ -200,7 +200,6 @@ import { useI18n, useModuleI18n } from '@/i18n/composables'; import { useToast } from '@/utils/toast'; import { MarkdownRender } from 'markstream-vue'; import 'markstream-vue/index.css'; -import 'highlight.js/styles/github.css'; type StepState = 'pending' | 'completed' | 'skipped'; type ComputerAccessRuntime = 'local' | 'none';