diff --git a/ui/package.json b/ui/package.json index 4bf25a6..dc177f9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "monkey-code-ui", - "version": "0.0.0", + "version": "0.6.0", "type": "module", "scripts": { "dev": "vite", diff --git a/ui/src/components/markDown/code.tsx b/ui/src/components/markDown/code.tsx index b1f1987..7a70b4d 100644 --- a/ui/src/components/markDown/code.tsx +++ b/ui/src/components/markDown/code.tsx @@ -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(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 ( -
- { - 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); - } - }} - /> -
+ ); }; diff --git a/ui/src/components/markDown/index.tsx b/ui/src/components/markDown/index.tsx index 3b683a1..21a6658 100644 --- a/ui/src/components/markDown/index.tsx +++ b/ui/src/components/markDown/index.tsx @@ -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; @@ -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, '')); +// 提取 块,解析 path、content、language,支持多个 ,每个加唯一 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 = {}; + // 替换所有 ... 并存入 contentMap + const newText = text.replace( + /([\s\S]*?)(<\/content>|$)/g, + (match, code) => { + const id = `content-${contentIndex++}`; + // 尝试提取 path + let path = ''; + let language = ''; + // 向前查找最近的 标签 + const pathMatch = text + .slice(0, text.indexOf(match)) + .match(/([\s\S]*?)<\/path>/); + if (pathMatch) { + path = pathMatch[1].trim(); + language = getBaseLanguageId(path.split('.').pop() || 'plaintext'); + } + contentMap[id] = { code: code.trim(), path, language }; + return ``; + } + ); + 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
    {props.children}
; + }, + suggest: (props: any) => { + return
  • {props.children}
  • ; + }, + diff: (props: any) => { const { node } = props; // 去掉 user-content- 前缀 @@ -294,20 +334,15 @@ const MarkDown = ({ }, command: ({ children }: React.HTMLAttributes) => { return ( - { - if (navigator.clipboard) { - navigator.clipboard.writeText( - String(children).replace(/\n$/, '') - ); - message.success('复制成功'); - } + options={{ + lineNumbers: 'off', }} - > - {String(children)} - + autoHeight + autoWidth + /> ); }, attemptcompletion: (props: React.HTMLAttributes) => { @@ -326,6 +361,7 @@ const MarkDown = ({ ); }, + code({ children, className, @@ -333,22 +369,15 @@ const MarkDown = ({ }: React.HTMLAttributes) { const match = /language-(\w+)/.exec(className || ''); return match ? ( - { - if (navigator.clipboard) { - navigator.clipboard.writeText( - String(children).replace(/\n$/, '') - ); - message.success('复制成功'); - } + - {String(children).replace(/\n$/, '')} - + /> ) : ( ); }, + content: (props: any) => { + const id = props.node?.properties?.id?.replace( + /^user-content-/, + '' + ); + const block = id ? writeToFileContentMap[id] : undefined; + if (!block) return null; + return ( + + ); + }, } as ExtendedComponents } > - {answer} + {answerMd} ); diff --git a/ui/src/components/sidebar/version.tsx b/ui/src/components/sidebar/version.tsx index 36aa055..4463bfe 100644 --- a/ui/src/components/sidebar/version.tsx +++ b/ui/src/components/sidebar/version.tsx @@ -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'); }} > diff --git a/ui/src/components/user/index.tsx b/ui/src/components/user/index.tsx index ed0e1cb..0b3fa00 100644 --- a/ui/src/components/user/index.tsx +++ b/ui/src/components/user/index.tsx @@ -26,9 +26,9 @@ const User = ({ - {username} + {username} {email && ( diff --git a/ui/src/pages/admin/adminTable.tsx b/ui/src/pages/admin/adminTable.tsx index d083fc2..9df63c6 100644 --- a/ui/src/pages/admin/adminTable.tsx +++ b/ui/src/pages/admin/adminTable.tsx @@ -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(); }, }, { diff --git a/ui/src/pages/chat/chatDetailModal.tsx b/ui/src/pages/chat/chatDetailModal.tsx index 40fc7c2..7bde7a4 100644 --- a/ui/src/pages/chat/chatDetailModal.tsx +++ b/ui/src/pages/chat/chatDetailModal.tsx @@ -98,7 +98,12 @@ const ChatDetailModal = ({ 对话记录-{data?.user?.username} } - width={1200} + sx={{ + '.MuiDialog-paper': { + maxWidth: 1300, + }, + }} + width={1300} open={open} onCancel={onClose} footer={null} diff --git a/ui/src/pages/completion/completionDetailModal.tsx b/ui/src/pages/completion/completionDetailModal.tsx index c14b684..7f52357 100644 --- a/ui/src/pages/completion/completionDetailModal.tsx +++ b/ui/src/pages/completion/completionDetailModal.tsx @@ -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 = { - 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|>'; diff --git a/ui/src/pages/completion/index.tsx b/ui/src/pages/completion/index.tsx index bd87d04..3ace377 100644 --- a/ui/src/pages/completion/index.tsx +++ b/ui/src/pages/completion/index.tsx @@ -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, }); diff --git a/ui/src/pages/dashboard/components/contributionModal.tsx b/ui/src/pages/dashboard/components/contributionModal.tsx index 055b69c..ec7df4e 100644 --- a/ui/src/pages/dashboard/components/contributionModal.tsx +++ b/ui/src/pages/dashboard/components/contributionModal.tsx @@ -17,8 +17,8 @@ const ContributionModal = ({ open={open} onCancel={onCancel} title='用户贡献榜' - showCancel={false} - okText='关闭' + footer={false} + onOk={onCancel} > { 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) => { - 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 ( { 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 = ({ ))} - - API 地址{' '} - - * - - + API 地址 - + API Secret - {ModelProvider[providerBrand].secretRequired && ( - - {' '} - * - - )} - + {ModelProvider[providerBrand].modelDocumentUrl && ( )} /> - {/* {providerBrand === 'AzureOpenAI' && ( - <> - - API Version - - ( - { - field.onChange(e.target.value); - setModelUserList([]); - setValue('model', ''); - setSuccess(false); - }} - /> - )} - /> - - )} */} - {providerBrand === 'Other' ? ( - <> - - 模型名称{' '} - - * - - - ( - - )} - /> - - 需要与模型供应商提供的名称完全一致,不要随便填写 - - - ) : ( - <> - - 模型名称{' '} - - * - - - ( - - {currentModelList.map((it) => ( - - {it.name} - - ))} - - )} - /> - {/* {ModelProvider[providerBrand].customHeader && ( - <> - - Header - - - ( - - )} - /> - = - ( - - )} - /> - - - )} */} - - )} + + + 模型名称 + + + ( + + {currentModelList.map((it) => ( + + {it.name} + + ))} + + )} + /> {error && ( { + const map: Record = { + typescriptreact: 'typescript', + javascriptreact: 'javascript', + tailwindcss: 'css', + shellscript: 'shell', + 'vue-html': 'vue', + tsx: 'typescript', + jsx: 'javascript', + py: 'python', + }; + return map[languageId] || languageId; +};