feat: 模型未配置,添加禁用功能, 修复对话记录滚动问题

This commit is contained in:
Gavan
2025-07-23 18:22:45 +08:00
parent 129a7929ae
commit c61bfab2ba
9 changed files with 223 additions and 77 deletions

View File

@@ -6,6 +6,7 @@ import { Modal } from '@c-x/ui';
import { useMemo, useState } from 'react';
import Qrcode from '@/assets/images/qrcode.png';
import Version from './version';
import { useCommonContext } from '@/hooks/context';
const ADMIN_MENUS = [
{
@@ -14,6 +15,7 @@ const ADMIN_MENUS = [
pathname: 'dashboard',
icon: 'icon-yibiaopan',
show: true,
disabled: false,
},
{
label: '对话记录',
@@ -21,6 +23,7 @@ const ADMIN_MENUS = [
pathname: 'chat',
icon: 'icon-duihuajilu1',
show: true,
disabled: false,
},
{
label: '补全记录',
@@ -28,6 +31,7 @@ const ADMIN_MENUS = [
pathname: 'completion',
icon: 'icon-buquanjilu',
show: true,
disabled: false,
},
{
label: '代码安全',
@@ -35,6 +39,7 @@ const ADMIN_MENUS = [
pathname: 'code-security',
icon: 'icon-daimaanquan1',
show: true,
disabled: false,
},
{
label: '模型管理',
@@ -42,6 +47,7 @@ const ADMIN_MENUS = [
pathname: 'model',
icon: 'icon-moxingguanli',
show: true,
disabled: false,
},
{
label: '成员管理',
@@ -49,6 +55,7 @@ const ADMIN_MENUS = [
pathname: 'user-management',
icon: 'icon-yonghuguanli1',
show: true,
disabled: false,
},
{
label: '管理员',
@@ -56,6 +63,7 @@ const ADMIN_MENUS = [
pathname: 'admin',
icon: 'icon-guanliyuan1',
show: true,
disabled: false,
},
];
@@ -66,6 +74,7 @@ const USER_MENUS = [
pathname: '/user/dashboard',
icon: 'icon-yibiaopan',
show: true,
disabled: false,
},
{
label: '对话记录',
@@ -73,6 +82,7 @@ const USER_MENUS = [
pathname: '/user/chat',
icon: 'icon-duihuajilu1',
show: true,
disabled: false,
},
{
label: '补全记录',
@@ -80,6 +90,7 @@ const USER_MENUS = [
pathname: '/user/completion',
icon: 'icon-buquanjilu',
show: true,
disabled: false,
},
// {
// label: '设置',
@@ -111,12 +122,15 @@ const Sidebar = () => {
const { pathname } = useLocation();
const theme = useTheme();
const [showQrcode, setShowQrcode] = useState(false);
const { isConfigModel } = useCommonContext();
const menus = useMemo(() => {
if (pathname.startsWith('/user/')) {
return USER_MENUS;
}
return ADMIN_MENUS;
}, [pathname]);
return isConfigModel
? ADMIN_MENUS.map((item) => ({ ...item, disabled: false }))
: ADMIN_MENUS.map((item) => ({ ...item, disabled: true }));
}, [pathname, isConfigModel]);
return (
<Stack
@@ -178,9 +192,15 @@ const Sidebar = () => {
zIndex: isActive ? 2 : 1,
color: isActive ? '#FFFFFF' : 'text.primary',
}}
onClick={(e) => {
if (it.disabled) {
e.preventDefault();
}
}}
>
<Button
variant={isActive ? 'contained' : 'text'}
disabled={it.pathname === 'model' ? false : it.disabled}
sx={{
width: '100%',
height: 50,

View File

@@ -1,5 +1,5 @@
import { createContext } from 'react';
import { DomainUser, DomainAdminUser } from '@/api/types';
import { DomainUser, DomainAdminUser, DomainModel } from '@/api/types';
export const AuthContext = createContext<
[
@@ -20,6 +20,13 @@ export const AuthContext = createContext<
]);
export const CommonContext = createContext<{
contactModalOpen: boolean;
setContactModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}>({ contactModalOpen: false, setContactModalOpen: () => {} });
coderModel: DomainModel[];
llmModel: DomainModel[];
isConfigModel: boolean;
refreshModel: () => void;
}>({
coderModel: [],
llmModel: [],
isConfigModel: false,
refreshModel: () => {},
});

View File

@@ -9,15 +9,15 @@ import '@/assets/styles/markdown.css';
import { ThemeProvider } from '@c-x/ui';
import { getUserProfile } from '@/api/UserManage';
import { getAdminProfile } from '@/api/Admin';
import { getMyModelList } from '@/api/Model';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { AuthContext } from './context';
import { DomainUser, DomainAdminUser } from './api/types';
import { AuthContext, CommonContext } from './context';
import { DomainUser, DomainAdminUser, DomainModel } from './api/types';
import { lightTheme } from './theme';
import router from './router';
import { getRedirectUrl } from './utils';
import { Loading } from '@c-x/ui';
dayjs.locale('zh-cn');
dayjs.extend(duration);
@@ -26,12 +26,56 @@ dayjs.extend(relativeTime);
const App = () => {
const [user, setUser] = useState<DomainUser | DomainAdminUser | null>(null);
const [loading, setLoading] = useState(true);
const [coderModel, setCoderModel] = useState<DomainModel[]>([]);
const [llmModel, setLlmModel] = useState<DomainModel[]>([]);
const [isConfigModel, setIsConfigModel] = useState(false);
const onGotoRedirect = (source: 'user' | 'admin') => {
const redirectUrl = getRedirectUrl(source);
window.location.href = redirectUrl.href;
};
const getModelList = () => {
return Promise.all([
getMyModelList({
model_type: 'coder',
}),
getMyModelList({
model_type: 'llm',
}),
])
.then((res) => {
setCoderModel(res[0] || []);
setLlmModel(res[1] || []);
return res;
})
.then(handleModelConfig);
};
const handleModelConfig = (res: [DomainModel[], DomainModel[]]) => {
if ((res[0] || [])?.length == 0 || (res[1] || [])?.length == 0) {
if (location.pathname !== '/model') {
window.location.href = '/model';
}
setIsConfigModel(false);
return false;
} else {
const isActive =
res[0].every((item) => item.is_active) &&
res[1].every((item) => item.is_active);
if (isActive) {
setIsConfigModel(true);
return true;
} else {
if (location.pathname !== '/model') {
window.location.href = '/model';
}
setIsConfigModel(false);
return false;
}
}
};
const getUser = () => {
setLoading(true);
if (location.pathname.startsWith('/user')) {
@@ -49,9 +93,13 @@ const App = () => {
return getAdminProfile()
.then((res) => {
setUser(res);
if (location.pathname.startsWith('/login')) {
onGotoRedirect('admin');
}
getModelList().then((res) => {
if (res) {
if (location.pathname.startsWith('/login')) {
onGotoRedirect('admin');
}
}
});
})
.finally(() => {
setLoading(false);
@@ -71,18 +119,27 @@ const App = () => {
return (
<ThemeProvider theme={lightTheme}>
<AuthContext.Provider
value={[
user,
{
loading,
setUser,
refreshUser: getUser,
},
]}
<CommonContext.Provider
value={{
coderModel,
llmModel,
isConfigModel,
refreshModel: getModelList,
}}
>
<RouterProvider router={router} />
</AuthContext.Provider>
<AuthContext.Provider
value={[
user,
{
loading,
setUser,
refreshUser: getUser,
},
]}
>
<RouterProvider router={router} />
</AuthContext.Provider>
</CommonContext.Provider>
</ThemeProvider>
);
};

View File

@@ -4,7 +4,16 @@ import { getListChatRecord } from '@/api/Billing';
import dayjs from 'dayjs';
import Card from '@/components/card';
import { Autocomplete, Box, FormControl, InputLabel, MenuItem, Select, Stack, TextField } from '@mui/material';
import {
Autocomplete,
Box,
FormControl,
InputLabel,
MenuItem,
Select,
Stack,
TextField,
} from '@mui/material';
import StyledLabel from '@/components/label';
import ChatDetailModal from './chatDetailModal';
@@ -180,7 +189,14 @@ const Chat = () => {
label='工作模式'
value={filterMode}
onChange={(e) =>
setfilterMode(e.target.value as 'code' | 'ask' | 'architect' | 'debug' | 'orchestrator')
setfilterMode(
e.target.value as
| 'code'
| 'ask'
| 'architect'
| 'debug'
| 'orchestrator'
)
}
>
<MenuItem value=''></MenuItem>
@@ -193,7 +209,7 @@ const Chat = () => {
</FormControl>
</Stack>
<Table
height='100%'
height='calc(100% - 52px)'
sx={{ mx: -2 }}
PaginationProps={{
sx: {

View File

@@ -87,6 +87,7 @@ const Dashboard = () => {
size='small'
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
sx={{ fontSize: 14 }}
disabled
>
<MenuItem value='24h'> 24 </MenuItem>
<MenuItem value='90d'> 90 </MenuItem>

View File

@@ -48,7 +48,7 @@ const ModelItem = ({
},
});
};
const onRemoveModel = () => {
Modal.confirm({
title: '删除模型',
@@ -114,18 +114,6 @@ const ModelItem = ({
boxShadow:
'rgba(54, 59, 76, 0.3) 0px 10px 30px 0px, rgba(54, 59, 76, 0.03) 0px 0px 1px 1px',
},
// '&::before': {
// content: '""',
// position: 'absolute',
// top: 0,
// left: 0,
// right: 0,
// height: 3,
// background: data.is_active
// ? 'linear-gradient(90deg, #4CAF50, #66BB6A)'
// : 'linear-gradient(90deg, #E0E0E0, #BDBDBD)',
// transition: 'all 0.3s ease',
// },
}}
>
<Stack
@@ -141,11 +129,30 @@ const ModelItem = ({
}
sx={{ fontSize: 24 }}
/>
<Stack direction='row' alignItems='center' gap={1} sx={{ fontSize: 14, minWidth: 0 }}>
<Box sx={{ fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<Stack
direction='row'
alignItems='center'
gap={1}
sx={{ fontSize: 14, minWidth: 0 }}
>
<Box
sx={{
fontWeight: 700,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{data.show_name || '未命名'}
</Box>
<Box sx={{ color: 'text.tertiary', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<Box
sx={{
color: 'text.tertiary',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
/ {data.model_name}
</Box>
</Stack>
@@ -187,13 +194,11 @@ const ModelItem = ({
sx={{ mt: 2 }}
>
<Stack direction='row' alignItems='center'>
{data.is_active && <StyledLabel color='success'>使</StyledLabel>}
{data.is_active && (
<StyledLabel color='success'>使</StyledLabel>
)}
</Stack>
<Stack
direction='row'
sx={{ button: { minWidth: 0 } }}
gap={2}
>
<Stack direction='row' sx={{ button: { minWidth: 0 } }} gap={2}>
{!data.is_active && (
<ButtonBase
disableRipple
@@ -206,15 +211,17 @@ const ModelItem = ({
</ButtonBase>
)}
{!data.is_internal && <ButtonBase
disableRipple
sx={{
color: 'info.main',
}}
onClick={() => onEdit(data)}
>
</ButtonBase>}
{!data.is_internal && (
<ButtonBase
disableRipple
sx={{
color: 'info.main',
}}
onClick={() => onEdit(data)}
>
</ButtonBase>
)}
{data.is_active && (
<ButtonBase
@@ -248,9 +255,16 @@ const ModelItem = ({
interface IModelCardProps {
title: string;
modelType: ConstsModelType;
data: DomainModel[];
refreshModel: () => void;
}
const ModelCard: React.FC<IModelCardProps> = ({ title, modelType }) => {
const ModelCard: React.FC<IModelCardProps> = ({
title,
modelType,
data,
refreshModel,
}) => {
const [open, setOpen] = useState(false);
const [editData, setEditData] = useState<DomainModel | null>(null);
@@ -259,12 +273,6 @@ const ModelCard: React.FC<IModelCardProps> = ({ title, modelType }) => {
setEditData(data);
};
const { data: modelList = [], refresh } = useRequest(() =>
getMyModelList({
model_type: modelType,
})
);
return (
<Card>
<Stack direction='row' justifyContent='space-between' alignItems='center'>
@@ -277,21 +285,18 @@ const ModelCard: React.FC<IModelCardProps> = ({ title, modelType }) => {
</Button>
</Stack>
{modelList?.length > 0 ? (
{data?.length > 0 ? (
<Grid container spacing={2} sx={{ mt: 2 }}>
{modelList.map((item) => (
<Grid
size={{ xs: 12, sm: 12, md: 12, lg: 6, xl: 4 }}
key={item.id}
>
<ModelItem data={item} onEdit={onEdit} refresh={refresh} />
{data.map((item) => (
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 6, xl: 4 }} key={item.id}>
<ModelItem data={item} onEdit={onEdit} refresh={refreshModel} />
</Grid>
))}
</Grid>
) : (
<Stack alignItems={'center'} sx={{ mt: 2 }}>
<Stack alignItems={'center'} sx={{ my: 2 }}>
<img src={NoData} width={150} alt='empty' />
<Box sx={{ color: 'text.tertiary', fontSize: 12 }}>
<Box sx={{ color: 'error.main', fontSize: 12 }}>
</Box>
</Stack>
@@ -303,7 +308,7 @@ const ModelCard: React.FC<IModelCardProps> = ({ title, modelType }) => {
setOpen(false);
setEditData(null);
}}
refresh={refresh}
refresh={refreshModel}
data={editData}
type={modelType}
/>

View File

@@ -1,16 +1,54 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import TokenUsage from './components/tokenUsage';
import ModelCard from './components/modelCard';
import { Stack } from '@mui/material';
import { ConstsModelType } from '@/api/types';
import { useCommonContext } from '@/hooks/context';
import { Modal } from '@c-x/ui';
const Model = () => {
const { coderModel, llmModel, refreshModel, isConfigModel } =
useCommonContext();
const isFirst = useRef(true);
useEffect(() => {
if (isFirst.current && !isConfigModel) {
isFirst.current = false;
let text = '';
if (
(coderModel || []).length === 0 ||
coderModel.every((it) => !it.is_active)
) {
text = '代码补全模型尚未配置激活,平台功能暂不可用!';
} else if (
(llmModel || []).length === 0 ||
llmModel.every((it) => !it.is_active)
) {
text = '对话模型尚未配置激活,平台功能暂不可用!';
} else {
text = '模型尚未配置激活,平台功能暂不可用!';
}
Modal.confirm({
title: '提示',
content: text,
showCancel: false,
okText: '去配置',
});
}
}, [isConfigModel, coderModel, llmModel]);
return (
<Stack gap={2}>
<TokenUsage />
<ModelCard title='对话模型' modelType={ConstsModelType.ModelTypeLLM} />
<ModelCard
title='对话模型'
data={llmModel}
refreshModel={refreshModel}
modelType={ConstsModelType.ModelTypeLLM}
/>
<ModelCard
title='代码补全模型'
data={coderModel}
refreshModel={refreshModel}
modelType={ConstsModelType.ModelTypeCoder}
/>
</Stack>

View File

@@ -115,8 +115,9 @@ const User = () => {
color='info'
sx={{ gap: 2 }}
onClick={() => setThirdPartyLoginSettingModalOpen(true)}
disabled
>
()
</Button>
</StyledCard>
<LoginHistory />

View File

@@ -20,6 +20,7 @@ const Dashboard = () => {
size='small'
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
sx={{ fontSize: 14 }}
disabled
>
<MenuItem value='24h'> 24 </MenuItem>
<MenuItem value='90d'> 90 </MenuItem>