Merge pull request #61 from guanweiwang/develop

pref: 优化 code 展示,修复 版本问题
This commit is contained in:
safe1ine
2025-07-07 19:11:19 +08:00
committed by GitHub
12 changed files with 218 additions and 110 deletions

View File

@@ -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"

121
ui/pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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<any>(null);
const [height, setHeight] = useState(100);
const [height, setHeight] = useState(420);
const [width, setWidth] = useState(MAX_WIDTH);
// 动态调整高度和宽度

View File

@@ -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<string>('');
useEffect(() => {
codeToHtml(String(children).replace(/\n$/, ''), {
lang,
theme,
}).then((html) => setHtml(html));
}, [children, theme, lang]);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};
export default Command;

View File

@@ -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<any>;
@@ -332,18 +334,10 @@ const MarkDown = ({
/>
);
},
command: ({ children }: React.HTMLAttributes<HTMLElement>) => {
return (
<Code
data={String(children).replace(/\n$/, '')}
language={'shell'}
options={{
lineNumbers: 'off',
}}
autoHeight
autoWidth
/>
);
command: async ({
children,
}: React.HTMLAttributes<HTMLElement>) => {
return <Command lang='shell'>{children}</Command>;
},
attemptcompletion: (props: React.HTMLAttributes<HTMLElement>) => {
return (
@@ -369,15 +363,7 @@ const MarkDown = ({
}: React.HTMLAttributes<HTMLElement>) {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<Code
data={String(children).replace(/\n$/, '')}
language={match?.[1] || 'plaintext'}
autoHeight
autoWidth
options={{
lineNumbers: 'off',
}}
/>
<Command>{String(children).replace(/\n$/, '')}</Command>
) : (
<code
{...rest}
@@ -404,8 +390,8 @@ const MarkDown = ({
<Code
data={block.code}
language={block.language || 'text'}
autoHeight
autoWidth
autoHeight={false}
autoWidth={false}
/>
);
},

View File

@@ -7,7 +7,8 @@ import { useEffect, useState } from 'react';
import packageJson from '../../../package.json';
const Version = () => {
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
const curVersion =
import.meta.env.VITE_APP_VERSION || `v${packageJson.version}`;
const [latestVersion, setLatestVersion] = useState<string | undefined>(
undefined
);
@@ -24,7 +25,7 @@ const Version = () => {
});
}, []);
if (latestVersion === undefined) return null;
// if (latestVersion === undefined) return null;
return (
<Stack
@@ -53,7 +54,7 @@ const Version = () => {
</Stack>
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
<Box sx={{ whiteSpace: 'nowrap' }}>{curVersion}</Box>
{latestVersion !== `v${curVersion}` && (
{latestVersion !== `${curVersion}` && (
<Tooltip
placement='top'
arrow

View File

@@ -11,7 +11,6 @@ import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { lightTheme } from './theme';
import router from './router';
dayjs.locale('zh-cn');

View File

@@ -2,17 +2,14 @@ import Avatar from '@/components/avatar';
import Card from '@/components/card';
import { getChatInfo } from '@/api/Billing';
import MarkDown from '@/components/markDown';
import { Ellipsis, Icon, Modal } from '@c-x/ui';
import { styled } from '@mui/material/styles';
import { Ellipsis, Modal } from '@c-x/ui';
import { styled } from '@mui/material';
import logo from '@/assets/images/logo.png';
import { useEffect, useState } from 'react';
import { DomainChatContent, DomainChatRecord } from '@/api/types';
type ToolInfo = any;
const StyledChatList = styled('div')(() => ({
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<DomainChatContent[]>([]);
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 = ({
<StyledChatList>
{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 (
<StyledChatRow key={idx} isUser={isUser}>
<StyledChatAvatar isUser={isUser}>
<Avatar
name={isUser ? name : undefined}
src={isUser ? undefined : logo}
sx={{
width: 48,
height: 48,
fontSize: 22,
}}
/>
</StyledChatAvatar>
<StyledChatUser key={idx} isUser={isUser}>
<StyledChatAvatar isUser={isUser}>
<Avatar
name={isUser ? name : undefined}
src={isUser ? undefined : logo}
sx={{
width: 24,
height: 24,
fontSize: 16,
}}
/>
</StyledChatAvatar>
<StyledChatName>{name}</StyledChatName>
</StyledChatUser>
<StyledChatBubble isUser={isUser}>
<MarkDown content={msg} />
</StyledChatBubble>

View File

@@ -135,7 +135,7 @@ const Chat = () => {
return (
<Card sx={{ flex: 1, height: '100%' }}>
<Table
height='calc(100%)'
height='100%'
sx={{ mx: -2 }}
PaginationProps={{
sx: {

View File

@@ -1,11 +1,11 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect } from 'react';
import { DomainCompletionRecord, DomainUser } from '@/api/types';
import { getListCompletionRecord } from '@/api/Billing';
import { useRequest } from 'ahooks';
import { Table } from '@c-x/ui';
import Card from '@/components/card';
import {
ButtonBase,
Box,
Stack,
MenuItem,
Select,
@@ -16,30 +16,14 @@ import {
} from '@mui/material';
import { getListUser } from '@/api/User';
import dayjs from 'dayjs';
import { useDebounceFn } from 'ahooks';
import { ColumnsType } from '@c-x/ui/dist/Table';
import { addCommasToNumber } from '@/utils';
import CompletionDetailModal from './completionDetailModal';
import StyledLabel from '@/components/label';
import { LANG_OPTIONS } from './constant';
import { ArrowDropDown as ArrowDropDownIcon } from '@mui/icons-material';
import User from '@/components/user';
// 防抖 hook
function useDebounce(fn: (...args: any[]) => void, delay: number) {
const timer = useRef<number | null>(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,
})
);
@@ -120,13 +104,12 @@ const Completion = () => {
width: 150,
render(_, record) {
return (
<ButtonBase
disableRipple
<Box
onClick={() => setCompletionDetailModal(record)}
sx={{ color: 'info.main' }}
sx={{ color: 'info.main', cursor: 'pointer' }}
>
</ButtonBase>
</Box>
);
},
},
@@ -172,16 +155,16 @@ const Completion = () => {
},
];
const debounceSetFilterLang = useDebounce(
const debounceSetFilterLang = useDebounceFn(
(val: string) => setFilterLang(val),
500
{
wait: 500,
}
);
return (
<Card sx={{ flex: 1, height: '100%' }}>
{/* 筛选项 */}
<Stack direction='row' spacing={2} sx={{ mb: 2 }}>
{/* 成员筛选 Autocomplete */}
<Autocomplete
size='small'
sx={{ minWidth: 220 }}
@@ -200,7 +183,6 @@ const Completion = () => {
renderInput={(params) => <TextField {...params} label='成员' />}
clearOnEscape
/>
{/* 语言筛选 Autocomplete */}
<Autocomplete
size='small'
sx={{ minWidth: 220 }}
@@ -212,13 +194,11 @@ const Completion = () => {
setFilterLang(newValue ? String(newValue) : '');
}}
onInputChange={(_, newInputValue) =>
debounceSetFilterLang(newInputValue)
debounceSetFilterLang.run(newInputValue)
}
popupIcon={<ArrowDropDownIcon />}
renderInput={(params) => <TextField {...params} label='语言' />}
clearOnEscape
/>
{/* 是否采纳筛选 Select 保持不变 */}
<FormControl size='small' sx={{ minWidth: 180 }}>
<InputLabel></InputLabel>
<Select
@@ -262,6 +242,7 @@ const Completion = () => {
},
}}
/>
<CompletionDetailModal
open={!!completionDetailModal}
onClose={() => setCompletionDetailModal(undefined)}

View File

@@ -110,7 +110,6 @@ const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => {
container
spacing={2}
sx={{
height: '100%',
overflow: 'auto',
borderRadius: 2.5,
}}

View File

@@ -1,19 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { getListUser } from '@/api/User';
import {
Stack,
TextField,
MenuItem,
InputAdornment,
IconButton,
Select,
FormControl,
InputLabel,
} from '@mui/material';
import dayjs from 'dayjs';
import { Stack, MenuItem, Select } from '@mui/material';
import { CusTabs } from '@c-x/ui';
import GlobalStatistic from './components/globalStatistic';
import CancelRoundedIcon from '@mui/icons-material/CancelRounded';
import { useRequest } from 'ahooks';
import MemberStatistic from './components/memberStatistic';
import { useParams } from 'react-router-dom';
@@ -27,7 +16,7 @@ const Dashboard = () => {
const { tab, id } = useParams();
const [tabValue, setTabValue] = useState(tab || 'global');
const [memberData, setMemberData] = useState<DomainUser | null>(null);
const [timeRange, setTimeRange] = useState<TimeRange>('90d');
const [timeRange, setTimeRange] = useState<TimeRange>('24h');
const { data: userData, refresh } = useRequest(
() =>