From 8aca5071a907234ba23d86a6f5f301b91acc9891 Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Mon, 7 Jul 2025 17:02:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20shell=20?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/package.json | 1 + ui/pnpm-lock.yaml | 121 ++++++++++++++++++ ui/src/components/markDown/code.tsx | 4 +- ui/src/components/markDown/command.tsx | 24 ++++ ui/src/components/markDown/index.tsx | 34 ++--- ui/src/components/sidebar/version.tsx | 2 +- ui/src/main.tsx | 1 - ui/src/pages/chat/chatDetailModal.tsx | 73 ++++++----- ui/src/pages/chat/index.tsx | 2 +- ui/src/pages/completion/index.tsx | 36 ++---- .../dashboard/components/globalStatistic.tsx | 1 - ui/src/pages/dashboard/index.tsx | 15 +-- 12 files changed, 212 insertions(+), 102 deletions(-) create mode 100644 ui/src/components/markDown/command.tsx diff --git a/ui/package.json b/ui/package.json index dc177f9..a564cf5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -50,6 +50,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", + "shiki": "^3.7.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5" diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 806e1bc..398e44c 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: globals: specifier: ^16.0.0 version: 16.2.0 + shiki: + specifier: ^3.7.0 + version: 3.7.0 typescript: specifier: ~5.8.3 version: 5.8.3 @@ -825,6 +828,27 @@ packages: cpu: [x64] os: [win32] + '@shikijs/core@3.7.0': + resolution: {integrity: sha512-yilc0S9HvTPyahHpcum8eonYrQtmGTU0lbtwxhA6jHv4Bm1cAdlPFRCJX4AHebkCm75aKTjjRAW+DezqD1b/cg==} + + '@shikijs/engine-javascript@3.7.0': + resolution: {integrity: sha512-0t17s03Cbv+ZcUvv+y33GtX75WBLQELgNdVghnsdhTgU3hVcWcMsoP6Lb0nDTl95ZJfbP1mVMO0p3byVh3uuzA==} + + '@shikijs/engine-oniguruma@3.7.0': + resolution: {integrity: sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==} + + '@shikijs/langs@3.7.0': + resolution: {integrity: sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==} + + '@shikijs/themes@3.7.0': + resolution: {integrity: sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==} + + '@shikijs/types@3.7.0': + resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1492,6 +1516,9 @@ packages: hast-util-sanitize@5.0.2: resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -1921,6 +1948,12 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2114,6 +2147,15 @@ packages: reftools@1.1.9: resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -2190,6 +2232,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.7.0: + resolution: {integrity: sha512-ZcI4UT9n6N2pDuM2n3Jbk0sR4Swzq43nLPgS/4h0E3B/NrFn2HKElrDtceSf8Zx/OWYOo7G1SAtBLypCp+YXqg==} + should-equal@2.0.0: resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} @@ -3074,6 +3119,39 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.0': optional: true + '@shikijs/core@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + + '@shikijs/themes@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + + '@shikijs/types@3.7.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.27.5 @@ -3827,6 +3905,20 @@ snapshots: '@ungap/structured-clone': 1.3.0 unist-util-position: 5.0.0 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -4465,6 +4557,14 @@ snapshots: ohash@2.0.11: {} + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4673,6 +4773,16 @@ snapshots: reftools@1.1.9: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 @@ -4784,6 +4894,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@3.7.0: + dependencies: + '@shikijs/core': 3.7.0 + '@shikijs/engine-javascript': 3.7.0 + '@shikijs/engine-oniguruma': 3.7.0 + '@shikijs/langs': 3.7.0 + '@shikijs/themes': 3.7.0 + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + should-equal@2.0.0: dependencies: should-type: 1.4.0 diff --git a/ui/src/components/markDown/code.tsx b/ui/src/components/markDown/code.tsx index 7a70b4d..8df87c7 100644 --- a/ui/src/components/markDown/code.tsx +++ b/ui/src/components/markDown/code.tsx @@ -4,7 +4,7 @@ import { useRef, useState, useEffect } from 'react'; const CHAR_WIDTH = 8; // 估算每个字符宽度,实际可根据字体调整 const MIN_WIDTH = 200; -const MAX_WIDTH = 1060; +const MAX_WIDTH = 960; const MAX_HEIGHT = 420; const Code = ({ @@ -21,7 +21,7 @@ const Code = ({ autoWidth?: boolean; }) => { const editorRef = useRef(null); - const [height, setHeight] = useState(100); + const [height, setHeight] = useState(420); const [width, setWidth] = useState(MAX_WIDTH); // 动态调整高度和宽度 diff --git a/ui/src/components/markDown/command.tsx b/ui/src/components/markDown/command.tsx new file mode 100644 index 0000000..ab4c832 --- /dev/null +++ b/ui/src/components/markDown/command.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; +import { codeToHtml } from 'shiki'; + +const Command = ({ + children, + theme = 'material-theme-darker', + lang = 'shell', +}: { + children: React.ReactNode; + theme?: string; + lang?: string; +}) => { + const [html, setHtml] = useState(''); + useEffect(() => { + codeToHtml(String(children).replace(/\n$/, ''), { + lang, + theme, + }).then((html) => setHtml(html)); + }, [children, theme, lang]); + + return
; +}; + +export default Command; diff --git a/ui/src/components/markDown/index.tsx b/ui/src/components/markDown/index.tsx index 21a6658..7536f92 100644 --- a/ui/src/components/markDown/index.tsx +++ b/ui/src/components/markDown/index.tsx @@ -6,9 +6,11 @@ import rehypeRaw from 'rehype-raw'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import remarkBreaks from 'remark-breaks'; import remarkGfm from 'remark-gfm'; + import { getBaseLanguageId } from '@/utils'; import Diff from './diff'; import Code from './code'; +import Command from './command'; interface ExtendedComponents extends Components { tools?: React.ComponentType; @@ -332,18 +334,10 @@ const MarkDown = ({ /> ); }, - command: ({ children }: React.HTMLAttributes) => { - return ( - - ); + command: async ({ + children, + }: React.HTMLAttributes) => { + return {children}; }, attemptcompletion: (props: React.HTMLAttributes) => { return ( @@ -369,15 +363,9 @@ const MarkDown = ({ }: React.HTMLAttributes) { const match = /language-(\w+)/.exec(className || ''); return match ? ( - + + {String(children).replace(/\n$/, '')} + ) : ( ); }, diff --git a/ui/src/components/sidebar/version.tsx b/ui/src/components/sidebar/version.tsx index 4463bfe..cc4c046 100644 --- a/ui/src/components/sidebar/version.tsx +++ b/ui/src/components/sidebar/version.tsx @@ -24,7 +24,7 @@ const Version = () => { }); }, []); - if (latestVersion === undefined) return null; + // if (latestVersion === undefined) return null; return ( ({ - background: '#f7f8fa', borderRadius: 4, padding: 24, minHeight: 400, @@ -22,18 +19,33 @@ const StyledChatList = styled('div')(() => ({ const StyledChatRow = styled('div', { shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser, theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: isUser ? 'flex-end' : 'flex-start', + gap: theme.spacing(1), + marginBottom: theme.spacing(2), +})); + +const StyledChatUser = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', })<{ isUser: boolean }>(({ isUser }) => ({ display: 'flex', flexDirection: isUser ? 'row-reverse' : 'row', - alignItems: 'flex-start', - marginBottom: 28, + alignItems: 'center', position: 'relative', })); +const StyledChatName = styled('div')(({ theme }) => ({ + color: theme.vars.palette.text.primary, + fontSize: '14px', + fontWeight: 500, +})); + const StyledChatAvatar = styled('div', { shouldForwardProp: (prop) => prop !== 'isUser', })<{ isUser: boolean }>(({ isUser }) => ({ - margin: isUser ? '0 0 0 18px' : '0 18px 0 0', + margin: isUser ? '0 0 0 12px' : '0 12px 0 0', display: 'flex', alignItems: 'flex-start', position: 'relative', @@ -44,18 +56,13 @@ const StyledChatBubble = styled('div', { shouldForwardProp: (prop) => prop !== 'isUser', })<{ isUser: boolean }>(({ isUser }) => ({ background: isUser ? '#e6f7ff' : '#f5f5f5', - borderRadius: 18, - boxShadow: '0 2px 8px rgba(0,0,0,0.06)', - padding: '16px 20px', + margin: isUser ? '0 36px 0 0' : '0 0 0 36px', + borderRadius: 12, + padding: '8px 12px', minHeight: 36, - maxWidth: 1100, + maxWidth: 1040, wordBreak: 'break-word', position: 'relative', - transition: 'box-shadow 0.2s', - cursor: 'pointer', - '&:hover': { - boxShadow: '0 4px 16px rgba(0,0,0,0.12)', - }, })); const ChatDetailModal = ({ @@ -68,9 +75,6 @@ const ChatDetailModal = ({ onClose: () => void; }) => { const [content, setContent] = useState([]); - const [showToolInfo, setShowToolInfo] = useState<{ [key: string]: ToolInfo }>( - {} - ); const getChatDetailModal = () => { if (!data) return; @@ -103,7 +107,7 @@ const ChatDetailModal = ({ maxWidth: 1300, }, }} - width={1300} + width={1200} open={open} onCancel={onClose} footer={null} @@ -112,21 +116,24 @@ const ChatDetailModal = ({ {content.map((item, idx) => { const isUser = item.role === 'user'; - const name = isUser ? data?.user?.username : 'AI'; + const name = isUser ? data?.user?.username : 'MonkeyCode'; const msg = item.content || ''; return ( - - - + + + + + {name} + diff --git a/ui/src/pages/chat/index.tsx b/ui/src/pages/chat/index.tsx index a3d04ff..5cadcb3 100644 --- a/ui/src/pages/chat/index.tsx +++ b/ui/src/pages/chat/index.tsx @@ -135,7 +135,7 @@ const Chat = () => { return ( void, delay: number) { - const timer = useRef(null); - const fnRef = useRef(fn); - fnRef.current = fn; - return useCallback( - (...args: any[]) => { - if (timer.current) clearTimeout(timer.current); - timer.current = setTimeout(() => { - fnRef.current(...args); - }, delay); - }, - [delay] - ); -} - const Completion = () => { const [page, setPage] = useState(1); const [size, setSize] = useState(20); @@ -60,7 +44,7 @@ const Completion = () => { const { data: userOptions = { users: [] } } = useRequest(() => getListUser({ page: 1, - size: 99999, + size: 10, }) ); @@ -172,16 +156,16 @@ const Completion = () => { }, ]; - const debounceSetFilterLang = useDebounce( + const debounceSetFilterLang = useDebounceFn( (val: string) => setFilterLang(val), - 500 + { + wait: 500, + } ); return ( - {/* 筛选项 */} - {/* 成员筛选 Autocomplete */} { renderInput={(params) => } clearOnEscape /> - {/* 语言筛选 Autocomplete */} { setFilterLang(newValue ? String(newValue) : ''); }} onInputChange={(_, newInputValue) => - debounceSetFilterLang(newInputValue) + debounceSetFilterLang.run(newInputValue) } - popupIcon={} renderInput={(params) => } clearOnEscape /> - {/* 是否采纳筛选 Select 保持不变 */} 是否采纳