Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "monkey-code-ui",
"version": "0.0.0",
"version": "0.6.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
172 changes: 118 additions & 54 deletions ui/src/components/markDown/code.tsx
Original file line number Diff line number Diff line change
@@ -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}
/>
);
};

Expand Down
127 changes: 86 additions & 41 deletions ui/src/components/markDown/index.tsx
Original file line number Diff line number Diff line change
@@ -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>;
Expand All @@ -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 }[] = [];
Expand Down Expand Up @@ -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;

Expand All @@ -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- 前缀
Expand Down Expand Up @@ -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>) => {
Expand All @@ -326,29 +361,23 @@ const MarkDown = ({
</div>
);
},

code({
children,
className,
...rest
}: 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}
Expand All @@ -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>
);
Expand Down
Loading