refactor(build): improve caching strategy with content hashing
feat(header): add translation support for menu items fix(models): enhance model factory handling with proper dialogs
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.7 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.3 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 11 MiB |
@@ -2,8 +2,26 @@ import React from "react";
|
||||
import { SvgIcon, type SvgIconProps } from "@mui/material";
|
||||
import logger from "@/utils/logger";
|
||||
|
||||
// 使用 import.meta.glob 预加载所有 SVG 文件
|
||||
const svgModules = import.meta.glob('/src/assets/svg/**/*.svg', {
|
||||
// 按目录分别预加载 SVG 文件,实现按需加载
|
||||
const llmSvgModules = import.meta.glob('/src/assets/svg/llm/*.svg', {
|
||||
query: '?react',
|
||||
import: 'default',
|
||||
eager: false
|
||||
});
|
||||
|
||||
const chunkSvgModules = import.meta.glob('/src/assets/svg/chunk-method/*.svg', {
|
||||
query: '?react',
|
||||
import: 'default',
|
||||
eager: false
|
||||
});
|
||||
|
||||
const fileSvgModules = import.meta.glob('/src/assets/svg/file-icon/*.svg', {
|
||||
query: '?react',
|
||||
import: 'default',
|
||||
eager: false
|
||||
});
|
||||
|
||||
const commonSvgModules = import.meta.glob('/src/assets/svg/*.svg', {
|
||||
query: '?react',
|
||||
import: 'default',
|
||||
eager: false
|
||||
@@ -27,8 +45,22 @@ const getPointPath = (point: AppSvgIconProps['point']) => {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 根据类型获取对应的模块映射
|
||||
const getSvgModules = (point: AppSvgIconProps['point']) => {
|
||||
switch (point) {
|
||||
case 'llm':
|
||||
return llmSvgModules;
|
||||
case 'chunk':
|
||||
return chunkSvgModules;
|
||||
case 'file':
|
||||
return fileSvgModules;
|
||||
default:
|
||||
return commonSvgModules;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 SVG 图标组件
|
||||
* 通用 SVG 图标组件 - 优化版本,支持按需加载
|
||||
* <AppSvgIcon name="add" sx={{ color: 'primary.main', fontSize: 30 }} />
|
||||
*/
|
||||
export default function AppSvgIcon(props: AppSvgIconProps) {
|
||||
@@ -46,8 +78,10 @@ export default function AppSvgIcon(props: AppSvgIconProps) {
|
||||
setLoading(true);
|
||||
const iconPath = `/src/assets/svg${pointPath}/${name}.svg`;
|
||||
|
||||
// 使用预定义的模块映射
|
||||
// 根据类型获取对应的模块映射
|
||||
const svgModules = getSvgModules(point);
|
||||
const moduleLoader = svgModules[iconPath];
|
||||
|
||||
if (moduleLoader) {
|
||||
const iconModule = await moduleLoader();
|
||||
setIcon(() => iconModule as React.FC<React.SVGProps<SVGSVGElement>>);
|
||||
@@ -63,10 +97,11 @@ export default function AppSvgIcon(props: AppSvgIconProps) {
|
||||
};
|
||||
|
||||
loadIcon();
|
||||
}, [name, pointPath]);
|
||||
}, [name, pointPath, point]);
|
||||
|
||||
if (loading) {
|
||||
return null; // 或者返回一个加载占位符
|
||||
// 返回一个简单的占位符,避免布局跳动
|
||||
return <SvgIcon {...rest} style={{ opacity: 0 }} />;
|
||||
}
|
||||
|
||||
if (!Icon) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import LanguageSwitcher from '../LanguageSwitcher';
|
||||
import { useAuth } from '@/hooks/login-hooks';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Header = () => {
|
||||
const { userInfo, logout } = useAuth();
|
||||
@@ -27,6 +28,8 @@ const Header = () => {
|
||||
const open = Boolean(anchorEl);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
@@ -193,14 +196,14 @@ const Header = () => {
|
||||
<ListItemIcon>
|
||||
<PersonIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>个人资料</ListItemText>
|
||||
<ListItemText>{t('setting.personalProfile')}</ListItemText>
|
||||
</MenuItem>
|
||||
{/* 模型配置 */}
|
||||
<MenuItem onClick={() => navigate('/setting/models')} sx={{ py: 1 }}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>模型配置</ListItemText>
|
||||
<ListItemText>{t('setting.modelSettings')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
@@ -209,7 +212,7 @@ const Header = () => {
|
||||
<ListItemIcon>
|
||||
<LogoutIcon fontSize="small" sx={{ color: '#d32f2f' }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>退出登录</ListItemText>
|
||||
<ListItemText>{t('setting.logout')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
@@ -161,7 +161,7 @@ export const useApiKeyDialog = (onSuccess?: () => void) => {
|
||||
};
|
||||
|
||||
// Ollama 对话框管理
|
||||
export const useOllamaDialog = () => {
|
||||
export const useOllamaDialog = (onSuccess?: () => void) => {
|
||||
const dialogState = useDialogState();
|
||||
const showMessage = useMessage();
|
||||
|
||||
@@ -179,6 +179,11 @@ export const useOllamaDialog = () => {
|
||||
});
|
||||
showMessage.success('Ollama 模型添加成功');
|
||||
dialogState.closeDialog();
|
||||
|
||||
// 调用成功回调
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Ollama 模型添加失败:', error);
|
||||
showMessage.error('Ollama 模型添加失败');
|
||||
@@ -186,7 +191,7 @@ export const useOllamaDialog = () => {
|
||||
} finally {
|
||||
dialogState.setLoading(false);
|
||||
}
|
||||
}, [dialogState, showMessage]);
|
||||
}, [dialogState, showMessage, onSuccess]);
|
||||
|
||||
return {
|
||||
...dialogState,
|
||||
@@ -340,7 +345,7 @@ export const useSystemModelSetting = (onSuccess?: () => void) => {
|
||||
// 统一的模型对话框管理器
|
||||
export const useModelDialogs = (onSuccess?: () => void) => {
|
||||
const apiKeyDialog = useApiKeyDialog(onSuccess);
|
||||
const ollamaDialog = useOllamaDialog();
|
||||
const ollamaDialog = useOllamaDialog(onSuccess);
|
||||
const configurationDialog = useConfigurationDialog(onSuccess);
|
||||
const systemDialog = useSystemModelSetting(onSuccess);
|
||||
const deleteOps = useDeleteOperations(onSuccess);
|
||||
|
||||
@@ -88,6 +88,28 @@ function ModelsPage() {
|
||||
return filterFactory || [];
|
||||
}, [llmFactory, myLlm]);
|
||||
|
||||
const showAddModel = (factoryName: string) => {
|
||||
const configurationFactories: LLMFactory[] = [
|
||||
LLM_FACTORY_LIST.AzureOpenAI,
|
||||
LLM_FACTORY_LIST.Bedrock,
|
||||
LLM_FACTORY_LIST.BaiduYiYan,
|
||||
LLM_FACTORY_LIST.FishAudio,
|
||||
LLM_FACTORY_LIST.GoogleCloud,
|
||||
LLM_FACTORY_LIST.TencentCloud,
|
||||
LLM_FACTORY_LIST.TencentHunYuan,
|
||||
LLM_FACTORY_LIST.XunFeiSpark,
|
||||
LLM_FACTORY_LIST.VolcEngine,
|
||||
]
|
||||
|
||||
const fN = factoryName as LLMFactory;
|
||||
if (!fN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LocalLlmFactories.includes(fN) ||
|
||||
configurationFactories.includes(fN);
|
||||
}
|
||||
|
||||
// 处理配置模型工厂
|
||||
const handleConfigureFactory = useCallback((factory: IFactory) => {
|
||||
if (factory == null) {
|
||||
@@ -126,7 +148,31 @@ function ModelsPage() {
|
||||
if (factoryName == null) {
|
||||
return;
|
||||
}
|
||||
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
|
||||
const factoryN = factoryName as LLMFactory;
|
||||
const configurationFactories: LLMFactory[] = [
|
||||
LLM_FACTORY_LIST.AzureOpenAI,
|
||||
LLM_FACTORY_LIST.Bedrock,
|
||||
LLM_FACTORY_LIST.BaiduYiYan,
|
||||
LLM_FACTORY_LIST.FishAudio,
|
||||
LLM_FACTORY_LIST.GoogleCloud,
|
||||
LLM_FACTORY_LIST.TencentCloud,
|
||||
LLM_FACTORY_LIST.TencentHunYuan,
|
||||
LLM_FACTORY_LIST.XunFeiSpark,
|
||||
LLM_FACTORY_LIST.VolcEngine,
|
||||
]
|
||||
if (LocalLlmFactories.includes(factoryN)) {
|
||||
// local llm
|
||||
modelDialogs.ollamaDialog.openDialog({
|
||||
llm_factory: factoryN,
|
||||
}, true);
|
||||
} else if (configurationFactories.includes(factoryN)) {
|
||||
// custom configuration llm
|
||||
modelDialogs.configurationDialog.openConfigurationDialog(factoryN);
|
||||
} else {
|
||||
// llm set api
|
||||
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryN, {}, true);
|
||||
}
|
||||
logger.debug('handleEditLlmFactory', factoryN);
|
||||
}, []);
|
||||
|
||||
const dialog = useDialog();
|
||||
@@ -247,7 +293,7 @@ function ModelsPage() {
|
||||
variant='contained' color='primary' startIcon={<EditIcon />}
|
||||
onClick={() => handleEditLlmFactory(factoryName)}
|
||||
>
|
||||
{t('setting.edit')}
|
||||
{ showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined' color='primary' startIcon={<DeleteIcon />}
|
||||
|
||||
@@ -129,43 +129,73 @@ request.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { status } = response;
|
||||
logger.info('🔍 Response interceptor triggered:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
responseType: response.config.responseType,
|
||||
hasData: !!response.data
|
||||
});
|
||||
|
||||
const { status } = response;
|
||||
// 处理特定状态码
|
||||
if (status === 413 || status === 504) {
|
||||
if (status == 413 || status == 504) {
|
||||
logger.info('⚠️ Handling HTTP error status:', status);
|
||||
snackbar.error(RetcodeMessage[status as ResultCode]);
|
||||
}
|
||||
if (status == 401) {
|
||||
logger.info('⚠️ Handling HTTP 401 unauthorized');
|
||||
notification.error(i18n.t('message.401'));
|
||||
redirectToLogin();
|
||||
}
|
||||
// 处理blob类型响应
|
||||
if (response.config.responseType === 'blob') {
|
||||
logger.info('📁 Blob response detected, skipping data processing');
|
||||
return response;
|
||||
}
|
||||
console.log('interceptors.response ------ response', response);
|
||||
logger.info('interceptors.response ------ response', response);
|
||||
const data: ResponseType = response.data;
|
||||
|
||||
// 处理业务错误码
|
||||
if (data?.code === 401) {
|
||||
if (data?.code == 401) {
|
||||
logger.info('⚠️ Handling business code 401 unauthorized');
|
||||
notification.error(data?.message, i18n.t('message.401'));
|
||||
redirectToLogin();
|
||||
} else if (data?.code !== 0) {
|
||||
// 处理其他业务错误
|
||||
logger.info('❌ Handling business error:', { code: data?.code, message: data?.message });
|
||||
const error = new CustomError(data?.message || i18n.t('message.requestError'));
|
||||
error.code = data?.code || -1;
|
||||
error.response = data;
|
||||
snackbar.warning(error.message);
|
||||
throw error;
|
||||
}
|
||||
logger.info('✅ Response processed successfully');
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
logger.info('❌ Response interceptor error handler triggered:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
url: error.config?.url,
|
||||
hasResponse: !!error.response
|
||||
});
|
||||
|
||||
const { status } = error.response || {};
|
||||
if (status == 401) {
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
// 处理网络错误
|
||||
if (error.message === FAILED_TO_FETCH || !error.response) {
|
||||
// notification.error(i18n.t('message.networkAnomaly'), i18n.t('message.networkAnomalyDescription'));
|
||||
logger.info('🌐 Network error detected:', error.message);
|
||||
} else if (error.response) {
|
||||
const { status, statusText } = error.response;
|
||||
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
||||
logger.info('🚫 HTTP error response:', { status, statusText, errorText });
|
||||
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
|
||||
}
|
||||
|
||||
logger.info('🔄 Rejecting promise with error');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -17,15 +17,32 @@ export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 优化缓存策略,使用内容哈希,启用长期缓存
|
||||
entryFileNames: 'assets/[name]-[hash].js',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
manualChunks: (id) => {
|
||||
// 将所有 SVG 文件打包到一个单独的 chunk 中
|
||||
// SVG 文件分割策略
|
||||
if (id.includes('.svg') && id.includes('?react')) {
|
||||
return 'svg-icons';
|
||||
// 排除 chunk-method 文件夹的 SVG
|
||||
if (id.includes('/chunk-method/')) {
|
||||
// 不打包 chunk-method 的 SVG,让它们保持为独立的资源文件
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 其他 SVG 文件正常分割
|
||||
if (id.includes('/llm/')) {
|
||||
return 'svg-llm';
|
||||
} else if (id.includes('/file-icon/')) {
|
||||
return 'svg-file';
|
||||
} else {
|
||||
return 'svg-common';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['154.9.253.114', 'localhost', 'teres.deep-pilot.chat'],
|
||||
|
||||
Reference in New Issue
Block a user