From b06f8d8827761f724a218338f04876e7c99e0f45 Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Thu, 3 Jul 2025 17:57:01 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E4=BB=AA?= =?UTF-8?q?=E8=A1=A8=E7=9B=98=E6=97=B6=E9=97=B4=E5=88=87=E6=8D=A2,=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8B=E8=BD=BD=E9=93=BE=E6=8E=A5,?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=B9=E8=AF=9D=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/package.json | 3 +- ui/pnpm-lock.yaml | 3 + ui/src/api/Dashboard.ts | 21 +- ui/src/api/types.ts | 108 ++++-- ui/src/assets/fonts/iconfont.js | 2 +- ui/src/components/markDown/diff.tsx | 77 +++++ ui/src/components/markDown/index.tsx | 320 ++++-------------- ui/src/components/sidebar/version.tsx | 2 +- ui/src/pages/chat/chatDetailModal.tsx | 279 +++++---------- ui/src/pages/chat/index.tsx | 10 +- .../completion/completionDetailModal.tsx | 12 +- .../dashboard/components/globalStatistic.tsx | 109 +++--- .../dashboard/components/memberStatistic.tsx | 70 ++-- .../dashboard/components/statisticCard.tsx | 7 +- ui/src/pages/dashboard/index.tsx | 23 +- ui/src/pages/invite/index.tsx | 6 +- .../pages/user/dingdingLoginSettingModal.tsx | 131 ------- ui/src/pages/user/index.tsx | 174 ++++------ .../user/thirdPartyLoginSettingModal.tsx | 215 ++++++++++++ ui/src/theme.ts | 7 + ui/src/utils/index.ts | 28 ++ 21 files changed, 827 insertions(+), 780 deletions(-) create mode 100644 ui/src/components/markDown/diff.tsx delete mode 100644 ui/src/pages/user/dingdingLoginSettingModal.tsx create mode 100644 ui/src/pages/user/thirdPartyLoginSettingModal.tsx diff --git a/ui/package.json b/ui/package.json index a46043f7..4bf25a6b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -35,7 +35,8 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "unist-util-visit": "^5.0.0" }, "devDependencies": { "@c-x/cx-swagger-api": "^0.0.10", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index ad5893dd..806e1bcf 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: remark-gfm: specifier: ^4.0.1 version: 4.0.1 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 devDependencies: '@c-x/cx-swagger-api': specifier: ^0.0.10 diff --git a/ui/src/api/Dashboard.ts b/ui/src/api/Dashboard.ts index f04426ea..a7c83552 100644 --- a/ui/src/api/Dashboard.ts +++ b/ui/src/api/Dashboard.ts @@ -19,6 +19,9 @@ import { DomainUserEvent, DomainUserHeatmapResp, DomainUserStat, + GetCategoryStatDashboardParams, + GetTimeStatDashboardParams, + GetUserCodeRankDashboardParams, GetUserEventsDashboardParams, GetUserHeatmapDashboardParams, GetUserStatDashboardParams, @@ -38,7 +41,10 @@ import { })` OK */ -export const getCategoryStatDashboard = (params: RequestParams = {}) => +export const getCategoryStatDashboard = ( + query: GetCategoryStatDashboardParams, + params: RequestParams = {}, +) => request< WebResp & { data?: DomainCategoryStat; @@ -46,6 +52,7 @@ export const getCategoryStatDashboard = (params: RequestParams = {}) => >({ path: `/api/v1/dashboard/category-stat`, method: "GET", + query: query, type: ContentType.Json, format: "json", ...params, @@ -90,7 +97,10 @@ export const getStatisticsDashboard = (params: RequestParams = {}) => })` OK */ -export const getTimeStatDashboard = (params: RequestParams = {}) => +export const getTimeStatDashboard = ( + query: GetTimeStatDashboardParams, + params: RequestParams = {}, +) => request< WebResp & { data?: DomainTimeStat; @@ -98,6 +108,7 @@ export const getTimeStatDashboard = (params: RequestParams = {}) => >({ path: `/api/v1/dashboard/time-stat`, method: "GET", + query: query, type: ContentType.Json, format: "json", ...params, @@ -116,7 +127,10 @@ export const getTimeStatDashboard = (params: RequestParams = {}) => })` OK */ -export const getUserCodeRankDashboard = (params: RequestParams = {}) => +export const getUserCodeRankDashboard = ( + query: GetUserCodeRankDashboardParams, + params: RequestParams = {}, +) => request< WebResp & { data?: DomainUserCodeRank[]; @@ -124,6 +138,7 @@ export const getUserCodeRankDashboard = (params: RequestParams = {}) => >({ path: `/api/v1/dashboard/user-code-rank`, method: "GET", + query: query, type: ContentType.Json, format: "json", ...params, diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 16e8cb1c..25966d0a 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ /* tslint:disable */ // @ts-nocheck /* @@ -11,32 +10,37 @@ */ export enum ConstsUserStatus { - UserStatusActive = "active", - UserStatusInactive = "inactive", - UserStatusLocked = "locked", + UserStatusActive = 'active', + UserStatusInactive = 'inactive', + UserStatusLocked = 'locked', } export enum ConstsUserPlatform { - UserPlatformEmail = "email", - UserPlatformDingTalk = "dingtalk", + UserPlatformEmail = 'email', + UserPlatformDingTalk = 'dingtalk', } export enum ConstsModelType { - ModelTypeLLM = "llm", - ModelTypeCoder = "coder", - ModelTypeEmbedding = "embedding", - ModelTypeAudio = "audio", - ModelTypeReranker = "reranker", + ModelTypeLLM = 'llm', + ModelTypeCoder = 'coder', + ModelTypeEmbedding = 'embedding', + ModelTypeAudio = 'audio', + ModelTypeReranker = 'reranker', } export enum ConstsModelStatus { - ModelStatusActive = "active", - ModelStatusInactive = "inactive", + ModelStatusActive = 'active', + ModelStatusInactive = 'inactive', +} + +export enum ConstsChatRole { + ChatRoleUser = 'user', + ChatRoleAssistant = 'assistant', } export enum ConstsAdminStatus { - AdminStatusActive = "active", - AdminStatusInactive = "inactive", + AdminStatusActive = 'active', + AdminStatusInactive = 'inactive', } export interface DomainAcceptCompletionReq { @@ -91,9 +95,17 @@ export interface DomainCategoryStat { work_mode?: DomainCategoryPoint[]; } -export interface DomainChatInfo { +export interface DomainChatContent { + /** 内容 */ content?: string; created_at?: number; + /** 角色,如user: 用户的提问 assistant: 机器人回复 */ + role?: ConstsChatRole; +} + +export interface DomainChatInfo { + /** 消息内容 */ + contents?: DomainChatContent[]; id?: string; } @@ -622,9 +634,53 @@ export interface GetListCompletionRecordParams { size?: number; } +export interface GetCategoryStatDashboardParams { + /** + * 持续时间 (小时或天数)` + * @min 24 + * @max 90 + */ + duration?: number; + /** 精度: "hour", "day" */ + precision: 'hour' | 'day'; + /** 用户ID,可jj */ + user_id?: string; +} + +export interface GetTimeStatDashboardParams { + /** + * 持续时间 (小时或天数)` + * @min 24 + * @max 90 + */ + duration?: number; + /** 精度: "hour", "day" */ + precision: 'hour' | 'day'; + /** 用户ID,可jj */ + user_id?: string; +} + +export interface GetUserCodeRankDashboardParams { + /** + * 持续时间 (小时或天数)` + * @min 24 + * @max 90 + */ + duration?: number; + /** 精度: "hour", "day" */ + precision: 'hour' | 'day'; + /** 用户ID,可jj */ + user_id?: string; +} + export interface GetUserEventsDashboardParams { - /** 用户ID */ - user_id: string; + /** + * 持续时间 (小时或天数)` + * @min 24 + * @max 90 + */ + /** 用户ID,可jj */ + user_id?: string; } export interface GetUserHeatmapDashboardParams { @@ -633,18 +689,26 @@ export interface GetUserHeatmapDashboardParams { } export interface GetUserStatDashboardParams { - /** 用户ID */ + /** + * 持续时间 (小时或天数)` + * @min 24 + * @max 90 + */ + duration?: number; + /** 精度: "hour", "day" */ + precision: 'hour' | 'day'; + /** 用户ID,可jj */ user_id?: string; } export interface GetMyModelListParams { /** 模型类型 llm:对话模型 coder:代码模型 */ - model_type?: "llm" | "coder" | "embedding" | "audio" | "reranker"; + model_type?: 'llm' | 'coder' | 'embedding' | 'audio' | 'reranker'; } export interface GetGetTokenUsageParams { /** 模型类型 llm:对话模型 coder:代码模型 */ - model_type: "llm" | "coder" | "embedding" | "audio" | "reranker"; + model_type: 'llm' | 'coder' | 'embedding' | 'audio' | 'reranker'; } export interface DeleteDeleteUserParams { @@ -677,7 +741,7 @@ export interface GetUserOauthCallbackParams { export interface GetUserOauthSignupOrInParams { /** 第三方平台 dingtalk */ - platform: "email" | "dingtalk"; + platform: 'email' | 'dingtalk'; /** 登录成功后跳转的 URL */ redirect_url?: string; /** 会话ID */ diff --git a/ui/src/assets/fonts/iconfont.js b/ui/src/assets/fonts/iconfont.js index f682689a..2b997c2b 100644 --- a/ui/src/assets/fonts/iconfont.js +++ b/ui/src/assets/fonts/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4940939='',(c=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,o,i,h,e,n=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}t=function(){var a,l=document.createElement("div");l.innerHTML=c._iconfont_svg_string_4940939,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?n(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),t()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(i=t,h=c.document,e=!1,d(),h.onreadystatechange=function(){"complete"==h.readyState&&(h.onreadystatechange=null,m())})}function m(){e||(e=!0,i())}function d(){try{h.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}m()}})(window); \ No newline at end of file +window._iconfont_svg_string_4940939='',(l=>{var a=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var t,h,i,o,e,m=function(a,c){c.parentNode.insertBefore(a,c)};if(a&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}t=function(){var a,c=document.createElement("div");c.innerHTML=l._iconfont_svg_string_4940939,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(a=document.body).firstChild?m(c,a.firstChild):a.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=l.document,e=!1,z(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,n())})}function n(){e||(e=!0,i())}function z(){try{o.documentElement.doScroll("left")}catch(a){return void setTimeout(z,50)}n()}})(window); \ No newline at end of file diff --git a/ui/src/components/markDown/diff.tsx b/ui/src/components/markDown/diff.tsx new file mode 100644 index 00000000..e3303cd1 --- /dev/null +++ b/ui/src/components/markDown/diff.tsx @@ -0,0 +1,77 @@ +import React, { useRef } from 'react'; +import { DiffEditor } from '@monaco-editor/react'; + +interface DiffProps { + original: string; + modified: string; + language?: string; + height?: string | number; +} + +const Diff: React.FC = ({ + original, + modified, + language = 'javascript', + height = 400, +}) => { + // 处理高度和宽度样式 + const boxHeight = typeof height === 'number' ? `${height}px` : height; + const boxWidth = 1000; // 默认宽度800px + + return ( +
+ +
+ ); +}; + +export default Diff; diff --git a/ui/src/components/markDown/index.tsx b/ui/src/components/markDown/index.tsx index 3a969649..25c313fb 100644 --- a/ui/src/components/markDown/index.tsx +++ b/ui/src/components/markDown/index.tsx @@ -1,7 +1,7 @@ // import { ToolInfo } from '@/api'; import { Icon, message } from '@c-x/ui'; import { Box, Button, IconButton, Stack, useTheme, alpha } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import ReactMarkdown, { Components } from 'react-markdown'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { @@ -13,6 +13,8 @@ import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import remarkBreaks from 'remark-breaks'; import remarkGfm from 'remark-gfm'; import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; +import Diff from './diff'; +import { visit } from 'unist-util-visit'; interface ExtendedComponents extends Components { tools?: React.ComponentType; @@ -43,6 +45,21 @@ export const toolTagNames = toolNames.map((name) => name.replace(/_/g, '')); type ToolInfo = any; +// 预处理 markdown,提取所有 内容,生成 diffMap +function preprocessMarkdown(mdContent: string) { + let diffIndex = 0; + const diffMap: Record = {}; + const newMd = mdContent.replace( + /([\s\S]*?)<\/diff>/g, + (_, diffContent) => { + const id = `diff-${diffIndex++}`; + diffMap[id] = diffContent; + return ``; + } + ); + return { newMd, diffMap }; +} + const MarkDown = ({ loading = false, content, @@ -59,7 +76,9 @@ const MarkDown = ({ handleSearchAbort?: () => void; }) => { const theme = useTheme(); + const [diffContent, setDiffContent] = useState([]); const [showThink, setShowThink] = useState(false); + const editorRef = useRef(null); // 删除 content 中 标签,并保留标签中的内容 const deleteTags = (content: string) => { @@ -73,6 +92,29 @@ const MarkDown = ({ const processContent = (content: string) => { let processedContent = deleteTags(content); + // 处理 标签(支持带属性),将其内容替换为 markdown 代码块 + processedContent = processedContent.replace( + /]*)?>([\s\S]*?)<\/file_content>/g, + (match, p1) => { + // 提取 path 属性 + const pathMatch = match.match(/path\s*=\s*["']([^"']+)["']/); + let lang = ''; + if (pathMatch) { + const fileName = pathMatch[1]; + const extMatch = fileName.match(/\.([a-zA-Z0-9]+)$/); + if (extMatch) { + lang = extMatch[1]; + } + } + // 去除首尾空行 + let code = p1.replace(/^\n+|\n+$/g, ''); + // 去除每行前面的行号 + code = code.replace(/^\s*\d+\s*\|\s?/gm, ''); + // 拼接 markdown 代码块 + return `\n\n\`\`\`${lang}\n${code}\n\`\`\`\n\n`; + } + ); + toolNames.forEach((toolName) => { const withUnderscore = toolName; const withoutUnderscore = toolName.replace(/_/g, ''); @@ -95,11 +137,9 @@ const MarkDown = ({ return processedContent; }; - const answer = processContent(content); - - console.log(answer); - - console.log(content); + // 预处理 markdown,提取 diffMap + const { newMd, diffMap } = preprocessMarkdown(content); + const answer = processContent(newMd); if (content.length === 0) return null; @@ -122,259 +162,48 @@ const MarkDown = ({ tagNames: [ ...(defaultSchema.tagNames! as string[]), 'command', - // 'tools', - // 'tool', - // 'toolname', - // 'toolargs', - // 'toolresult', - // 'error', 'attemptcompletion', ...toolTagNames, + 'diff', ], }, ], ]} components={ { - error: ({ - children, - ...rest - }: React.HTMLAttributes) => { - return ( -
- {children} -
- ); - }, - tools: ({ - id = '', - ...rest - }: React.HTMLAttributes) => { - const _id = id.replace('user-content-', ''); - return ( -
-
-
- - 工具调用 -
-
-
- {!showToolInfo[_id].done && ( - - - - - )} -
- ); - }, - tool: ({ - id = '', - ...rest - }: React.HTMLAttributes & { id?: string }) => { - const _id = id.replace('user-content-', ''); - const className = showToolInfo[_id] - ? showToolInfo[_id].args - ? 'chat-tool chat-tool-expend-args' - : showToolInfo[_id].result - ? 'chat-tool chat-tool-expend-result' - : 'chat-tool' - : 'chat-tool'; - return ( -
-
- {!!showToolInfo[_id] && ( -
-
{ - setShowToolInfo({ - ...showToolInfo, - [_id]: { - args: !showToolInfo[_id].args, - result: false, - done: showToolInfo[_id].done, - }, - }); - }} - > - 参数 - -
- {showToolInfo[_id].done && ( -
{ - setShowToolInfo({ - ...showToolInfo, - [_id]: { - args: false, - result: !showToolInfo[_id].result, - done: showToolInfo[_id].done, - }, - }); - }} - > - 结果 - -
- )} -
- )} -
- ); - }, - toolname: (props: React.HTMLAttributes) => { - return
{props.children}
; - }, - toolargs: ({ - children, - ...rest - }: React.HTMLAttributes) => { - const safeChildren = React.Children.toArray(children).filter( - (child) => child !== '\n' - ); - let innerText: React.ReactNode = ''; - try { - if ( - safeChildren.length > 1 && - React.isValidElement(safeChildren[1]) - ) { - const secondChild = safeChildren[1] as React.ReactElement<{ - children?: React.ReactNode; - }>; - if (secondChild.props && secondChild.props.children) { - const jsonString = String(secondChild.props.children); - innerText = JSON.stringify(JSON.parse(jsonString), null, 2); - } - } else { - innerText = safeChildren; + diff: (props: any) => { + const { node } = props; + // 去掉 user-content- 前缀 + const id = node?.properties?.id?.replace(/^user-content-/, ''); + const rawDiff = id ? diffMap[id] : ''; + // 解析 rawDiff 为 original 和 modified + let original = '', + modified = ''; + if (rawDiff) { + const match = rawDiff.match( + /<{2,} *SEARCH([\s\S]*?)={2,}([\s\S]*?)>{2,} *REPLACE/ + ); + if (match) { + // 清理行号标记和分隔线 + const cleanDiff = (str: string) => + str + .replace(/:start_line:\d+\n?[-=]+/g, '') + .replace(/^-{2,}\n?/gm, '') + .replace(/^={2,}\n?/gm, '') + .replace(/^\s+|\s+$/g, ''); + original = cleanDiff(match[1].trim()); + modified = cleanDiff(match[2].trim()); } - } catch (err) { - console.error(err); - innerText = safeChildren; } return ( -
-
{innerText}
-
- ); - }, - toolresult: ({ - children, - ...rest - }: React.HTMLAttributes) => { - const safeChildren = React.Children.toArray( - children || [] - ).filter((child) => child !== '\n'); - const hasPreTag = safeChildren.some( - (child) => React.isValidElement(child) && child.type === 'pre' - ); - return hasPreTag ? ( -
- ) : ( -
-
-                
); }, - - // return ( - //
- //
- // {!loading && ( - // setShowThink(!showThink)} - // sx={{ - // bgcolor: 'background.paper', - // ':hover': { - // bgcolor: alpha(theme.palette.primary.main, 0.1), - // color: theme.palette.primary.main, - // }, - // }} - // > - // - // - // )} - //
- // ); - // }, - h1: (props: React.HTMLAttributes) => ( -

- ), a: ({ children, style, @@ -450,7 +279,6 @@ const MarkDown = ({ ...rest }: React.HTMLAttributes) { const match = /language-(\w+)/.exec(className || ''); - console.log(children, rest); return match ? ( { ); useEffect(() => { - fetch('https://release.baizhi.cloud/monekycode/version.txt') + fetch('https://release.baizhi.cloud/monkeycode/version.txt') .then((response) => response.text()) .then((data) => { setLatestVersion(data); diff --git a/ui/src/pages/chat/chatDetailModal.tsx b/ui/src/pages/chat/chatDetailModal.tsx index 8712a1bb..de92528a 100644 --- a/ui/src/pages/chat/chatDetailModal.tsx +++ b/ui/src/pages/chat/chatDetailModal.tsx @@ -2,16 +2,62 @@ import Avatar from '@/components/avatar'; import Card from '@/components/card'; import { getChatInfo } from '@/api/Billing'; import MarkDown from '@/components/markDown'; -import { addCommasToNumber, processText } from '@/utils'; import { Ellipsis, Icon, Modal } from '@c-x/ui'; -import { Box, Stack, Tooltip, useTheme } from '@mui/material'; -import dayjs from 'dayjs'; +import { styled } from '@mui/material/styles'; +import logo from '@/assets/images/logo.png'; + import { useEffect, useState } from 'react'; -import { DomainChatRecord } from '@/api/types'; +import { DomainChatContent, DomainChatRecord } from '@/api/types'; -type ConversationItem = any; type ToolInfo = any; +const StyledChatList = styled('div')(() => ({ + background: '#f7f8fa', + borderRadius: 4, + padding: 24, + minHeight: 400, + maxHeight: 600, + overflowY: 'auto', +})); + +const StyledChatRow = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser }) => ({ + display: 'flex', + flexDirection: isUser ? 'row-reverse' : 'row', + alignItems: 'flex-start', + marginBottom: 28, + position: 'relative', +})); + +const StyledChatAvatar = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser }) => ({ + margin: isUser ? '0 0 0 18px' : '0 18px 0 0', + display: 'flex', + alignItems: 'flex-start', + position: 'relative', + top: 0, +})); + +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', + minHeight: 36, + maxWidth: 1100, + 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 = ({ data, open, @@ -21,10 +67,7 @@ const ChatDetailModal = ({ open: boolean; onClose: () => void; }) => { - const theme = useTheme(); - const [ChatDetailModal, setChatDetailModal] = - useState(null); - const [content, setContent] = useState(''); + const [content, setContent] = useState([]); const [showToolInfo, setShowToolInfo] = useState<{ [key: string]: ToolInfo }>( {} ); @@ -32,34 +75,8 @@ const ChatDetailModal = ({ const getChatDetailModal = () => { if (!data) return; getChatInfo({ id: data.id! }).then((res) => { - setContent(res.content || ''); + setContent(res.contents || []); }); - // getConversationChatDetailModal({ id }).then((res) => { - // const newAnswer = res.answer - // const toolWrapsIds = newAnswer.match(//g)?.map(match => { - // const idMatch = match.match(//); - // return idMatch ? idMatch[1] : null; - // }).filter(Boolean) || []; - // const toolIds = newAnswer.match(//g)?.map(match => { - // const idMatch = match.match(//); - // return idMatch ? idMatch[1] : null; - // }).filter(Boolean) || []; - // const obj: { [key: string]: ToolInfo } = {} - // toolWrapsIds.forEach(id => { - // obj[id!] = { - // done: true, - // } - // }) - // toolIds.forEach(id => { - // obj[id!] = { - // args: false, - // result: false, - // done: true, - // } - // }) - // setShowToolInfo(obj) - // setChatDetailModal({ ...res, answer: processText(res.answer) }) - // }) }; useEffect(() => { @@ -81,169 +98,41 @@ const ChatDetailModal = ({ 对话记录-{data?.user?.username} } - width={800} + width={1200} open={open} onCancel={onClose} footer={null} > - {ChatDetailModal ? ( - - - {ChatDetailModal.created_at && ( - - - {dayjs(ChatDetailModal.created_at).format( - 'YYYY-MM-DD HH:mm:ss' - )} - - )} - {ChatDetailModal.remote_ip && ( - - - {ChatDetailModal.remote_ip} - - )} - {ChatDetailModal.model && ( - - - 使用模型 - {ChatDetailModal.model} - - )} - {data?.input_tokens && data?.output_tokens && ( - - - 输入 Token 使用: {addCommasToNumber(data?.input_tokens)} - - - 输出 Token 使用: {addCommasToNumber(data?.output_tokens)} - - - } - > - - - Token 统计 - - {addCommasToNumber( - data?.input_tokens + data?.output_tokens - )} - - - - - )} - - {ChatDetailModal.references?.length > 0 && ( - <> - - 内容来源 - - - {ChatDetailModal.references.map((item: any, index: number) => ( - - - } - /> - - - {item.title} - - - - ))} - - - )} - - 回答 - - - ) : ( - - )} - - + + + {content.map((item, idx) => { + const isUser = item.role === 'user'; + const name = isUser ? data?.user?.username : 'AI'; + const msg = item.content || ''; + return ( + + + + + + + + + ); + })} + ); diff --git a/ui/src/pages/chat/index.tsx b/ui/src/pages/chat/index.tsx index 63b4594c..e7c5a308 100644 --- a/ui/src/pages/chat/index.tsx +++ b/ui/src/pages/chat/index.tsx @@ -60,9 +60,15 @@ const Chat = () => { return ( setChatDetailModal(record)} - sx={{ cursor: 'pointer', color: 'info.main' }} + sx={{ + cursor: 'pointer', + color: 'info.main', + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }} > - {cleanValue} + {cleanValue} ); }, diff --git a/ui/src/pages/completion/completionDetailModal.tsx b/ui/src/pages/completion/completionDetailModal.tsx index 883d1be6..c07110ba 100644 --- a/ui/src/pages/completion/completionDetailModal.tsx +++ b/ui/src/pages/completion/completionDetailModal.tsx @@ -6,6 +6,16 @@ import MonacoEditor from '@monaco-editor/react'; import { useEffect, useState, useRef } from 'react'; import { DomainCompletionRecord } from '@/api/types'; +function getBaseLanguageId(languageId: string): string { + const map: Record = { + typescriptreact: 'typescript', + javascriptreact: 'javascript', + tailwindcss: 'css', + 'vue-html': 'vue', + }; + return map[languageId] || languageId; +} + const ChatDetailModal = ({ data, open, @@ -103,7 +113,7 @@ const ChatDetailModal = ({
({ fontSize: 12, @@ -22,16 +30,46 @@ export const StyledHighlight = styled('span')(({ theme }) => ({ padding: '0 4px', })); -const GlobalStatistic = () => { +const GlobalStatistic = ({ timeRange }: { timeRange: TimeRange }) => { + const [timeDuration, settimeDuration] = useState({ + duration: timeRange === '90d' ? 90 : 24, + precision: timeRange === '90d' ? 'day' : 'hour', + }); + const { data: statisticsData } = useRequest(getStatisticsDashboard); - const { data: userCodeRankData } = useRequest(getUserCodeRankDashboard); - const { data: timeStatData } = useRequest(getTimeStatDashboard); + + const { data: userCodeRankData } = useRequest( + () => getUserCodeRankDashboard(timeDuration), + { + refreshDeps: [timeDuration], + } + ); + + const { data: timeStatData } = useRequest( + () => getTimeStatDashboard(timeDuration), + { + refreshDeps: [timeDuration], + } + ); + const { data: categoryStatData = { program_language: [], work_mode: [], }, - } = useRequest(getCategoryStatDashboard); + } = useRequest(() => getCategoryStatDashboard(timeDuration), { + refreshDeps: [timeDuration], + }); + + const getRangeData = ( + data: Record[], + timeRange: TimeRange, + label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' } + ) => { + return timeRange === '90d' + ? getRecent90DaysData(data, label) + : getRecent24HoursData(data, label); + }; const { userActiveChartData, @@ -49,34 +87,24 @@ const GlobalStatistic = () => { real_time_tokens = [], accepted_per = [], } = timeStatData || {}; - const userActiveChartData = getRecent90DaysData(active_users, { - valueLabel: 'value', - }); - const chatChartData = getRecent90DaysData(chats, { - valueLabel: 'value', - }); - const codeCompletionChartData = getRecent90DaysData(code_completions, { - valueLabel: 'value', - }); - const codeLineChartData = getRecent90DaysData(lines_of_code, { - valueLabel: 'value', - }); - const realTimeTokenChartData = getRecent60MinutesData(real_time_tokens, { - valueLabel: 'value', - }); - const acceptedPerChartData = getRecent90DaysData(accepted_per, { - valueLabel: 'value', - }); + const label = { valueLabel: 'value' }; return { - userActiveChartData, - chatChartData, - codeCompletionChartData, - codeLineChartData, - realTimeTokenChartData, - acceptedPerChartData, + userActiveChartData: getRangeData(active_users, timeRange, label), + chatChartData: getRangeData(chats, timeRange, label), + codeCompletionChartData: getRangeData(code_completions, timeRange, label), + codeLineChartData: getRangeData(lines_of_code, timeRange, label), + realTimeTokenChartData: getRecent60MinutesData(real_time_tokens, label), + acceptedPerChartData: getRangeData(accepted_per, timeRange, label), }; }, [timeStatData]); + useEffect(() => { + settimeDuration({ + duration: timeRange === '90d' ? 90 : 24, + precision: timeRange === '90d' ? 'day' : 'hour', + }); + }, [timeRange]); + return ( { borderRadius: 2.5, }} > - {/* */} @@ -97,7 +124,7 @@ const GlobalStatistic = () => { data={userActiveChartData} extra={ <> - 最近 90 天共 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 {timeStatData?.total_users || 0} @@ -107,20 +134,20 @@ const GlobalStatistic = () => { /> - + @@ -136,7 +163,7 @@ const GlobalStatistic = () => { data={chatChartData} extra={ <> - 最近 90 天共 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 {timeStatData?.total_chats || 0} @@ -151,7 +178,7 @@ const GlobalStatistic = () => { data={codeCompletionChartData} extra={ <> - 最近 90 天共 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 {timeStatData?.total_completions || 0} @@ -166,7 +193,7 @@ const GlobalStatistic = () => { data={codeLineChartData} extra={ <> - 最近 90 天共修改 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改 {timeStatData?.total_lines_of_code || 0} @@ -181,7 +208,7 @@ const GlobalStatistic = () => { data={acceptedPerChartData} extra={ <> - 最近 90 天平均采纳率为 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为 {(timeStatData?.total_accepted_per || 0).toFixed(2)} diff --git a/ui/src/pages/dashboard/components/memberStatistic.tsx b/ui/src/pages/dashboard/components/memberStatistic.tsx index 53186cb3..8d81f657 100644 --- a/ui/src/pages/dashboard/components/memberStatistic.tsx +++ b/ui/src/pages/dashboard/components/memberStatistic.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Grid2 as Grid } from '@mui/material'; import { useParams } from 'react-router-dom'; import MemberInfo from './memberInfo'; @@ -12,18 +12,31 @@ import { getUserHeatmapDashboard, } from '@/api/Dashboard'; import { StyledHighlight } from './globalStatistic'; -import { getRecent90DaysData } from '@/utils'; +import { getRecent90DaysData, getRecent24HoursData } from '@/utils'; import { DomainUser } from '@/api/types'; +import { TimeRange } from '../index'; + +interface TimeDuration { + duration: number; + precision: 'day' | 'hour'; +} const MemberStatistic = ({ memberData, userList, onMemberChange, + timeRange, }: { memberData: DomainUser | null; userList: DomainUser[]; onMemberChange: (data: DomainUser) => void; + timeRange: TimeRange; }) => { + const [timeDuration, setTimeDuration] = useState({ + duration: timeRange === '90d' ? 90 : 24, + precision: timeRange === '90d' ? 'day' : 'hour', + }); + const { id } = useParams(); const { data: userEvents } = useRequest( () => @@ -40,9 +53,10 @@ const MemberStatistic = ({ () => getUserStatDashboard({ user_id: id || '', + ...timeDuration, }), { - refreshDeps: [id], + refreshDeps: [id, timeDuration], manual: false, ready: !!id, } @@ -59,6 +73,23 @@ const MemberStatistic = ({ } ); + useEffect(() => { + setTimeDuration({ + duration: timeRange === '90d' ? 90 : 24, + precision: timeRange === '90d' ? 'day' : 'hour', + }); + }, [timeRange]); + + const getRangeData = ( + data: Record[], + timeRange: TimeRange, + label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' } + ) => { + return timeRange === '90d' + ? getRecent90DaysData(data, label) + : getRecent24HoursData(data, label); + }; + const { chatChartData, codeCompletionChartData, @@ -71,18 +102,15 @@ const MemberStatistic = ({ code_completions = [], lines_of_code = [], } = userStat || {}; - const chatChartData = getRecent90DaysData(chats, { - valueLabel: 'value', - }); - const codeCompletionChartData = getRecent90DaysData(code_completions, { - valueLabel: 'value', - }); - const codeLineChartData = getRecent90DaysData(lines_of_code, { - valueLabel: 'value', - }); - const acceptedPerChartData = getRecent90DaysData(accepted_per, { - valueLabel: 'value', - }); + const label = { valueLabel: 'value' }; + const chatChartData = getRangeData(chats, timeRange, label); + const codeCompletionChartData = getRangeData( + code_completions, + timeRange, + label + ); + const codeLineChartData = getRangeData(lines_of_code, timeRange, label); + const acceptedPerChartData = getRangeData(accepted_per, timeRange, label); return { chatChartData, codeCompletionChartData, @@ -112,14 +140,14 @@ const MemberStatistic = ({ @@ -133,7 +161,7 @@ const MemberStatistic = ({ data={chatChartData} extra={ <> - 最近 90 天共 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 {userStat?.total_chats || 0} 个对话任务 @@ -146,7 +174,7 @@ const MemberStatistic = ({ data={codeCompletionChartData} extra={ <> - 最近 90 天共 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 {userStat?.total_completions || 0} @@ -161,7 +189,7 @@ const MemberStatistic = ({ data={codeLineChartData} extra={ <> - 最近 90 天共修改 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改 {userStat?.total_lines_of_code || 0} @@ -176,7 +204,7 @@ const MemberStatistic = ({ data={acceptedPerChartData} extra={ <> - 最近 90 天平均采纳率为 + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为 {(userStat?.total_accepted_per || 0).toFixed(2)} diff --git a/ui/src/pages/dashboard/components/statisticCard.tsx b/ui/src/pages/dashboard/components/statisticCard.tsx index de6f3c2d..7d5008a3 100644 --- a/ui/src/pages/dashboard/components/statisticCard.tsx +++ b/ui/src/pages/dashboard/components/statisticCard.tsx @@ -3,6 +3,7 @@ import { styled, Stack, Box } from '@mui/material'; import { Empty } from '@c-x/ui'; import dayjs from 'dayjs'; import { useNavigate } from 'react-router-dom'; +import { TimeRange } from '../index'; import Card from '@/components/card'; import { @@ -53,8 +54,10 @@ const StyledSerialNumber = styled('span')<{ num: number }>(({ theme, num }) => { export const ContributionCard = ({ data = [], + timeRange, }: { data?: DomainUserCodeRank[]; + timeRange: TimeRange; }) => { const navigate = useNavigate(); @@ -62,7 +65,9 @@ export const ContributionCard = ({ 用户贡献榜 - 最近 90 天 + + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'} + { const navigate = useNavigate(); const { tab, id } = useParams(); const [tabValue, setTabValue] = useState(tab || 'global'); - const [memberId, setMemberId] = useState(id || ''); const [memberData, setMemberData] = useState(null); + const [timeRange, setTimeRange] = useState('90d'); const { data: userData, refresh } = useRequest( () => @@ -34,11 +39,9 @@ const Dashboard = () => { manual: true, onSuccess: (res) => { if (id) { - setMemberId(id); setMemberData(res.users?.find((item) => item.id === id) || null); } else { setMemberData(res.users?.[0] || null); - setMemberId(res.users?.[0]?.id || ''); navigate(`/dashboard/member/${res.users?.[0]?.id}`); } }, @@ -54,7 +57,6 @@ const Dashboard = () => { }, [tabValue]); const onMemberChange = (data: DomainUser) => { - setMemberId(data.id!); setMemberData(data); navigate(`/dashboard/member/${data.id}`); }; @@ -90,14 +92,25 @@ const Dashboard = () => { }, }} /> + - {tabValue === 'global' && } + {tabValue === 'global' && } {tabValue === 'member' && ( )} diff --git a/ui/src/pages/invite/index.tsx b/ui/src/pages/invite/index.tsx index 758b31c5..1ccff4ef 100644 --- a/ui/src/pages/invite/index.tsx +++ b/ui/src/pages/invite/index.tsx @@ -322,7 +322,11 @@ const Invite = () => { borderRadius: 1, height: 48, }} - onClick={onNext} + onClick={() => { + setTimeout(() => { + onNext(); + }, 500); + }} > 下载客户端 diff --git a/ui/src/pages/user/dingdingLoginSettingModal.tsx b/ui/src/pages/user/dingdingLoginSettingModal.tsx deleted file mode 100644 index 2d571f24..00000000 --- a/ui/src/pages/user/dingdingLoginSettingModal.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Modal, message, Loading } from '@c-x/ui'; -import { useForm, Controller } from 'react-hook-form'; -import { StyledFormLabel } from '@/components/form'; -import { putUpdateSetting } from '@/api/User'; -import { - Box, - Typography, - IconButton, - Paper, - TextField, - Stack, -} from '@mui/material'; - -const DingingLoginSettingModal = ({ - open, - onClose, - onOk, -}: { - open: boolean; - onClose: () => void; - onOk: () => void; -}) => { - const { - control, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - defaultValues: { - dingtalk_client_id: '', - dingtalk_client_secret: '', - // title: '', - }, - }); - - useEffect(() => { - if (open) { - reset(); - } - }, [open]); - - const onSubmit = handleSubmit((data) => { - putUpdateSetting({ ...data, enable_dingtalk_oauth: true }).then(() => { - message.success('设置成功'); - onClose(); - onOk(); - }); - }); - - return ( - - - - Client ID - ( - - )} - /> - - - Client Secret - ( - - )} - /> - - {/* - 标题名称,默认为 身份认证-钉钉登录 - ( - { - field.onChange(e.target.value); - }} - /> - )} - /> - */} - - - ); -}; - -export default DingingLoginSettingModal; diff --git a/ui/src/pages/user/index.tsx b/ui/src/pages/user/index.tsx index 7a8316c2..2e2c0115 100644 --- a/ui/src/pages/user/index.tsx +++ b/ui/src/pages/user/index.tsx @@ -7,9 +7,6 @@ import { Switch, Button, Box, - Select, - MenuItem, - Radio, } from '@mui/material'; import { Icon, Modal } from '@c-x/ui'; import { useRequest } from 'ahooks'; @@ -17,7 +14,7 @@ import { getGetSetting, putUpdateSetting } from '@/api/User'; import MemberManage from './memberManage'; import LoginHistory from './loginHistory'; import { message } from '@c-x/ui'; -import DingingLoginSettingModal from './dingdingLoginSettingModal'; +import ThirdPartyLoginSettingModal from './thirdPartyLoginSettingModal'; const StyledCard = styled(Card)({ display: 'flex', @@ -34,9 +31,8 @@ const StyledLabel = styled('div')(({ theme }) => ({ })); const User = () => { - const [dingdingLoginSettingModalOpen, setDingdingLoginSettingModalOpen] = + const [thirdPartyLoginSettingModalOpen, setThirdPartyLoginSettingModalOpen] = useState(false); - const [dingdingCheck, setDingdingCheck] = useState(false); const { data = { enable_sso: false, @@ -45,11 +41,7 @@ const User = () => { enable_dingtalk_oauth: false, }, refresh, - } = useRequest(getGetSetting, { - onSuccess: (data) => { - setDingdingCheck(data.enable_dingtalk_oauth!); - }, - }); + } = useRequest(getGetSetting); const { runAsync: updateSetting } = useRequest(putUpdateSetting, { manual: true, @@ -59,106 +51,72 @@ const User = () => { }, }); - const onDisabledDingdingLogin = () => { - Modal.confirm({ - title: '提示', - content: '确定要关闭钉钉登录吗?', - onOk: () => { - updateSetting({ enable_dingtalk_oauth: false }).then(() => { - refresh(); - }); - }, - }); - }; - return ( - - - - - - - 第三方登录 - - - - - - - - 禁止成员使用密码登录 - - updateSetting({ disable_password_login: e.target.checked }) - } - /> - - - 强制成员启用两步认证 - { - updateSetting({ force_two_factor_auth: e.target.checked }); - }} - /> - - - + /> + + + + + + + 第三方登录 + + {data.enable_dingtalk_oauth ? '已开启钉钉登录' : '未开启'} + + + + + + + + + + + - setDingdingLoginSettingModalOpen(false)} + setThirdPartyLoginSettingModalOpen(false)} + settingData={data} onOk={() => { refresh(); }} diff --git a/ui/src/pages/user/thirdPartyLoginSettingModal.tsx b/ui/src/pages/user/thirdPartyLoginSettingModal.tsx new file mode 100644 index 00000000..bf57abc1 --- /dev/null +++ b/ui/src/pages/user/thirdPartyLoginSettingModal.tsx @@ -0,0 +1,215 @@ +import { Button, Radio, Stack, Box, TextField } from '@mui/material'; +import { Modal, Icon, message } from '@c-x/ui'; +import { useState, useEffect } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { StyledFormLabel } from '@/components/form'; +import { putUpdateSetting } from '@/api/User'; +import { DomainSetting } from '@/api/types'; + +type LoginType = 'dingding' | 'wechat' | 'feishu' | 'oauth' | 'none'; + +const ThirdPartyLoginSettingModal = ({ + open, + onCancel, + settingData, + onOk, +}: { + open: boolean; + onCancel: () => void; + settingData: DomainSetting; + onOk: () => void; +}) => { + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + defaultValues: { + dingtalk_client_id: '', + dingtalk_client_secret: '', + // title: '', + }, + }); + + const [loginType, setLoginType] = useState( + settingData?.enable_dingtalk_oauth ? 'dingding' : 'none' + ); + + useEffect(() => { + if (open) { + reset(); + } + }, [open]); + + useEffect(() => { + if (settingData?.enable_dingtalk_oauth) { + setLoginType('dingding'); + } + }, [settingData]); + + const onSubmit = handleSubmit((data) => { + if (loginType === 'none') { + putUpdateSetting({ ...data, enable_dingtalk_oauth: false }).then(() => { + message.success('设置成功'); + onCancel(); + onOk(); + }); + } + if (loginType === 'dingding') { + putUpdateSetting({ ...data, enable_dingtalk_oauth: true }).then(() => { + message.success('设置成功'); + onCancel(); + onOk(); + }); + } + }); + + return ( + + + + + + + + + {loginType === 'dingding' && ( + + + Client ID + ( + + )} + /> + + + Client Secret + ( + + )} + /> + + {/* + 标题名称,默认为 身份认证-钉钉登录 + ( + { + field.onChange(e.target.value); + }} + /> + )} + /> + */} + + )} + + ); +}; + +export default ThirdPartyLoginSettingModal; diff --git a/ui/src/theme.ts b/ui/src/theme.ts index 49fe147f..1c791cde 100644 --- a/ui/src/theme.ts +++ b/ui/src/theme.ts @@ -126,6 +126,13 @@ const lightTheme = createTheme( }, }, }, + MuiInputLabel: { + styleOverrides: { + root: { + fontSize: 14, + }, + }, + }, MuiMenu: { styleOverrides: { paper: { diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index eca20c43..9406032a 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -218,3 +218,31 @@ export const formatNumber = (num: number) => { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } }; + +export const getRecent24HoursData = ( + data: Record[] = [], + label: { keyLabel?: string; valueLabel?: string } = {} +) => { + const { keyLabel = 'timestamp', valueLabel = 'tokens' } = label; + const xData: string[] = []; + const yData: number[] = []; + const dateMap: Record = {}; + + data.forEach((item) => { + // 转为整点小时 + const hour = dayjs + .unix(item[keyLabel]!) + .startOf('hour') + .format('YYYY-MM-DD HH:00'); + dateMap[hour] = item[valueLabel]!; + }); + + // 当前整点 + const now = dayjs().startOf('hour'); + for (let i = 23; i >= 0; i--) { + const time = now.subtract(i, 'hour').format('YYYY-MM-DD HH:00'); + xData.push(time); + yData.push(dateMap[time] || 0); + } + return { xData, yData }; +}; From 79fdae79472b6e7620222fcdc1742831ede2c9e2 Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Thu, 3 Jul 2025 19:11:41 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E8=AE=B0=E5=BD=95=20diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/components/markDown/diff.tsx | 40 +++++-- ui/src/components/markDown/index.tsx | 109 +++++++++++++----- ui/src/pages/auth/index.tsx | 13 +-- .../completion/completionDetailModal.tsx | 7 -- ui/src/pages/completion/index.tsx | 2 +- ui/src/theme.ts | 1 + 6 files changed, 118 insertions(+), 54 deletions(-) diff --git a/ui/src/components/markDown/diff.tsx b/ui/src/components/markDown/diff.tsx index e3303cd1..f2e7fc19 100644 --- a/ui/src/components/markDown/diff.tsx +++ b/ui/src/components/markDown/diff.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import { DiffEditor } from '@monaco-editor/react'; interface DiffProps { @@ -14,6 +14,30 @@ const Diff: React.FC = ({ language = 'javascript', height = 400, }) => { + const editorRef = useRef(null); + const monacoRef = useRef(null); + + // 卸载时主动 dispose + useEffect(() => { + return () => { + if (editorRef.current && monacoRef.current) { + const editor = editorRef.current; + // DiffEditor getModel() 返回 [original, modified] + const models = editor.getModel ? editor.getModel() : []; + if (models && Array.isArray(models)) { + models.forEach( + (model: any) => model && model.dispose && model.dispose() + ); + } + } + }; + }, []); + + const handleMount = (editor: any, monaco: any) => { + editorRef.current = editor; + monacoRef.current = monaco; + }; + // 处理高度和宽度样式 const boxHeight = typeof height === 'number' ? `${height}px` : height; const boxWidth = 1000; // 默认宽度800px @@ -27,25 +51,19 @@ const Diff: React.FC = ({ height='100%' width='100%' language={language} - original={original} - modified={modified} + original={original || ''} + modified={modified || ''} theme='vs-dark' + onMount={handleMount} options={{ readOnly: true, minimap: { enabled: false }, fontSize: 14, scrollBeyondLastLine: false, - wordWrap: 'on', + wordWrap: 'off', lineNumbers: 'on', glyphMargin: false, folding: false, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - handleMouseWheel: false, - alwaysConsumeMouseWheel: false, - useShadows: false, - }, overviewRulerLanes: 0, guides: { indentation: true, diff --git a/ui/src/components/markDown/index.tsx b/ui/src/components/markDown/index.tsx index 25c313fb..3b683a13 100644 --- a/ui/src/components/markDown/index.tsx +++ b/ui/src/components/markDown/index.tsx @@ -43,13 +43,80 @@ export const toolNames = [ // 去掉下划线的标签名,用于Markdown渲染 export const toolTagNames = toolNames.map((name) => name.replace(/_/g, '')); -type ToolInfo = any; +// 支持多组 diff 分隔符,容错处理 +function parseAndMergeDiffs(diffText: string) { + const diffBlocks: { search: string; replace: string }[] = []; + const lines = diffText.split('\n'); + let inDiff = false; + let inSearch = false; + let inReplace = false; + let searchBuffer: string[] = []; + let replaceBuffer: string[] = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^<+ *SEARCH/.test(line)) { + inDiff = true; + inSearch = true; + inReplace = false; + searchBuffer = []; + replaceBuffer = []; + continue; + } + if (/^====+$/.test(line)) { + if (inDiff && inSearch) { + inSearch = false; + inReplace = true; + continue; + } + } + if (/^>+ *REPLACE/.test(line)) { + if (inDiff && inReplace) { + diffBlocks.push({ + search: searchBuffer.join('\n'), + replace: replaceBuffer.join('\n'), + }); + inDiff = false; + inReplace = false; + continue; + } + } + if (inDiff) { + if (inSearch) { + searchBuffer.push(line); + } else if (inReplace) { + replaceBuffer.push(line); + } + } + } + // 容错:如果最后一组没有正常结束 + if (inDiff) { + diffBlocks.push({ + search: searchBuffer.join('\n'), + replace: replaceBuffer.join('\n'), + }); + } + + const mergedSearch = diffBlocks.map((b) => b.search).join('\n'); + const mergedReplace = diffBlocks.map((b) => b.replace).join('\n'); + + return { mergedSearch, mergedReplace, diffBlocks }; +} // 预处理 markdown,提取所有 内容,生成 diffMap function preprocessMarkdown(mdContent: string) { let diffIndex = 0; const diffMap: Record = {}; - const newMd = mdContent.replace( + // 自动补全未闭合的 + let fixedMd = mdContent; + const openDiffCount = (fixedMd.match(//g) || []).length; + const closeDiffCount = (fixedMd.match(/<\/diff>/g) || []).length; + if (openDiffCount > closeDiffCount) { + // 补全缺失的 + for (let i = 0; i < openDiffCount - closeDiffCount; i++) { + fixedMd += ''; + } + } + const newMd = fixedMd.replace( /([\s\S]*?)<\/diff>/g, (_, diffContent) => { const id = `diff-${diffIndex++}`; @@ -63,22 +130,11 @@ function preprocessMarkdown(mdContent: string) { const MarkDown = ({ loading = false, content, - showToolInfo = {}, - setShowToolInfo, - setCurrentToolId, - handleSearchAbort, }: { loading?: boolean; content: string; - showToolInfo: Record; - setShowToolInfo: (value: Record) => void; - setCurrentToolId?: (value: string) => void; - handleSearchAbort?: () => void; }) => { const theme = useTheme(); - const [diffContent, setDiffContent] = useState([]); - const [showThink, setShowThink] = useState(false); - const editorRef = useRef(null); // 删除 content 中 标签,并保留标签中的内容 const deleteTags = (content: string) => { @@ -176,25 +232,22 @@ const MarkDown = ({ // 去掉 user-content- 前缀 const id = node?.properties?.id?.replace(/^user-content-/, ''); const rawDiff = id ? diffMap[id] : ''; - // 解析 rawDiff 为 original 和 modified let original = '', modified = ''; if (rawDiff) { - const match = rawDiff.match( - /<{2,} *SEARCH([\s\S]*?)={2,}([\s\S]*?)>{2,} *REPLACE/ - ); - if (match) { - // 清理行号标记和分隔线 - const cleanDiff = (str: string) => - str - .replace(/:start_line:\d+\n?[-=]+/g, '') - .replace(/^-{2,}\n?/gm, '') - .replace(/^={2,}\n?/gm, '') - .replace(/^\s+|\s+$/g, ''); - original = cleanDiff(match[1].trim()); - modified = cleanDiff(match[2].trim()); - } + const { mergedSearch, mergedReplace } = + parseAndMergeDiffs(rawDiff); + // 清理行号标记和分隔线 + const cleanDiff = (str: string) => + str + .replace(/:start_line:\d+\n?[-=]+/g, '') + .replace(/^-{2,}\n?/gm, '') + .replace(/^={2,}\n?/gm, '') + .replace(/^\n+|\n+$/g, ''); + original = cleanDiff(mergedSearch); + modified = cleanDiff(mergedReplace); } + return ( { // 渲染登录表单 const renderLoginForm = () => ( <> - - - - Monkey Code - - - {renderUsernameField()} @@ -322,6 +315,12 @@ const AuthPage = () => { return ( + + + + Monkey Code + + {!loginSetting.disable_password_login && renderLoginForm()} {loginSetting.enable_dingtalk_oauth && dingdingLogin()} diff --git a/ui/src/pages/completion/completionDetailModal.tsx b/ui/src/pages/completion/completionDetailModal.tsx index c07110ba..c14b6841 100644 --- a/ui/src/pages/completion/completionDetailModal.tsx +++ b/ui/src/pages/completion/completionDetailModal.tsx @@ -125,13 +125,6 @@ const ChatDetailModal = ({ lineNumbers: 'on', glyphMargin: false, folding: false, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - handleMouseWheel: false, - alwaysConsumeMouseWheel: false, - useShadows: false, - }, overviewRulerLanes: 0, guides: { indentation: true, diff --git a/ui/src/pages/completion/index.tsx b/ui/src/pages/completion/index.tsx index 334e1644..f6e40c01 100644 --- a/ui/src/pages/completion/index.tsx +++ b/ui/src/pages/completion/index.tsx @@ -65,7 +65,7 @@ const Completion = () => { const [filterLang, setFilterLang] = useState(''); const [filterAccept, setFilterAccept] = useState< 'accepted' | 'unaccepted' | '' - >(''); + >('accepted'); const { data: userOptions = { users: [] } } = useRequest(() => getListUser({ diff --git a/ui/src/theme.ts b/ui/src/theme.ts index 1c791cde..9e6a7417 100644 --- a/ui/src/theme.ts +++ b/ui/src/theme.ts @@ -85,6 +85,7 @@ const lightTheme = createTheme( borderWidth: '1px !important', }, borderRadius: '10px !important', + fontSize: 14, }, }, }, From 52c5a8e4d948601ce1fde887507f951af63069bb Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Thu, 3 Jul 2025 19:23:35 +0800 Subject: [PATCH 3/3] fix: ts error --- ui/src/pages/chat/chatDetailModal.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui/src/pages/chat/chatDetailModal.tsx b/ui/src/pages/chat/chatDetailModal.tsx index de92528a..40fc7c2c 100644 --- a/ui/src/pages/chat/chatDetailModal.tsx +++ b/ui/src/pages/chat/chatDetailModal.tsx @@ -123,11 +123,7 @@ const ChatDetailModal = ({ /> - + );