Merge pull request #56 from guanweiwang/main

feat: ui 修改, bug 修复
This commit is contained in:
Yoko
2025-07-04 18:34:01 +08:00
committed by GitHub
12 changed files with 285 additions and 323 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "monkey-code-ui",
"version": "0.0.0",
"version": "0.6.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,59 +1,123 @@
// @ts-nocheck
import React from 'react';
import MonacoEditor from '@monaco-editor/react';
import { getBaseLanguageId } from '@/utils';
import { useRef, useState, useEffect } from 'react';
const CHAR_WIDTH = 8; // 估算每个字符宽度,实际可根据字体调整
const MIN_WIDTH = 200;
const MAX_WIDTH = 1060;
const MAX_HEIGHT = 420;
const Code = ({
data,
language,
options,
autoHeight = true,
autoWidth = true,
}: {
data: string;
language: string;
options?: any;
autoHeight?: boolean;
autoWidth?: boolean;
}) => {
const editorRef = useRef<any>(null);
const [height, setHeight] = useState(100);
const [width, setWidth] = useState(MAX_WIDTH);
// 动态调整高度和宽度
const updateSize = () => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
// 获取视觉高度(自适应视觉行数)
if (autoHeight) {
const contentHeight = editorRef.current.getContentHeight();
const newHeight = Math.min(contentHeight, MAX_HEIGHT);
setHeight(newHeight);
}
if (autoWidth) {
const lines = model.getLinesContent();
const maxLineLength = lines.reduce(
(max: number, line: string) => Math.max(max, line.length),
0
);
const newWidth = Math.min(
Math.max(maxLineLength * CHAR_WIDTH + 40, MIN_WIDTH),
MAX_WIDTH
);
setWidth(newWidth);
}
};
useEffect(() => {
updateSize();
}, [data]);
// 监听编辑器内容变化和布局变化,动态调整高度
const handleEditorDidMount = (editor: any) => {
editorRef.current = editor;
updateSize();
editor.onDidContentSizeChange(() => {
updateSize();
});
// 隐藏光标
const editorDom = editor.getDomNode();
if (editorDom) {
const style = document.createElement('style');
style.innerHTML = `.monaco-editor .cursor { display: none !important; }`;
editorDom.appendChild(style);
}
};
const Code = () => {
return (
<div style={{ height: 420 }}>
<MonacoEditor
height='100%'
language={getBaseLanguageId(data?.program_language || 'plaintext')}
value={editorValue}
theme='vs-dark'
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
scrollBeyondLastLine: false,
wordWrap: 'on',
lineNumbers: 'on',
glyphMargin: false,
folding: false,
overviewRulerLanes: 0,
guides: {
indentation: true,
highlightActiveIndentation: true,
highlightActiveBracketPair: false,
},
renderLineHighlight: 'none',
cursorStyle: 'line',
cursorBlinking: 'solid',
cursorWidth: 0,
contextmenu: false,
selectionHighlight: false,
selectOnLineNumbers: false,
occurrencesHighlight: 'off',
links: false,
hover: { enabled: false },
codeLens: false,
dragAndDrop: false,
mouseWheelZoom: false,
accessibilitySupport: 'off',
bracketPairColorization: { enabled: false },
matchBrackets: 'never',
}}
onMount={(editor) => {
editorRef.current = editor;
setEditorReady(true);
// 隐藏光标
const editorDom = editor.getDomNode();
if (editorDom) {
const style = document.createElement('style');
style.innerHTML = `.monaco-editor .cursor { display: none !important; }`;
editorDom.appendChild(style);
}
}}
/>
</div>
<MonacoEditor
height={height}
width={width}
language={getBaseLanguageId(language || 'plaintext')}
value={data}
theme='vs-dark'
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
scrollBeyondLastLine: false,
wordWrap: 'on',
glyphMargin: false,
folding: false,
overviewRulerLanes: 0,
guides: {
indentation: true,
highlightActiveIndentation: true,
highlightActiveBracketPair: false,
},
renderLineHighlight: 'none',
cursorStyle: 'line',
cursorBlinking: 'solid',
cursorWidth: 0,
contextmenu: false,
selectionHighlight: false,
selectOnLineNumbers: false,
occurrencesHighlight: 'off',
links: false,
hover: { enabled: false },
codeLens: false,
dragAndDrop: false,
mouseWheelZoom: false,
accessibilitySupport: 'off',
bracketPairColorization: { enabled: false },
matchBrackets: 'never',
lineNumbers: 'on',
verticalScrollbarSize: 0,
horizontalScrollbarSize: 0,
scrollbar: {
vertical: 'hidden',
},
...options,
}}
onMount={handleEditorDidMount}
/>
);
};

View File

@@ -1,20 +1,14 @@
// import { ToolInfo } from '@/api';
import { Icon, message } from '@c-x/ui';
import { Box, Button, IconButton, Stack, useTheme, alpha } from '@mui/material';
import React, { useState, useRef } from 'react';
import { message } from '@c-x/ui';
import { Box, useTheme } from '@mui/material';
import React from 'react';
import ReactMarkdown, { Components } from 'react-markdown';
import SyntaxHighlighter from 'react-syntax-highlighter';
import {
github,
anOldHope,
} from 'react-syntax-highlighter/dist/esm/styles/hljs';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
import { getBaseLanguageId } from '@/utils';
import Diff from './diff';
import { visit } from 'unist-util-visit';
import Code from './code';
interface ExtendedComponents extends Components {
tools?: React.ComponentType<any>;
@@ -38,11 +32,47 @@ export const toolNames = [
'switch_mode',
'new_task',
'fetch_instructions',
'follow_up',
];
// 去掉下划线的标签名用于Markdown渲染
export const toolTagNames = toolNames.map((name) => name.replace(/_/g, ''));
// 提取 <write_to_file> 块,解析 path、content、language支持多个 <content>,每个加唯一 id返回 newText 和 contentMap
export interface WriteToFileContentMap {
[id: string]: {
code: string;
path: string;
language: string;
};
}
export function preprocessWriteToFile(text: string) {
let contentIndex = 0;
const contentMap: WriteToFileContentMap = {};
// 替换所有 <content>...</content> 为 <content id="content-x"></content> 并存入 contentMap
const newText = text.replace(
/<content>([\s\S]*?)(<\/content>|$)/g,
(match, code) => {
const id = `content-${contentIndex++}`;
// 尝试提取 path
let path = '';
let language = '';
// 向前查找最近的 <path> 标签
const pathMatch = text
.slice(0, text.indexOf(match))
.match(/<path>([\s\S]*?)<\/path>/);
if (pathMatch) {
path = pathMatch[1].trim();
language = getBaseLanguageId(path.split('.').pop() || 'plaintext');
}
contentMap[id] = { code: code.trim(), path, language };
return `<content id="${id}"></content>`;
}
);
return { newText, contentMap };
}
// 支持多组 diff 分隔符,容错处理
function parseAndMergeDiffs(diffText: string) {
const diffBlocks: { search: string; replace: string }[] = [];
@@ -195,7 +225,9 @@ const MarkDown = ({
// 预处理 markdown提取 diffMap
const { newMd, diffMap } = preprocessMarkdown(content);
const answer = processContent(newMd);
const { newText, contentMap: writeToFileContentMap } =
preprocessWriteToFile(newMd);
const answerMd = processContent(newText);
if (content.length === 0) return null;
@@ -218,15 +250,23 @@ const MarkDown = ({
tagNames: [
...(defaultSchema.tagNames! as string[]),
'command',
'attemptcompletion',
...toolTagNames,
'diff',
'suggest',
'content',
],
},
],
]}
components={
{
followup: (props: any) => {
return <ul>{props.children}</ul>;
},
suggest: (props: any) => {
return <li>{props.children}</li>;
},
diff: (props: any) => {
const { node } = props;
// 去掉 user-content- 前缀
@@ -294,20 +334,15 @@ const MarkDown = ({
},
command: ({ children }: React.HTMLAttributes<HTMLElement>) => {
return (
<SyntaxHighlighter
<Code
data={String(children).replace(/\n$/, '')}
language={'shell'}
style={github}
onClick={() => {
if (navigator.clipboard) {
navigator.clipboard.writeText(
String(children).replace(/\n$/, '')
);
message.success('复制成功');
}
options={{
lineNumbers: 'off',
}}
>
{String(children)}
</SyntaxHighlighter>
autoHeight
autoWidth
/>
);
},
attemptcompletion: (props: React.HTMLAttributes<HTMLElement>) => {
@@ -326,6 +361,7 @@ const MarkDown = ({
</div>
);
},
code({
children,
className,
@@ -333,22 +369,15 @@ const MarkDown = ({
}: React.HTMLAttributes<HTMLElement>) {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter
showLineNumbers
{...rest}
language={match[1] || 'bash'}
style={anOldHope}
onClick={() => {
if (navigator.clipboard) {
navigator.clipboard.writeText(
String(children).replace(/\n$/, '')
);
message.success('复制成功');
}
<Code
data={String(children).replace(/\n$/, '')}
language={match?.[1] || 'plaintext'}
autoHeight
autoWidth
options={{
lineNumbers: 'off',
}}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
/>
) : (
<code
{...rest}
@@ -364,10 +393,26 @@ const MarkDown = ({
</code>
);
},
content: (props: any) => {
const id = props.node?.properties?.id?.replace(
/^user-content-/,
''
);
const block = id ? writeToFileContentMap[id] : undefined;
if (!block) return null;
return (
<Code
data={block.code}
language={block.language || 'text'}
autoHeight
autoWidth
/>
);
},
} as ExtendedComponents
}
>
{answer}
{answerMd}
</ReactMarkdown>
</Box>
);

View File

@@ -44,9 +44,7 @@ const Version = () => {
},
}}
onClick={() => {
window.open(
'https://pandawiki.docs.baizhi.cloud/node/01971615-05b8-7924-9af7-15f73784f893'
);
window.open('https://monkeycode.docs.baizhi.cloud/welcome');
}}
>
<Stack direction={'row'} alignItems={'center'} gap={0.5}>

View File

@@ -26,9 +26,9 @@ const User = ({
<Stack direction={'row'} alignItems={'center'} gap={1}>
<Avatar
name={username}
sx={{ width: 22, height: 22, fontSize: 14 }}
sx={{ width: 20, height: 20, fontSize: 12 }}
/>
<Typography>{username}</Typography>
<Typography sx={{ pt: '2px' }}>{username}</Typography>
</Stack>
</Link>
{email && (

View File

@@ -190,7 +190,7 @@ const AdminTable = () => {
title: '最近活跃时间',
dataIndex: 'last_active_at',
render: (text) => {
return dayjs(text).format('YYYY-MM-DD HH:mm:ss');
return text === 0 ? '从未使用' : dayjs.unix(text).fromNow();
},
},
{

View File

@@ -98,7 +98,12 @@ const ChatDetailModal = ({
-{data?.user?.username}
</Ellipsis>
}
width={1200}
sx={{
'.MuiDialog-paper': {
maxWidth: 1300,
},
}}
width={1300}
open={open}
onCancel={onClose}
footer={null}

View File

@@ -5,16 +5,16 @@ import MonacoEditor from '@monaco-editor/react';
import { useEffect, useState, useRef } from 'react';
import { DomainCompletionRecord } from '@/api/types';
import { getBaseLanguageId } from '@/utils';
function getBaseLanguageId(languageId: string): string {
const map: Record<string, string> = {
typescriptreact: 'typescript',
javascriptreact: 'javascript',
tailwindcss: 'css',
'vue-html': 'vue',
};
return map[languageId] || languageId;
}
// 删除 <|im_start|> 和 <|im_end|> 及其间内容的工具函数
const removeImBlocks = (text: string) => {
// 匹配前后可能的换行符
return text.replace(
/(^[ \t]*\r?\n)?<\|im_start\|>[\s\S]*?<\|im_end\|>(\r?\n)?/g,
''
);
};
const ChatDetailModal = ({
data,
@@ -33,7 +33,8 @@ const ChatDetailModal = ({
const getChatDetailModal = () => {
if (!data) return;
getCompletionInfo({ id: data.id! }).then((res) => {
const rawPrompt = res.prompt || '';
// 先去除 <|im_start|> 和 <|im_end|> 及其间内容
const rawPrompt = removeImBlocks(res.prompt || '');
const content = res.content || '';
// 找到三个特殊标记的位置
const prefixTag = '<|fim_prefix|>';

View File

@@ -82,15 +82,16 @@ const Completion = () => {
is_accept?: 'accepted' | 'unaccepted' | '';
}) => {
setLoading(true);
const isAccept = params.is_accept || filterAccept;
const res = await getListCompletionRecord({
page: params.page || page,
size: params.size || size,
language: params.language || filterLang,
author: params.author || filterUser,
is_accept:
params.is_accept === 'accepted'
isAccept === 'accepted'
? true
: params.is_accept === 'unaccepted'
: isAccept === 'unaccepted'
? false
: undefined,
});

View File

@@ -17,8 +17,8 @@ const ContributionModal = ({
open={open}
onCancel={onCancel}
title='用户贡献榜'
showCancel={false}
okText='关闭'
footer={false}
onOk={onCancel}
>
<Box
gap={0.5}

View File

@@ -1,10 +1,3 @@
// import {
// addModel,
// getModelByProviderBrand,
// ModelItem,
// testModel,
// updateModel,
// } from '@/api';
import {
postCreateModel,
putUpdateModel,
@@ -14,9 +7,9 @@ import {
import Card from '@/components/card';
import { ModelProvider } from '../constant';
import { Icon, message, Modal } from '@c-x/ui';
import { StyledFormLabel } from '@/components/form';
import {
Box,
Button,
MenuItem,
Stack,
TextField,
@@ -26,7 +19,6 @@ import {
import { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import {
DomainModelBasic,
ConstsModelType,
DomainUpdateModelReq,
DomainCreateModelReq,
@@ -51,7 +43,6 @@ const ModelModal = ({
modelType,
}: AddModelProps) => {
const theme = useTheme();
const spaceId = 1;
const {
formState: { errors },
handleSubmit,
@@ -64,10 +55,7 @@ const ModelModal = ({
provider: data?.provider || 'DeepSeek',
api_base: data?.api_base || ModelProvider.DeepSeek.defaultBaseUrl,
model_name: data?.model_name || '',
// api_version: data?.api_version || '',
api_key: data?.api_key || '',
// api_header_key: data?.api_header?.split('=')[0] || '',
// api_header_value: data?.api_header?.split('=')[1] || '',
},
});
@@ -78,34 +66,6 @@ const ModelModal = ({
const [loading, setLoading] = useState(false);
const [modelLoading, setModelLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
// const getModel = (value: AddModelForm) => {
// let header = '';
// if (value.api_header_key && value.api_header_value) {
// header = value.api_header_key + '=' + value.api_header_value;
// }
// setModelLoading(true);
// getModelByProviderBrand({
// space_id: spaceId,
// api_key: value.api_key,
// base_url: value.base_url,
// provider_brand: value.provider_brand,
// api_header: header,
// })
// .then((res) => {
// setModelUserList(res.models || []);
// if (data && (res.models || []).find((it) => it.model === data.model)) {
// setValue('model', data.model);
// } else {
// setValue('model', res.models?.[0]?.model || '');
// }
// setSuccess(true);
// })
// .finally(() => {
// setModelLoading(false);
// });
// };
const onCreateModel = (value: DomainCreateModelReq) => {
return postCreateModel({
@@ -131,10 +91,6 @@ const ModelModal = ({
};
const onSubmit = (value: Required<DomainCreateModelReq>) => {
const header = '';
// if (value.api_header_key && value.api_header_value) {
// header = value.api_header_key + '=' + value.api_header_value;
// }
setError('');
setLoading(true);
postCheckModel({
@@ -162,26 +118,12 @@ const ModelModal = ({
useEffect(() => {
if (open) {
if (data) {
if (data.provider_brand && data.provider_brand !== 'Other') {
// getModel({
// api_key: data.api_key || '',
// base_url: data.base_url || '',
// model: data.model || '',
// provider_brand: data.provider_brand || '',
// api_version: data.api_version || '',
// api_header_key: data.api_header?.split('=')[0] || '',
// api_header_value: data.api_header?.split('=')[1] || '',
// });
}
reset(
{
provider: data.provider || 'Other',
model_name: data.model_name || '',
api_base: data.api_base || '',
api_key: data.api_key || '',
// api_version: data.api_version || '',
// api_header_key: data.api_header?.split('=')[0] || '',
// api_header_value: data.api_header?.split('=')[1] || '',
},
{
keepDefaultValues: true,
@@ -213,10 +155,15 @@ const ModelModal = ({
useEffect(() => {
if (currentModelList.length > 0) {
setValue('api_base', currentModelList[0].api_base || '');
setValue('model_name', currentModelList[0].name || '');
if (data) {
setValue('api_base', data.api_base || '');
setValue('model_name', data.model_name || '');
} else {
setValue('api_base', currentModelList[0].api_base || '');
setValue('model_name', currentModelList[0].name || '');
}
}
}, [currentModelList]);
}, [currentModelList, data]);
return (
<Modal
@@ -226,7 +173,6 @@ const ModelModal = ({
onCancel={() => {
reset();
setModelUserList([]);
setSuccess(false);
setLoading(false);
setError('');
onClose();
@@ -277,17 +223,13 @@ const ModelModal = ({
}}
onClick={() => {
if (data) return;
// setModelUserList([]);
setError('');
reset(
{
provider: it.provider as keyof typeof ModelProvider,
api_base: '',
model_name: '',
// api_version: '',
api_key: '',
// api_header_key: '',
// api_header_value: '',
},
{
keepDefaultValues: true,
@@ -306,12 +248,7 @@ const ModelModal = ({
))}
</Stack>
<Box sx={{ flex: 1 }}>
<Box sx={{ fontSize: 14, lineHeight: '32px' }}>
API {' '}
<Box component={'span'} sx={{ color: 'red' }}>
*
</Box>
</Box>
<StyledFormLabel required>API </StyledFormLabel>
<Controller
control={control}
name='api_base'
@@ -344,15 +281,11 @@ const ModelModal = ({
justifyContent={'space-between'}
sx={{ fontSize: 14, lineHeight: '32px', mt: 2 }}
>
<Box>
<StyledFormLabel
required={ModelProvider[providerBrand].secretRequired}
>
API Secret
{ModelProvider[providerBrand].secretRequired && (
<Box component={'span'} sx={{ color: 'red' }}>
{' '}
*
</Box>
)}
</Box>
</StyledFormLabel>
{ModelProvider[providerBrand].modelDocumentUrl && (
<Box
component={'span'}
@@ -396,131 +329,32 @@ const ModelModal = ({
/>
)}
/>
{/* {providerBrand === 'AzureOpenAI' && (
<>
<Box sx={{ fontSize: 14, lineHeight: '32px', mt: 2 }}>
API Version
</Box>
<Controller
control={control}
name='api_version'
render={({ field }) => (
<TextField
{...field}
fullWidth
size='small'
placeholder='2024-10-21'
error={!!errors.api_version}
helperText={errors.api_version?.message}
onChange={(e) => {
field.onChange(e.target.value);
setModelUserList([]);
setValue('model', '');
setSuccess(false);
}}
/>
)}
/>
</>
)} */}
{providerBrand === 'Other' ? (
<>
<Box sx={{ fontSize: 14, lineHeight: '32px', mt: 2 }}>
{' '}
<Box component={'span'} sx={{ color: 'red' }}>
*
</Box>
</Box>
<Controller
control={control}
name='model_name'
rules={{
required: '模型名称不能为空',
}}
render={({ field }) => (
<TextField
{...field}
fullWidth
size='small'
placeholder=''
error={!!errors.model_name}
helperText={errors.model_name?.message}
/>
)}
/>
<Box sx={{ fontSize: 12, color: 'error.main', mt: 1 }}>
便
</Box>
</>
) : (
<>
<Box sx={{ fontSize: 14, lineHeight: '32px', mt: 2 }}>
{' '}
<Box component={'span'} sx={{ color: 'red' }}>
*
</Box>
</Box>
<Controller
control={control}
name='model_name'
render={({ field }) => (
<TextField
{...field}
fullWidth
select
size='small'
placeholder=''
error={!!errors.model_name}
helperText={errors.model_name?.message}
>
{currentModelList.map((it) => (
<MenuItem key={it.name} value={it.name}>
{it.name}
</MenuItem>
))}
</TextField>
)}
/>
{/* {ModelProvider[providerBrand].customHeader && (
<>
<Box sx={{ fontSize: 14, lineHeight: '32px', mt: 2 }}>
Header
</Box>
<Stack direction={'row'} gap={1}>
<Controller
control={control}
name='api_header_key'
render={({ field }) => (
<TextField
{...field}
fullWidth
size='small'
placeholder='key'
error={!!errors.api_header_key}
helperText={errors.api_header_key?.message}
/>
)}
/>
<Box sx={{ fontSize: 14, lineHeight: '36px' }}>=</Box>
<Controller
control={control}
name='api_header_value'
render={({ field }) => (
<TextField
{...field}
fullWidth
size='small'
placeholder='value'
error={!!errors.api_header_value}
helperText={errors.api_header_value?.message}
/>
)}
/>
</Stack>
</>
)} */}
</>
)}
<Box sx={{ mt: 2 }}>
<StyledFormLabel required></StyledFormLabel>
</Box>
<Controller
control={control}
name='model_name'
render={({ field }) => (
<TextField
{...field}
fullWidth
select
size='small'
placeholder=''
error={!!errors.model_name}
helperText={errors.model_name?.message}
>
{currentModelList.map((it) => (
<MenuItem key={it.name} value={it.name}>
{it.name}
</MenuItem>
))}
</TextField>
)}
/>
{error && (
<Card
sx={{

View File

@@ -246,3 +246,17 @@ export const getRecent24HoursData = (
}
return { xData, yData };
};
export const getBaseLanguageId = (languageId: string): string => {
const map: Record<string, string> = {
typescriptreact: 'typescript',
javascriptreact: 'javascript',
tailwindcss: 'css',
shellscript: 'shell',
'vue-html': 'vue',
tsx: 'typescript',
jsx: 'javascript',
py: 'python',
};
return map[languageId] || languageId;
};