Merge pull request #296 from awesomeYG/feat-employee

Feat employee
This commit is contained in:
Yoko
2025-08-25 19:04:59 +08:00
committed by GitHub
16 changed files with 958 additions and 7932 deletions

7838
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"@c-x/ui": "^1.0.9",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@hookform/resolvers": "^5.2.1",
"@monaco-editor/react": "4.7.0",
"@mui/icons-material": "^6.4.12",
"@mui/lab": "6.0.0-beta.19",
@@ -39,11 +40,12 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"unist-util-visit": "^5.0.0",
"vite-plugin-dts": "^4.5.4"
"vite-plugin-dts": "^4.5.4",
"zod": "^4.0.17"
},
"devDependencies": {
"@c-x/cx-swagger-api": "^0.0.10",
"@eslint/js": "^9.25.0",
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.2",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.1.2",

28
ui/pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@emotion/styled':
specifier: ^11.14.0
version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.10)(react@19.1.1))(@types/react@19.1.10)(react@19.1.1)
'@hookform/resolvers':
specifier: ^5.2.1
version: 5.2.1(react-hook-form@7.62.0(react@19.1.1))
'@monaco-editor/react':
specifier: 4.7.0
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -95,12 +98,15 @@ importers:
vite-plugin-dts:
specifier: ^4.5.4
version: 4.5.4(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.5.1))
zod:
specifier: ^4.0.17
version: 4.0.17
devDependencies:
'@c-x/cx-swagger-api':
specifier: ^0.0.10
version: 0.0.10(@types/node@24.2.1)(typescript@5.8.3)
'@eslint/js':
specifier: ^9.25.0
specifier: ^9.33.0
version: 9.33.0
'@types/react':
specifier: ^19.1.2
@@ -534,6 +540,11 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@hookform/resolvers@5.2.1':
resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==}
peerDependencies:
react-hook-form: ^7.55.0
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -921,6 +932,9 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
@@ -2744,6 +2758,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zod@4.0.17:
resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
@@ -3128,6 +3145,11 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@19.1.1))':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.62.0(react@19.1.1)
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -3492,6 +3514,8 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
'@standard-schema/utils@0.3.0': {}
'@types/argparse@1.0.38': {}
'@types/babel__core@7.20.5':
@@ -5715,6 +5739,8 @@ snapshots:
yocto-queue@0.1.0: {}
zod@4.0.17: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0

166
ui/src/api/AiEmployee.ts Normal file
View File

@@ -0,0 +1,166 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
import request, { ContentType, RequestParams } from "./httpClient";
import {
DomainAIEmployee,
DomainCreateAIEmployeeReq,
DomainListAIEmployeeResp,
DomainUUIDReq,
DomainUpdateAIEmployeeReq,
GetAiemployeeInfoParams,
GetAiemployeeListParams,
WebResp,
} from "./types";
/**
* @description 获取AI员工列表
*
* @tags AIEmployee
* @name GetAiemployeeList
* @summary 获取AI员工列表
* @request GET:/api/v1/aiemployee
* @response `200` `(WebResp & {
data?: DomainListAIEmployeeResp,
})` OK
*/
export const getAiemployeeList = (
query: GetAiemployeeListParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainListAIEmployeeResp;
}
>({
path: `/api/v1/aiemployee`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 更新AI员工
*
* @tags AIEmployee
* @name PutAiemployeeUpdate
* @summary 更新AI员工
* @request PUT:/api/v1/aiemployee
* @response `200` `(WebResp & {
data?: DomainAIEmployee,
})` OK
*/
export const putAiemployeeUpdate = (
param: DomainUpdateAIEmployeeReq,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainAIEmployee;
}
>({
path: `/api/v1/aiemployee`,
method: "PUT",
body: param,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 创建AI员工
*
* @tags AIEmployee
* @name PostAiemployeeCreate
* @summary 创建AI员工
* @request POST:/api/v1/aiemployee
* @response `200` `(WebResp & {
data?: DomainAIEmployee,
})` OK
*/
export const postAiemployeeCreate = (
param: DomainCreateAIEmployeeReq,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainAIEmployee;
}
>({
path: `/api/v1/aiemployee`,
method: "POST",
body: param,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 删除AI员工
*
* @tags AIEmployee
* @name DeleteAiemployeeDelete
* @summary 删除AI员工
* @request DELETE:/api/v1/aiemployee
* @response `200` `WebResp` OK
*/
export const deleteAiemployeeDelete = (
param: DomainUUIDReq,
params: RequestParams = {},
) =>
request<WebResp>({
path: `/api/v1/aiemployee`,
method: "DELETE",
body: param,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 获取AI员工详情
*
* @tags AIEmployee
* @name GetAiemployeeInfo
* @summary 获取AI员工详情
* @request GET:/api/v1/aiemployee/info
* @response `200` `(WebResp & {
data?: DomainAIEmployee,
})` OK
*/
export const getAiemployeeInfo = (
query: GetAiemployeeInfoParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainAIEmployee;
}
>({
path: `/api/v1/aiemployee/info`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});

View File

@@ -1,4 +1,5 @@
export * from './Admin'
export * from './AiEmployee'
export * from './Billing'
export * from './Cli'
export * from './CodeSnippet'

View File

@@ -156,22 +156,39 @@ export enum ConstsAIEmployeePosition {
}
export interface DomainAIEmployee {
/** 管理员 */
admin?: DomainAdminUser;
/** 完成的任务数 */
completed_count?: number;
/** 创建时间 */
created_at?: number;
id?: string;
/** 是否在issue评论中@工程师 */
issue_at_comment?: boolean;
/** 是否处理新Issues */
issue_open?: boolean;
/** 最后活跃时间 */
last_active_at?: number;
/** 是否mr/pr在评论中@工程师 */
mr_pr_at_comment?: boolean;
/** 是否处理全部新增PR/MR */
mr_pr_open?: boolean;
/** 名称 */
name?: string;
/** 仓库平台 */
platform?: ConstsRepoPlatform;
/** 职位 */
position?: ConstsAIEmployeePosition;
/** 仓库 URL */
repository_url?: string;
/** 仓库用户名 */
repository_user?: string;
/** 仓库 token */
token?: string;
/** 仓库 webhook 密钥 */
webhook_secret?: string;
/** 仓库 webhook URL */
webhook_url?: string;
}
export interface DomainAcceptCompletionReq {
@@ -396,10 +413,17 @@ export interface DomainCreateAIEmployeeReq {
mr_pr_at_comment?: boolean;
/** 是否处理全部新增PR/MR */
mr_pr_open?: boolean;
/** AI 员工名称 */
name?: string;
/** 仓库平台 */
platform?: ConstsRepoPlatform;
/** 职位 */
position?: ConstsAIEmployeePosition;
/** 仓库 URL */
repo_url?: string;
/** 仓库用户名 */
repo_user?: string;
/** 仓库 token */
token?: string;
}
@@ -1126,6 +1150,7 @@ export interface DomainUpdateAIEmployeeReq {
platform?: ConstsRepoPlatform;
position?: ConstsAIEmployeePosition;
repo_url?: string;
repo_user?: string;
token?: string;
}

View File

@@ -8,7 +8,7 @@ const ADMIN_BREADCRUMB_MAP: Record<string, { title: string; to: string }> = {
chat: { title: '对话记录', to: '/chat' },
completion: { title: '补全记录', to: '/completion' },
codescan: { title: '代码安全', to: '/codescan' },
model: { title: '模型管理', to: '/model' },
employee: { title: 'AI 员工', to: '/employee' },
'member-management': { title: '成员管理', to: '/member-management' },
admin: { title: '管理员', to: '/admin' },
};

View File

@@ -41,11 +41,11 @@ const ADMIN_MENUS = [
show: true,
disabled: false,
},
{
label: '模型管理',
value: '/model',
pathname: 'model',
icon: 'icon-moxingguanli',
{
label: 'AI 员工',
value: '/employee',
pathname: 'employee',
icon: 'icon-zhanghao',
show: true,
disabled: false,
},
@@ -137,7 +137,7 @@ const Sidebar = () => {
}
return isConfigModel
? ADMIN_MENUS.map((item) => ({ ...item, disabled: false }))
: ADMIN_MENUS.map((item) => ({ ...item, disabled: true }));
: ADMIN_MENUS.map((item) => ({ ...item, disabled: item.pathname !== 'general-setting' }));
}, [pathname, isConfigModel]);
return (
@@ -208,7 +208,7 @@ const Sidebar = () => {
>
<Button
variant={isActive ? 'contained' : 'text'}
disabled={it.pathname === 'model' ? false : it.disabled}
disabled={it.disabled}
sx={{
width: '100%',
height: 50,

View File

@@ -88,8 +88,8 @@ const App = () => {
const handleModelConfig = (res: [DomainModel[], DomainModel[]]) => {
if ((res[0] || [])?.length == 0 || (res[1] || [])?.length == 0) {
if (location.pathname !== '/model') {
window.location.href = '/model';
if (location.pathname !== '/general-setting') {
window.location.href = '/general-setting';
}
setIsConfigModel(false);
return false;
@@ -101,8 +101,8 @@ const App = () => {
setIsConfigModel(true);
return true;
} else {
if (location.pathname !== '/model') {
window.location.href = '/model';
if (location.pathname !== '/general-setting') {
window.location.href = '/general-setting';
}
setIsConfigModel(false);
return false;

View File

@@ -0,0 +1,302 @@
import {
ConstsAIEmployeePosition,
ConstsRepoPlatform,
DomainAIEmployee,
DomainUpdateAIEmployeeReq,
postAiemployeeCreate,
putAiemployeeUpdate,
} from "@/api";
import { Ellipsis, message, Modal } from "@c-x/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import {
Box,
Checkbox,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
IconButton,
Radio,
RadioGroup,
Stack,
TextField
} from "@mui/material";
import { useEffect, useState } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
const formSchema = z.object({
issue_at_comment: z.boolean().default(false),
/** 是否处理新Issues */
issue_open: z.boolean().default(false),
/** 是否mr/pr在评论中@工程师 */
mr_pr_at_comment: z.boolean().default(false),
/** 是否处理全部新增PR/MR */
mr_pr_open: z.boolean().default(false),
name: z.string().min(1, "必填项").default(""),
platform: z
.enum(ConstsRepoPlatform)
.default(ConstsRepoPlatform.RepoPlatformGitLab),
position: z
.enum(ConstsAIEmployeePosition)
.default(ConstsAIEmployeePosition.AIEmployeePositionEngineer),
repo_url: z.string().min(1, "必填项").default(""),
token: z.string().min(1, "必填项").default(""),
});
const EmloyeeModal = ({
open,
onClose,
onChanged, // 添加一个回调函数,用于在创建成功后刷新列表
record,
}: {
open: boolean;
onClose: () => void;
onChanged?: () => void; // 可选的回调函数
record?: DomainUpdateAIEmployeeReq;
}) => {
const [webhookOpen, setWebhookOpen] = useState(false);
const [webhookUrl, setWebhookUrl] = useState<
Pick<DomainAIEmployee, "webhook_url" | "webhook_secret"> | undefined
>();
const {
reset,
register,
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: zodResolver(formSchema),
defaultValues: formSchema.parse({}),
});
const handleChange = handleSubmit(
async (data) => {
const res = await (record
? putAiemployeeUpdate({ ...data, id: record.id })
: postAiemployeeCreate(data));
onChanged?.(); // 调用回调函数,刷新列表
setWebhookUrl(res);
setWebhookOpen(true);
},
(e) => {
console.log(e);
}
);
const checkitems = [
{ key: "issue_open", label: "自动跟进所有的 Issue" },
{ key: "mr_pr_open", label: "自动跟进所有的 Merge/Pull Request" },
{ key: "issue_at_comment", label: "允许在 Issue 中被 @" },
{ key: "mr_pr_at_comment", label: "允许在 Merge/Pull Request 中被 @" },
] as const;
useEffect(() => {
if (open) reset(record || formSchema.parse({}));
}, [record, reset, open]);
const onCloseWebhook = () => {
setWebhookOpen(false);
setWebhookUrl(undefined);
};
return (
<>
<Modal
title={record ? "编辑 AI 员工" : "创建 AI 员工"}
width={600}
open={open}
// onOk={() => setOpenWebhook(true)}
onOk={handleChange}
onCancel={onClose}
okText={record ? "更新" : "创建"}
cancelText="取消"
>
<Stack spacing={2} sx={{ fontSize: "13px" }}>
<TextField
label="AI 员工名称"
fullWidth
size="small"
{...register("name")}
error={!!errors.name}
helperText={errors.name?.message}
/>
<Stack
direction={"row"}
component={FormControl}
alignItems="center"
spacing={3}
>
<FormLabel id="demo-row-radio-buttons-group-label">
AI
</FormLabel>
<Controller
control={control}
name="position"
render={({ field }) => (
<RadioGroup
row
value={field.value}
onChange={(e) => {
field.onChange(e.target.value);
}}
>
{[
ConstsAIEmployeePosition.AIEmployeePositionEngineer,
ConstsAIEmployeePosition.AIEmployeePositionTester,
ConstsAIEmployeePosition.AIEmployeePositionProductManager,
].map((item) => (
<FormControlLabel
key={item}
value={item}
control={<Radio />}
label={item}
disabled={
item !==
ConstsAIEmployeePosition.AIEmployeePositionEngineer
}
/>
))}
</RadioGroup>
)}
/>
</Stack>
<FormGroup>
{checkitems.map((item) => (
<Controller
key={item.key}
control={control}
name={item.key}
render={({ field }) => (
<FormControlLabel
sx={{ mt: -2 }}
control={<Checkbox {...field} checked={field.value} />}
label={item.label}
/>
)}
/>
))}
</FormGroup>
<TextField
label="工作项目的 Git 仓库"
fullWidth
size="small"
{...register("repo_url")}
error={!!errors.repo_url}
helperText={errors.repo_url?.message}
/>
<Stack
direction={"row"}
component={FormControl}
alignItems="center"
spacing={3}
>
<FormLabel id="demo-row-radio-buttons-group-label">
Git
</FormLabel>
<Controller
control={control}
name="platform"
render={({ field }) => (
<RadioGroup
row
value={field.value}
onChange={(e) => {
field.onChange(e.target.value);
}}
>
{[
ConstsRepoPlatform.RepoPlatformGitHub,
ConstsRepoPlatform.RepoPlatformGitLab,
ConstsRepoPlatform.RepoPlatformGitee,
ConstsRepoPlatform.RepoPlatformGitea,
].map((item) => (
<FormControlLabel
key={item}
value={item}
control={<Radio />}
label={item}
disabled={item !== ConstsRepoPlatform.RepoPlatformGitLab}
/>
))}
</RadioGroup>
)}
/>
</Stack>
<TextField
label="Git 仓库的访问令牌"
fullWidth
size="small"
{...register("token")}
error={!!errors.token}
helperText={errors.token?.message}
/>
</Stack>
</Modal>
<Modal
title="Webhook 配置信息"
width={830}
open={webhookOpen}
onOk={onCloseWebhook}
onCancel={onCloseWebhook}
showCancel={false}
okText="确定"
>
{webhookUrl?.webhook_secret && (
<Stack
spacing={2}
sx={{
mt: 2,
fontSize: "14px",
"& > .MuiStack-root > div:nth-child(2)": {
fontWeight: 600,
bgcolor: "background.paper",
px: 1,
py: 0.5,
borderRadius: "4px",
},
}}
>
<Stack direction="row" alignItems={"center"} spacing={2}>
<Box sx={{ flexShrink: 0, minWidth: "130px" }}>Webhook URL: </Box>
<Ellipsis>{webhookUrl?.webhook_url}</Ellipsis>
<CopyToClipboard
text={webhookUrl?.webhook_url || ""}
onCopy={() => {
message.success("复制成功");
}}
>
<IconButton
color="primary"
size="small"
sx={{ alignSelf: "flex-end" }}
>
<ContentCopyIcon />
</IconButton>
</CopyToClipboard>
</Stack>
<Stack direction="row" alignItems={"center"} spacing={2}>
<Box sx={{ flexShrink: 0, minWidth: "130px" }}>
Webhook Secret:{" "}
</Box>
<Ellipsis>{webhookUrl?.webhook_secret}</Ellipsis>
<CopyToClipboard
text={webhookUrl?.webhook_secret || ""}
onCopy={() => {
message.success("复制成功");
}}
>
<IconButton
color="primary"
size="small"
sx={{ alignSelf: "flex-end" }}
>
<ContentCopyIcon />
</IconButton>
</CopyToClipboard>
</Stack>
</Stack>
)}
</Modal>
</>
);
};
export default EmloyeeModal;

View File

@@ -0,0 +1,302 @@
import { Ellipsis, Icon, message, Modal, Table } from "@c-x/ui";
import { useEffect, useState } from "react";
import Card from "@/components/card";
import { Box, Button, Stack } from "@mui/material";
import { deleteAiemployeeDelete, getAiemployeeList } from "@/api/AiEmployee";
import {
ConstsRepoPlatform,
DomainAIEmployee,
DomainUpdateAIEmployeeReq,
} from "@/api/types";
import { ColumnsType } from "@c-x/ui/dist/Table";
import dayjs from "dayjs";
import EmloyeeModal from "./emloyeeModal";
const gitPlatformIcons = {
[ConstsRepoPlatform.RepoPlatformGitHub]: "icon-github",
[ConstsRepoPlatform.RepoPlatformGitLab]: "icon-gitlab",
[ConstsRepoPlatform.RepoPlatformGitee]: "icon-gitee",
[ConstsRepoPlatform.RepoPlatformGitea]: "icon-gitea",
} as const;
const EmployeeTaskList = () => {
const [page, setPage] = useState(1);
const [size, setSize] = useState(20);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<DomainAIEmployee[]>([]);
const [detail, setDetail] = useState<DomainUpdateAIEmployeeReq | undefined>();
const [open, setOpen] = useState(false);
const onClose = () => {
setOpen(false);
setDetail(undefined);
};
const onChanged = () => {
onClose();
fetchData({});
};
const fetchData = async (params: { page?: number; size?: number }) => {
setLoading(true);
const res = await getAiemployeeList({
page: params.page || page,
size: params.size || size,
});
setLoading(false);
setTotal(res.total_count || 0);
setDataSource(res.items || []);
};
useEffect(() => {
setPage(1);
fetchData({
page: 1,
});
}, []);
const handleEdit = (record: DomainAIEmployee) => {
setOpen(true);
setDetail({
...record,
repo_url: record.repository_url,
repo_user: record.repository_user,
});
};
const handleDelete = (record: DomainAIEmployee) => {
Modal.confirm({
title: "提示",
okText: "删除",
okButtonProps: {
color: "error",
},
content: (
<>
AI {" "}
<Box component="span" sx={{ fontWeight: 700, color: "text.primary" }}>
{record!.name}
</Box>{" "}
</>
),
onOk: () => {
deleteAiemployeeDelete({ id: record.id! }).then(() => {
message.success("删除成功");
fetchData({});
});
},
});
};
const columns: ColumnsType<DomainAIEmployee> = [
{
dataIndex: "name",
title: "AI 员工",
width: 240,
render: (name, record) => {
return (
<Stack direction="column">
<Stack direction="row" alignItems="center" spacing={1}>
<Icon
type="icon-jiqiren"
sx={{ fontSize: 20, color: "text.main" }}
/>
<Box
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{record?.name}
</Box>
</Stack>
<Box
sx={{
color: "text.tertiary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontSize: "0.8rem",
mt: "4px",
}}
>
<Box
sx={{
lineHeight: "16px",
}}
>
{record?.position}
</Box>
</Box>
</Stack>
);
},
},
{
title: "工作项目",
dataIndex: "platform",
width: 300,
render: (platform, record) => {
return (
<Stack direction="column">
<Stack direction="row" alignItems="center" spacing={1}>
<Icon
type={gitPlatformIcons[record.platform as ConstsRepoPlatform]}
sx={{ fontSize: 16, color: "text.main" }}
/>
<Box
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{record?.repository_url
? record?.repository_url.split("/").pop()
: record?.platform}
</Box>
</Stack>
<Ellipsis
sx={{
color: "text.secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontSize: "0.8rem",
}}
>
{record?.repository_url}
</Ellipsis>
</Stack>
);
},
},
{
title: "创建者",
dataIndex: "creater",
render: (creater, record) => {
return (
<Stack direction="column">
<Box
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{record?.admin?.username}
</Box>
<Box
sx={{
color: "text.secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontSize: "0.8rem",
}}
>
{dayjs(Number(record?.created_at) * 1000).fromNow()}
</Box>
</Stack>
);
},
},
{
title: "工作状态",
dataIndex: "status",
render: (status, record) => {
return (
<Stack direction="column">
<Box
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{record?.completed_count || 0}
</Box>
<Box
sx={{
color: "text.secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontSize: "0.8rem",
}}
>
{dayjs(Number(record?.last_active_at) * 1000).fromNow()}
</Box>
</Stack>
);
},
},
{
title: "操作",
dataIndex: "opt",
render: (status, record) => {
return (
<Stack direction="row" spacing={1}>
<Button variant="text" onClick={() => handleEdit(record)}>
</Button>
<Button
variant="text"
color="error"
onClick={() => handleDelete(record)}
>
</Button>
</Stack>
);
},
},
];
return (
<Card sx={{ flex: 1, height: "100%" }}>
<Stack height="100%">
<Button
variant="contained"
size="small"
sx={{ mb: 2, alignSelf: "flex-end" }}
onClick={() => setOpen(true)}
>
AI
</Button>
<Table
sx={{ mx: -2, flexGrow: 1, overflow: "auto" }}
PaginationProps={{
sx: {
pt: 2,
mx: 2,
},
}}
loading={loading}
columns={columns}
dataSource={dataSource}
rowKey="id"
pagination={{
page,
pageSize: size,
total,
onChange: (page: number, size: number) => {
setPage(page);
setSize(size);
fetchData({
page: page,
size: size,
});
},
}}
/>
<EmloyeeModal
open={open}
record={detail}
onClose={onClose}
onChanged={onChanged}
/>
</Stack>
</Card>
);
};
export default EmployeeTaskList;

View File

@@ -1,26 +1,26 @@
import Card from '@/components/card';
import { Box, Button, Stack, TextField } from "@mui/material"
import { message } from '@c-x/ui';
import { useEffect, useState } from "react"
import { getGetSetting, putUpdateSetting } from '@/api/Admin';
import { DomainUpdateSettingReq } from '@/api/types';
import Card from "@/components/card";
import { Box, Button, Stack, TextField } from "@mui/material";
import { message } from "@c-x/ui";
import { useEffect, useState } from "react";
import { getGetSetting, putUpdateSetting } from "@/api/Admin";
import { DomainUpdateSettingReq } from "@/api/types";
const CardServiceSettings = () => {
const [baseURL, setBaseURL] = useState('');
const [initialBaseURL, setInitialBaseURL] = useState('');
const [baseURL, setBaseURL] = useState("");
const [initialBaseURL, setInitialBaseURL] = useState("");
useEffect(() => {
const fetchInitialBaseURL = async () => {
try {
const response = await getGetSetting();
const initialValue = response.base_url || '';
const initialValue = response.base_url || "";
setBaseURL(initialValue);
setInitialBaseURL(initialValue);
} catch (err: any) {
message.error('Failed to fetch initial base URL:', err);
message.error("Failed to fetch initial base URL:", err);
// 如果获取失败,可以设置一个默认值或者保持空字符串
setBaseURL('');
setInitialBaseURL('');
setBaseURL("");
setInitialBaseURL("");
}
};
@@ -35,11 +35,11 @@ const CardServiceSettings = () => {
try {
const parsedURL = new URL(url);
// Check if the protocol is either http or https
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
if (parsedURL.protocol !== "http:" && parsedURL.protocol !== "https:") {
return false;
}
// Check if the URL is a base URL (no path or only root path)
if (parsedURL.pathname !== '/' && parsedURL.pathname !== '') {
if (parsedURL.pathname !== "/" && parsedURL.pathname !== "") {
return false;
}
return true;
@@ -51,7 +51,7 @@ const CardServiceSettings = () => {
const handleSave = async () => {
// Check if the baseURL is valid before saving
if (baseURL && !isValidURL(baseURL)) {
message.error('请输入一个有效的 URL 地址');
message.error("请输入一个有效的 URL 地址");
return;
}
@@ -60,63 +60,84 @@ const CardServiceSettings = () => {
base_url: baseURL,
};
await putUpdateSetting(setting);
message.success('保存成功');
message.success("保存成功");
setInitialBaseURL(baseURL);
} catch (err: any) {
message.error('保存失败:', err);
message.error("保存失败:", err);
}
};
const hasValueChanged = baseURL !== initialBaseURL;
return <Card sx={{p : 0, borderBottom: '1px solid #e0e0e0'}}>
<Box sx={{
fontWeight: 'bold',
px: 2,
py: 1.5,
bgcolor: 'rgb(248, 249, 250)',
borderTopLeftRadius: '10px',
borderTopRightRadius: '10px',
}}>MonkeyCode </Box>
<Stack direction='column'>
<Box sx={{ width: '100%' }}>
<Stack direction='row' alignItems={'center'} justifyContent={'space-between'} sx={{
m: 2,
height: 32,
fontWeight: 'bold',
}}>
<Box sx={{
'&::before': {
content: '""',
display: 'inline-block',
width: 4,
height: 12,
bgcolor: 'common.black',
borderRadius: '2px',
mr: 1,
},
}}>MonkeyCode </Box>
{hasValueChanged && <Button variant="contained" size="small" onClick={handleSave}></Button>}
</Stack>
<Box sx={{ m: 2 }}>
<TextField
fullWidth
value={baseURL}
onChange={handleBaseURLChange}
placeholder={baseURL ? '' : window.location.origin}
/>
<Box sx={{
mt: 1,
fontSize: '0.75rem',
color: 'warning.main',
fontWeight: 'normal'
}}>
VSCode MonkeyCode
return (
<Card sx={{ p: 0, mb: 2, borderBottom: "1px solid #e0e0e0" }}>
<Box
sx={{
fontWeight: "bold",
px: 2,
py: 1.5,
bgcolor: "rgb(248, 249, 250)",
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
}}
>
MonkeyCode
</Box>
<Stack direction="column">
<Box sx={{ width: "100%" }}>
<Stack
direction="row"
alignItems={"center"}
justifyContent={"space-between"}
sx={{
m: 2,
height: 32,
fontWeight: "bold",
}}
>
<Box
sx={{
"&::before": {
content: '""',
display: "inline-block",
width: 4,
height: 12,
bgcolor: "common.black",
borderRadius: "2px",
mr: 1,
},
}}
>
MonkeyCode
</Box>
{hasValueChanged && (
<Button variant="contained" size="small" onClick={handleSave}>
</Button>
)}
</Stack>
<Box sx={{ m: 2 }}>
<TextField
fullWidth
value={baseURL}
onChange={handleBaseURLChange}
placeholder={baseURL ? "" : window.location.origin}
/>
<Box
sx={{
mt: 1,
fontSize: "0.75rem",
color: "warning.main",
fontWeight: "normal",
}}
>
VSCode MonkeyCode
</Box>
</Box>
</Box>
</Box>
</Stack>
</Card>
}
</Stack>
</Card>
);
};
export default CardServiceSettings;
export default CardServiceSettings;

View File

@@ -1,16 +1,18 @@
import CardServiceSettings from './components/cardServiceSettings';
import { Grid2 as Grid } from '@mui/material';
import CardAdminUser from './components/cardAdminUser';
import CardServiceSettings from "./components/cardServiceSettings";
import { Grid2 as Grid } from "@mui/material";
import CardAdminUser from "./components/cardAdminUser";
import Model from "@/pages/model";
const GeneralSetting = () => {
return (
<Grid container spacing={2} sx={{ height: '100%' }}>
<Grid size={6}>
<CardServiceSettings />
</Grid>
<Grid size={6}>
<CardAdminUser />
</Grid>
<Grid container spacing={2} sx={{ height: "100%" }}>
<Grid size={6}>
<CardServiceSettings />
<Model />
</Grid>
<Grid size={6}>
<CardAdminUser />
</Grid>
</Grid>
);
};

View File

@@ -10,7 +10,7 @@ import StyledLabel from '@/components/label';
import { Icon, Modal, message } from '@c-x/ui';
import { addCommasToNumber } from '@/utils';
import NoData from '@/assets/images/nodata.png';
import { ModelModal, Model, DEFAULT_MODEL_PROVIDERS} from '@yokowu/modelkit-ui';
import { ModelModal, DEFAULT_MODEL_PROVIDERS} from '@yokowu/modelkit-ui';
import { localModelToModelKitModel, modelService } from '@/pages/model/components/services/modelService';
const ModelItem = ({
@@ -300,7 +300,7 @@ const ModelCard: React.FC<IModelCardProps> = ({
{data?.length > 0 ? (
<Grid container spacing={2} sx={{ mt: 2 }}>
{data.map((item) => (
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 6, xl: 4 }} key={item.id}>
<Grid size={{ lg: 12, xl: 6 }} key={item.id}>
<ModelItem data={item} onEdit={onEdit} refresh={refreshModel} />
</Grid>
))}

View File

@@ -41,6 +41,7 @@ const UserLogin = LazyLoadable(lazy(() => import('@/pages/user/login')));
const Expectation = LazyLoadable(lazy(() => import('@/pages/expectation')));
const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat')));
const AdminCodeScan = LazyLoadable(lazy(() => import('@/pages/codescan')));
const AdminEmployee = LazyLoadable(lazy(() => import('@/pages/employee')));
const UserCodeScan = LazyLoadable(lazy(() => import('@/pages/user/codescan')));
const UserCompletion = LazyLoadable(
lazy(() => import('@/pages/user/completion'))
@@ -77,8 +78,8 @@ const routerConfig = [
element: <Completion />,
},
{
path: 'model',
element: <Model />,
path: 'employee',
element: <AdminEmployee />,
},
{
path: 'member-management',

View File

@@ -178,6 +178,8 @@ const lightTheme = createTheme(
MuiFormLabel: {
styleOverrides: {
root: {
color: 'unset',
fontSize: '0.8rem',
fontFamily: 'var(--font-gilory), var(--font-HarmonyOS)',
},
asterisk: {
@@ -192,6 +194,20 @@ const lightTheme = createTheme(
},
},
},
MuiRadio: {
styleOverrides: {
root: {
fontSize: '0.8rem',
},
},
},
MuiFormControlLabel: {
styleOverrides: {
label: {
fontSize: '0.8rem',
},
},
},
MuiTableCell: {
styleOverrides: {
root: {