mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-06 16:53:24 +08:00
@@ -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",
|
||||
|
||||
3
ui/pnpm-lock.yaml
generated
3
ui/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
File diff suppressed because one or more lines are too long
95
ui/src/components/markDown/diff.tsx
Normal file
95
ui/src/components/markDown/diff.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { DiffEditor } from '@monaco-editor/react';
|
||||
|
||||
interface DiffProps {
|
||||
original: string;
|
||||
modified: string;
|
||||
language?: string;
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
const Diff: React.FC<DiffProps> = ({
|
||||
original,
|
||||
modified,
|
||||
language = 'javascript',
|
||||
height = 400,
|
||||
}) => {
|
||||
const editorRef = useRef<any>(null);
|
||||
const monacoRef = useRef<any>(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
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: boxHeight, width: boxWidth }}
|
||||
className='monaco-diff-editor-wrapper'
|
||||
>
|
||||
<DiffEditor
|
||||
height='100%'
|
||||
width='100%'
|
||||
language={language}
|
||||
original={original || ''}
|
||||
modified={modified || ''}
|
||||
theme='vs-dark'
|
||||
onMount={handleMount}
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'off',
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diff;
|
||||
@@ -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<any>;
|
||||
@@ -41,25 +43,98 @@ 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,提取所有 <diff> 内容,生成 diffMap
|
||||
function preprocessMarkdown(mdContent: string) {
|
||||
let diffIndex = 0;
|
||||
const diffMap: Record<string, string> = {};
|
||||
// 自动补全未闭合的 </diff>
|
||||
let fixedMd = mdContent;
|
||||
const openDiffCount = (fixedMd.match(/<diff>/g) || []).length;
|
||||
const closeDiffCount = (fixedMd.match(/<\/diff>/g) || []).length;
|
||||
if (openDiffCount > closeDiffCount) {
|
||||
// 补全缺失的 </diff>
|
||||
for (let i = 0; i < openDiffCount - closeDiffCount; i++) {
|
||||
fixedMd += '</diff>';
|
||||
}
|
||||
}
|
||||
const newMd = fixedMd.replace(
|
||||
/<diff>([\s\S]*?)<\/diff>/g,
|
||||
(_, diffContent) => {
|
||||
const id = `diff-${diffIndex++}`;
|
||||
diffMap[id] = diffContent;
|
||||
return `<diff id="${id}"></diff>`;
|
||||
}
|
||||
);
|
||||
return { newMd, diffMap };
|
||||
}
|
||||
|
||||
const MarkDown = ({
|
||||
loading = false,
|
||||
content,
|
||||
showToolInfo = {},
|
||||
setShowToolInfo,
|
||||
setCurrentToolId,
|
||||
handleSearchAbort,
|
||||
}: {
|
||||
loading?: boolean;
|
||||
content: string;
|
||||
showToolInfo: Record<string, ToolInfo>;
|
||||
setShowToolInfo: (value: Record<string, ToolInfo>) => void;
|
||||
setCurrentToolId?: (value: string) => void;
|
||||
handleSearchAbort?: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [showThink, setShowThink] = useState(false);
|
||||
|
||||
// 删除 content 中 <thinking> 和 <execute_command> 标签,并保留标签中的内容
|
||||
const deleteTags = (content: string) => {
|
||||
@@ -73,6 +148,29 @@ const MarkDown = ({
|
||||
const processContent = (content: string) => {
|
||||
let processedContent = deleteTags(content);
|
||||
|
||||
// 处理 <file_content> 标签(支持带属性),将其内容替换为 markdown 代码块
|
||||
processedContent = processedContent.replace(
|
||||
/<file_content(?:\s+[^>]*)?>([\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 +193,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 +218,45 @@ const MarkDown = ({
|
||||
tagNames: [
|
||||
...(defaultSchema.tagNames! as string[]),
|
||||
'command',
|
||||
// 'tools',
|
||||
// 'tool',
|
||||
// 'toolname',
|
||||
// 'toolargs',
|
||||
// 'toolresult',
|
||||
// 'error',
|
||||
'attemptcompletion',
|
||||
...toolTagNames,
|
||||
'diff',
|
||||
],
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={
|
||||
{
|
||||
error: ({
|
||||
children,
|
||||
...rest
|
||||
}: React.HTMLAttributes<HTMLElement>) => {
|
||||
return (
|
||||
<div className='chat-error' {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
tools: ({
|
||||
id = '',
|
||||
...rest
|
||||
}: React.HTMLAttributes<HTMLElement>) => {
|
||||
const _id = id.replace('user-content-', '');
|
||||
return (
|
||||
<div className='chat-tools' id={_id} {...rest}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div className='chat-tool-name-text'>
|
||||
<Icon type='icon-gongju-tool' />
|
||||
工具调用
|
||||
</div>
|
||||
</div>
|
||||
<div {...rest} style={{ zIndex: 1 }}></div>
|
||||
{!showToolInfo[_id].done && (
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => setCurrentToolId?.(_id)}
|
||||
className='chat-tool-run'
|
||||
sx={{
|
||||
width: 80,
|
||||
bgcolor: 'primary.main',
|
||||
'.MuiButton-startIcon': {
|
||||
mr: 0,
|
||||
},
|
||||
}}
|
||||
startIcon={
|
||||
<Icon
|
||||
type='icon-yunhang'
|
||||
sx={{ fontSize: 12, mr: 0 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
运行
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
sx={{ width: 80 }}
|
||||
onClick={handleSearchAbort}
|
||||
>
|
||||
结束
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
tool: ({
|
||||
id = '',
|
||||
...rest
|
||||
}: React.HTMLAttributes<HTMLElement> & { 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 (
|
||||
<div className={className} id={_id}>
|
||||
<div {...rest} style={{ zIndex: 1 }}></div>
|
||||
{!!showToolInfo[_id] && (
|
||||
<div className='chat-tool-expend-btn'>
|
||||
<div
|
||||
className={
|
||||
showToolInfo[_id].args
|
||||
? 'chat-tool-expend-text chat-tool-expend-text-active'
|
||||
: 'chat-tool-expend-text'
|
||||
}
|
||||
onClick={() => {
|
||||
setShowToolInfo({
|
||||
...showToolInfo,
|
||||
[_id]: {
|
||||
args: !showToolInfo[_id].args,
|
||||
result: false,
|
||||
done: showToolInfo[_id].done,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
参数
|
||||
<ExpandMoreRoundedIcon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
ml: 0,
|
||||
transform: showToolInfo[_id].args
|
||||
? 'rotate(-180deg)'
|
||||
: 'rotate(0deg)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{showToolInfo[_id].done && (
|
||||
<div
|
||||
className={
|
||||
showToolInfo[_id].result
|
||||
? 'chat-tool-expend-text chat-tool-expend-text-active'
|
||||
: 'chat-tool-expend-text'
|
||||
}
|
||||
onClick={() => {
|
||||
setShowToolInfo({
|
||||
...showToolInfo,
|
||||
[_id]: {
|
||||
args: false,
|
||||
result: !showToolInfo[_id].result,
|
||||
done: showToolInfo[_id].done,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
结果
|
||||
<ExpandMoreRoundedIcon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
ml: 0,
|
||||
transform: showToolInfo[_id].result
|
||||
? 'rotate(-180deg)'
|
||||
: 'rotate(0deg)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
toolname: (props: React.HTMLAttributes<HTMLElement>) => {
|
||||
return <div className='chat-tool-name'>{props.children}</div>;
|
||||
},
|
||||
toolargs: ({
|
||||
children,
|
||||
...rest
|
||||
}: React.HTMLAttributes<HTMLElement>) => {
|
||||
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;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
innerText = safeChildren;
|
||||
diff: (props: any) => {
|
||||
const { node } = props;
|
||||
// 去掉 user-content- 前缀
|
||||
const id = node?.properties?.id?.replace(/^user-content-/, '');
|
||||
const rawDiff = id ? diffMap[id] : '';
|
||||
let original = '',
|
||||
modified = '';
|
||||
if (rawDiff) {
|
||||
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 (
|
||||
<div className='chat-tool-args'>
|
||||
<pre {...rest}>{innerText}</pre>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
toolresult: ({
|
||||
children,
|
||||
...rest
|
||||
}: React.HTMLAttributes<HTMLElement>) => {
|
||||
const safeChildren = React.Children.toArray(
|
||||
children || []
|
||||
).filter((child) => child !== '\n');
|
||||
const hasPreTag = safeChildren.some(
|
||||
(child) => React.isValidElement(child) && child.type === 'pre'
|
||||
);
|
||||
return hasPreTag ? (
|
||||
<div
|
||||
className='chat-tool-result'
|
||||
{...rest}
|
||||
children={safeChildren}
|
||||
/>
|
||||
) : (
|
||||
<div className='chat-tool-result'>
|
||||
<pre {...rest} children={safeChildren} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
// return (
|
||||
// <div id='chat-thinking'>
|
||||
// <div
|
||||
// className={!showThink ? 'three-ellipsis' : ''}
|
||||
// {...props}
|
||||
// ></div>
|
||||
// {!loading && (
|
||||
// <IconButton
|
||||
// size='small'
|
||||
// onClick={() => setShowThink(!showThink)}
|
||||
// sx={{
|
||||
// bgcolor: 'background.paper',
|
||||
// ':hover': {
|
||||
// bgcolor: alpha(theme.palette.primary.main, 0.1),
|
||||
// color: theme.palette.primary.main,
|
||||
// },
|
||||
// }}
|
||||
// >
|
||||
// <ExpandMoreRoundedIcon
|
||||
// sx={{
|
||||
// fontSize: 18,
|
||||
// flexShrink: 0,
|
||||
// transform: showThink
|
||||
// ? 'rotate(-180deg)'
|
||||
// : 'rotate(0deg)',
|
||||
// }}
|
||||
// />
|
||||
// </IconButton>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h2 {...props} />
|
||||
),
|
||||
return (
|
||||
<Diff
|
||||
original={original}
|
||||
modified={modified}
|
||||
language='javascript'
|
||||
height={400}
|
||||
/>
|
||||
);
|
||||
},
|
||||
a: ({
|
||||
children,
|
||||
style,
|
||||
@@ -450,7 +332,6 @@ const MarkDown = ({
|
||||
...rest
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
console.log(children, rest);
|
||||
return match ? (
|
||||
<SyntaxHighlighter
|
||||
showLineNumbers
|
||||
|
||||
@@ -13,7 +13,7 @@ const Version = () => {
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
@@ -294,13 +294,6 @@ const AuthPage = () => {
|
||||
// 渲染登录表单
|
||||
const renderLoginForm = () => (
|
||||
<>
|
||||
<LogoContainer>
|
||||
<LogoImage src={Logo} alt='Monkey Code Logo' />
|
||||
<LogoTitle variant='h4' gutterBottom>
|
||||
Monkey Code
|
||||
</LogoTitle>
|
||||
</LogoContainer>
|
||||
|
||||
<Box component='form' onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={12}>{renderUsernameField()}</Grid>
|
||||
@@ -322,6 +315,12 @@ const AuthPage = () => {
|
||||
return (
|
||||
<StyledContainer id='box'>
|
||||
<StyledPaper elevation={3}>
|
||||
<LogoContainer>
|
||||
<LogoImage src={Logo} alt='Monkey Code Logo' />
|
||||
<LogoTitle variant='h4' gutterBottom>
|
||||
Monkey Code
|
||||
</LogoTitle>
|
||||
</LogoContainer>
|
||||
{!loginSetting.disable_password_login && renderLoginForm()}
|
||||
{loginSetting.enable_dingtalk_oauth && dingdingLogin()}
|
||||
</StyledPaper>
|
||||
|
||||
@@ -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 { useEffect, useState } from 'react';
|
||||
import { DomainChatRecord } from '@/api/types';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
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<ConversationItem | null>(null);
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [content, setContent] = useState<DomainChatContent[]>([]);
|
||||
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(/<tools id="([^"]+)">/g)?.map(match => {
|
||||
// const idMatch = match.match(/<tools id="([^"]+)">/);
|
||||
// return idMatch ? idMatch[1] : null;
|
||||
// }).filter(Boolean) || [];
|
||||
// const toolIds = newAnswer.match(/<tool id="([^"]+)">/g)?.map(match => {
|
||||
// const idMatch = match.match(/<tool id="([^"]+)">/);
|
||||
// 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,37 @@ const ChatDetailModal = ({
|
||||
对话记录-{data?.user?.username}
|
||||
</Ellipsis>
|
||||
}
|
||||
width={800}
|
||||
width={1200}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
>
|
||||
{ChatDetailModal ? (
|
||||
<Box sx={{ fontSize: 14 }}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={3}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: 'text.auxiliary',
|
||||
}}
|
||||
>
|
||||
{ChatDetailModal.created_at && (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Icon type='icon-a-shijian2' />
|
||||
{dayjs(ChatDetailModal.created_at).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{ChatDetailModal.remote_ip && (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Icon type='icon-IPdizhijiancha' />
|
||||
{ChatDetailModal.remote_ip}
|
||||
</Stack>
|
||||
)}
|
||||
{ChatDetailModal.model && (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Icon type='icon-moxing' />
|
||||
使用模型
|
||||
<Box>{ChatDetailModal.model}</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{data?.input_tokens && data?.output_tokens && (
|
||||
<Tooltip
|
||||
title={
|
||||
<Stack gap={1} sx={{ minWidth: 100, py: 1 }}>
|
||||
<Box>
|
||||
输入 Token 使用: {addCommasToNumber(data?.input_tokens)}
|
||||
</Box>
|
||||
<Box>
|
||||
输出 Token 使用: {addCommasToNumber(data?.output_tokens)}
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Icon type='icon-moxing' />
|
||||
Token 统计
|
||||
<Box>
|
||||
{addCommasToNumber(
|
||||
data?.input_tokens + data?.output_tokens
|
||||
)}
|
||||
</Box>
|
||||
<Icon type='icon-a-wenhao8' />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
{ChatDetailModal.references?.length > 0 && (
|
||||
<>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: '4px',
|
||||
height: '12px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
内容来源
|
||||
</Stack>
|
||||
<Card sx={{ p: 2, bgcolor: 'background.paper2' }}>
|
||||
{ChatDetailModal.references.map((item: any, index: number) => (
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
key={index}
|
||||
>
|
||||
<Avatar
|
||||
src={item.favicon}
|
||||
sx={{ width: 18, height: 18 }}
|
||||
errorIcon={
|
||||
<Icon
|
||||
type='icon-ditu_diqiu'
|
||||
sx={{ fontSize: 18, color: 'text.auxiliary' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Ellipsis>
|
||||
<Box
|
||||
component={'a'}
|
||||
href={item.url}
|
||||
target='_blank'
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Box>
|
||||
</Ellipsis>
|
||||
</Stack>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: '4px',
|
||||
height: '12px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
回答
|
||||
</Stack>
|
||||
</Box>
|
||||
) : (
|
||||
<Box></Box>
|
||||
)}
|
||||
<Card
|
||||
sx={{
|
||||
'.markdown-body': {
|
||||
background: 'transparent',
|
||||
},
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<MarkDown
|
||||
showToolInfo={showToolInfo}
|
||||
setShowToolInfo={setShowToolInfo}
|
||||
content={content}
|
||||
/>
|
||||
<Card sx={{ p: 0, background: 'transparent', boxShadow: 'none' }}>
|
||||
<StyledChatList>
|
||||
{content.map((item, idx) => {
|
||||
const isUser = item.role === 'user';
|
||||
const name = isUser ? data?.user?.username : 'AI';
|
||||
const msg = item.content || '';
|
||||
return (
|
||||
<StyledChatRow key={idx} isUser={isUser}>
|
||||
<StyledChatAvatar isUser={isUser}>
|
||||
<Avatar
|
||||
name={isUser ? name : undefined}
|
||||
src={isUser ? undefined : logo}
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
fontSize: 22,
|
||||
}}
|
||||
/>
|
||||
</StyledChatAvatar>
|
||||
<StyledChatBubble isUser={isUser}>
|
||||
<MarkDown content={msg} />
|
||||
</StyledChatBubble>
|
||||
</StyledChatRow>
|
||||
);
|
||||
})}
|
||||
</StyledChatList>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -60,9 +60,15 @@ const Chat = () => {
|
||||
return (
|
||||
<Box
|
||||
onClick={() => setChatDetailModal(record)}
|
||||
sx={{ cursor: 'pointer', color: 'info.main' }}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
color: 'info.main',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Ellipsis>{cleanValue}</Ellipsis>
|
||||
{cleanValue}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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<string, string> = {
|
||||
typescriptreact: 'typescript',
|
||||
javascriptreact: 'javascript',
|
||||
tailwindcss: 'css',
|
||||
'vue-html': 'vue',
|
||||
};
|
||||
return map[languageId] || languageId;
|
||||
}
|
||||
|
||||
const ChatDetailModal = ({
|
||||
data,
|
||||
open,
|
||||
@@ -103,7 +113,7 @@ const ChatDetailModal = ({
|
||||
<div style={{ height: 420 }}>
|
||||
<MonacoEditor
|
||||
height='100%'
|
||||
language={data?.program_language || 'plaintext'}
|
||||
language={getBaseLanguageId(data?.program_language || 'plaintext')}
|
||||
value={editorValue}
|
||||
theme='vs-dark'
|
||||
options={{
|
||||
@@ -115,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,
|
||||
|
||||
@@ -65,7 +65,7 @@ const Completion = () => {
|
||||
const [filterLang, setFilterLang] = useState('');
|
||||
const [filterAccept, setFilterAccept] = useState<
|
||||
'accepted' | 'unaccepted' | ''
|
||||
>('');
|
||||
>('accepted');
|
||||
|
||||
const { data: userOptions = { users: [] } } = useRequest(() =>
|
||||
getListUser({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Grid2 as Grid, styled } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
getStatisticsDashboard,
|
||||
getUserCodeRankDashboard,
|
||||
@@ -13,8 +12,17 @@ import LineCharts from './lineCharts';
|
||||
import PieCharts from './pieCharts';
|
||||
import BarCharts from './barCharts';
|
||||
import { ContributionCard } from './statisticCard';
|
||||
import { DomainTimeStat } from '@/api/types';
|
||||
import { getRecent90DaysData, getRecent60MinutesData } from '@/utils';
|
||||
import {
|
||||
getRecent90DaysData,
|
||||
getRecent60MinutesData,
|
||||
getRecent24HoursData,
|
||||
} from '@/utils';
|
||||
import { TimeRange } from '../index';
|
||||
|
||||
interface TimeDuration {
|
||||
duration: number;
|
||||
precision: 'day' | 'hour';
|
||||
}
|
||||
|
||||
export const StyledHighlight = styled('span')(({ theme }) => ({
|
||||
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<TimeDuration>({
|
||||
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<string, number>[],
|
||||
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 (
|
||||
<Grid
|
||||
container
|
||||
@@ -87,7 +115,6 @@ const GlobalStatistic = () => {
|
||||
borderRadius: 2.5,
|
||||
}}
|
||||
>
|
||||
{/* <Grid container size={9} spacing={2}> */}
|
||||
<Grid size={3}>
|
||||
<UserCard data={statisticsData} />
|
||||
</Grid>
|
||||
@@ -97,7 +124,7 @@ const GlobalStatistic = () => {
|
||||
data={userActiveChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_users || 0}
|
||||
</StyledHighlight>
|
||||
@@ -107,20 +134,20 @@ const GlobalStatistic = () => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<ContributionCard data={userCodeRankData} />
|
||||
<ContributionCard data={userCodeRankData} timeRange={timeRange} />
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<PieCharts
|
||||
title='工作模式-对话任务'
|
||||
data={categoryStatData.work_mode || []}
|
||||
extra='最近 90 天'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<PieCharts
|
||||
title='编程语言'
|
||||
data={categoryStatData.program_language || []}
|
||||
extra='最近 90 天'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
@@ -136,7 +163,7 @@ const GlobalStatistic = () => {
|
||||
data={chatChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_chats || 0}
|
||||
</StyledHighlight>
|
||||
@@ -151,7 +178,7 @@ const GlobalStatistic = () => {
|
||||
data={codeCompletionChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_completions || 0}
|
||||
</StyledHighlight>
|
||||
@@ -166,7 +193,7 @@ const GlobalStatistic = () => {
|
||||
data={codeLineChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共修改
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改
|
||||
<StyledHighlight>
|
||||
{timeStatData?.total_lines_of_code || 0}
|
||||
</StyledHighlight>
|
||||
@@ -181,7 +208,7 @@ const GlobalStatistic = () => {
|
||||
data={acceptedPerChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天平均采纳率为
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为
|
||||
<StyledHighlight>
|
||||
{(timeStatData?.total_accepted_per || 0).toFixed(2)}
|
||||
</StyledHighlight>
|
||||
|
||||
@@ -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<TimeDuration>({
|
||||
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<string, number>[],
|
||||
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 = ({
|
||||
<Grid size={6}>
|
||||
<PieCharts
|
||||
title='工作模式-对话任务'
|
||||
extra='最近 90 天'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
data={userStat?.work_mode || []}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<PieCharts
|
||||
title='编程语言'
|
||||
extra='最近 90 天'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
data={userStat?.program_language || []}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -133,7 +161,7 @@ const MemberStatistic = ({
|
||||
data={chatChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>{userStat?.total_chats || 0}</StyledHighlight>
|
||||
个对话任务
|
||||
</>
|
||||
@@ -146,7 +174,7 @@ const MemberStatistic = ({
|
||||
data={codeCompletionChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>
|
||||
{userStat?.total_completions || 0}
|
||||
</StyledHighlight>
|
||||
@@ -161,7 +189,7 @@ const MemberStatistic = ({
|
||||
data={codeLineChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天共修改
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改
|
||||
<StyledHighlight>
|
||||
{userStat?.total_lines_of_code || 0}
|
||||
</StyledHighlight>
|
||||
@@ -176,7 +204,7 @@ const MemberStatistic = ({
|
||||
data={acceptedPerChartData}
|
||||
extra={
|
||||
<>
|
||||
最近 90 天平均采纳率为
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为
|
||||
<StyledHighlight>
|
||||
{(userStat?.total_accepted_per || 0).toFixed(2)}
|
||||
</StyledHighlight>
|
||||
|
||||
@@ -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 = ({
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<Stack direction='row' justifyContent='space-between' alignItems='center'>
|
||||
<Box sx={{ fontWeight: 700 }}>用户贡献榜</Box>
|
||||
<Box sx={{ fontSize: 12, color: 'text.tertiary' }}>最近 90 天</Box>
|
||||
<Box sx={{ fontSize: 12, color: 'text.tertiary' }}>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
MenuItem,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import { CusTabs } from '@c-x/ui';
|
||||
@@ -17,12 +20,14 @@ import { useParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { DomainUser } from '@/api/types';
|
||||
|
||||
export type TimeRange = '90d' | '24h';
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
const { tab, id } = useParams();
|
||||
const [tabValue, setTabValue] = useState(tab || 'global');
|
||||
const [memberId, setMemberId] = useState(id || '');
|
||||
const [memberData, setMemberData] = useState<DomainUser | null>(null);
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('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 = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
labelId='time-range-label'
|
||||
value={timeRange}
|
||||
size='small'
|
||||
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
|
||||
sx={{ fontSize: 14 }}
|
||||
>
|
||||
<MenuItem value='24h'>最近 24 小时</MenuItem>
|
||||
<MenuItem value='90d'>最近 90 天</MenuItem>
|
||||
</Select>
|
||||
</Stack>
|
||||
|
||||
{tabValue === 'global' && <GlobalStatistic />}
|
||||
{tabValue === 'global' && <GlobalStatistic timeRange={timeRange} />}
|
||||
{tabValue === 'member' && (
|
||||
<MemberStatistic
|
||||
memberData={memberData}
|
||||
userList={userList}
|
||||
onMemberChange={onMemberChange}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -322,7 +322,11 @@ const Invite = () => {
|
||||
borderRadius: 1,
|
||||
height: 48,
|
||||
}}
|
||||
onClick={onNext}
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
onNext();
|
||||
}, 500);
|
||||
}}
|
||||
>
|
||||
下载客户端
|
||||
</Button>
|
||||
|
||||
@@ -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 (
|
||||
<Modal
|
||||
title='钉钉登录设置'
|
||||
width={800}
|
||||
open={open}
|
||||
onOk={onSubmit}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Box>
|
||||
<StyledFormLabel required>Client ID</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_id'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Id 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_id}
|
||||
helperText={errors.dingtalk_client_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<StyledFormLabel required>Client Secret</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_secret'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Secret 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_secret}
|
||||
helperText={errors.dingtalk_client_secret?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
{/* <Box>
|
||||
<StyledFormLabel>标题名称,默认为 身份认证-钉钉登录</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='title'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.title}
|
||||
helperText={errors.title?.message}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box> */}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DingingLoginSettingModal;
|
||||
@@ -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 (
|
||||
<Stack gap={2} direction={'row'}>
|
||||
<Stack gap={2} direction={'column'}>
|
||||
<MemberManage />
|
||||
</Stack>
|
||||
<Stack gap={2} direction={'column'}>
|
||||
<Card >
|
||||
<StyledLabel>第三方登录</StyledLabel>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
spacing={2}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 3 }}
|
||||
onClick={() => {
|
||||
if (dingdingCheck) {
|
||||
onDisabledDingdingLogin();
|
||||
} else {
|
||||
setDingdingLoginSettingModalOpen(true);
|
||||
<Stack gap={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={6}>
|
||||
<MemberManage />
|
||||
</Grid>
|
||||
<Grid size={6} container spacing={2}>
|
||||
<Grid size={12}>
|
||||
<StyledCard>
|
||||
<StyledLabel>强制成员启用两步认证</StyledLabel>
|
||||
<Switch
|
||||
checked={data?.force_two_factor_auth}
|
||||
onChange={(e) => {
|
||||
updateSetting({ force_two_factor_auth: e.target.checked });
|
||||
}}
|
||||
/>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<StyledCard>
|
||||
<StyledLabel>禁止成员使用密码登录</StyledLabel>
|
||||
<Switch
|
||||
checked={data?.disable_password_login}
|
||||
onChange={(e) =>
|
||||
updateSetting({ disable_password_login: e.target.checked })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Radio size='small' sx={{ p: 0.5 }} checked={dingdingCheck} />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Icon type='icon-dingding' sx={{ fontSize: 18 }}></Icon>
|
||||
钉钉
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 3 }}
|
||||
disabled
|
||||
>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Icon type='icon-weixin' sx={{ fontSize: 18 }}></Icon>
|
||||
微信
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 3 }}
|
||||
disabled
|
||||
>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Icon type='icon-github' sx={{ fontSize: 18 }}></Icon>
|
||||
GitHub
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
<StyledCard>
|
||||
<StyledLabel>禁止成员使用密码登录</StyledLabel>
|
||||
<Switch
|
||||
checked={data?.disable_password_login}
|
||||
onChange={(e) =>
|
||||
updateSetting({ disable_password_login: e.target.checked })
|
||||
}
|
||||
/>
|
||||
</StyledCard>
|
||||
<StyledCard>
|
||||
<StyledLabel>强制成员启用两步认证</StyledLabel>
|
||||
<Switch
|
||||
checked={data?.force_two_factor_auth}
|
||||
onChange={(e) => {
|
||||
updateSetting({ force_two_factor_auth: e.target.checked });
|
||||
}}
|
||||
/>
|
||||
</StyledCard>
|
||||
<LoginHistory />
|
||||
</Stack>
|
||||
/>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
<DingingLoginSettingModal
|
||||
open={dingdingLoginSettingModalOpen}
|
||||
onClose={() => setDingdingLoginSettingModalOpen(false)}
|
||||
<Grid size={12}>
|
||||
<StyledCard sx={{ height: '100%' }}>
|
||||
<StyledLabel>
|
||||
第三方登录
|
||||
<Box
|
||||
component='span'
|
||||
sx={{
|
||||
ml: 2,
|
||||
color: data.enable_dingtalk_oauth ? 'success.main' : 'gray',
|
||||
fontWeight: 400,
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{data.enable_dingtalk_oauth ? '已开启钉钉登录' : '未开启'}
|
||||
</Box>
|
||||
</StyledLabel>
|
||||
<Button
|
||||
color='info'
|
||||
sx={{ gap: 2 }}
|
||||
onClick={() => setThirdPartyLoginSettingModalOpen(true)}
|
||||
>
|
||||
配置
|
||||
</Button>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
<Grid size={12}>
|
||||
<LoginHistory />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<ThirdPartyLoginSettingModal
|
||||
open={thirdPartyLoginSettingModalOpen}
|
||||
onCancel={() => setThirdPartyLoginSettingModalOpen(false)}
|
||||
settingData={data}
|
||||
onOk={() => {
|
||||
refresh();
|
||||
}}
|
||||
|
||||
215
ui/src/pages/user/thirdPartyLoginSettingModal.tsx
Normal file
215
ui/src/pages/user/thirdPartyLoginSettingModal.tsx
Normal file
@@ -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<LoginType>(
|
||||
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 (
|
||||
<Modal
|
||||
open={open}
|
||||
width={900}
|
||||
onCancel={onCancel}
|
||||
title='第三方登录配置'
|
||||
onOk={onSubmit}
|
||||
>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
spacing={2}
|
||||
sx={{ mt: 2, height: 'calc(100% - 40px)' }}
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 2 }}
|
||||
onClick={() => {
|
||||
setLoginType('none');
|
||||
}}
|
||||
>
|
||||
<Radio size='small' sx={{ p: 0.5 }} checked={loginType === 'none'} />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
不启用
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 2 }}
|
||||
onClick={() => {
|
||||
setLoginType('dingding');
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
size='small'
|
||||
sx={{ p: 0.5 }}
|
||||
checked={loginType === 'dingding'}
|
||||
/>
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
钉钉登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button variant='outlined' color='primary' sx={{ gap: 2 }} disabled>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
企业微信登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button variant='outlined' color='primary' sx={{ gap: 2 }} disabled>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
飞书登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button variant='outlined' color='primary' sx={{ gap: 2 }} disabled>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
OAuth 登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
{loginType === 'dingding' && (
|
||||
<Stack gap={2} sx={{ mt: 4 }}>
|
||||
<Box>
|
||||
<StyledFormLabel required>Client ID</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_id'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Id 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_id}
|
||||
helperText={errors.dingtalk_client_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<StyledFormLabel required>Client Secret</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_secret'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Secret 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_secret}
|
||||
helperText={errors.dingtalk_client_secret?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
{/* <Box>
|
||||
<StyledFormLabel>标题名称,默认为 身份认证-钉钉登录</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='title'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.title}
|
||||
helperText={errors.title?.message}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box> */}
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThirdPartyLoginSettingModal;
|
||||
@@ -85,6 +85,7 @@ const lightTheme = createTheme(
|
||||
borderWidth: '1px !important',
|
||||
},
|
||||
borderRadius: '10px !important',
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -126,6 +127,13 @@ const lightTheme = createTheme(
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputLabel: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenu: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
|
||||
@@ -218,3 +218,31 @@ export const formatNumber = (num: number) => {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
};
|
||||
|
||||
export const getRecent24HoursData = (
|
||||
data: Record<string, number>[] = [],
|
||||
label: { keyLabel?: string; valueLabel?: string } = {}
|
||||
) => {
|
||||
const { keyLabel = 'timestamp', valueLabel = 'tokens' } = label;
|
||||
const xData: string[] = [];
|
||||
const yData: number[] = [];
|
||||
const dateMap: Record<string, number> = {};
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user