Merge pull request #50 from guanweiwang/main

feat: 统计直接时间切换,优化对话记录
This commit is contained in:
Yoko
2025-07-03 20:13:46 +08:00
committed by GitHub
23 changed files with 917 additions and 810 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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,

View File

@@ -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

View 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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>
);
},

View File

@@ -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,

View File

@@ -65,7 +65,7 @@ const Completion = () => {
const [filterLang, setFilterLang] = useState('');
const [filterAccept, setFilterAccept] = useState<
'accepted' | 'unaccepted' | ''
>('');
>('accepted');
const { data: userOptions = { users: [] } } = useRequest(() =>
getListUser({

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -322,7 +322,11 @@ const Invite = () => {
borderRadius: 1,
height: 48,
}}
onClick={onNext}
onClick={() => {
setTimeout(() => {
onNext();
}, 500);
}}
>
</Button>

View File

@@ -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;

View File

@@ -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();
}}

View 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;

View File

@@ -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: {

View File

@@ -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 };
};