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 { SvgIcon, type SvgIconProps } from "@mui/material";
|
||||||
import logger from "@/utils/logger";
|
import logger from "@/utils/logger";
|
||||||
|
|
||||||
// 使用 import.meta.glob 预加载所有 SVG 文件
|
// 按目录分别预加载 SVG 文件,实现按需加载
|
||||||
const svgModules = import.meta.glob('/src/assets/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',
|
query: '?react',
|
||||||
import: 'default',
|
import: 'default',
|
||||||
eager: false
|
eager: false
|
||||||
@@ -27,8 +45,22 @@ const getPointPath = (point: AppSvgIconProps['point']) => {
|
|||||||
return ''
|
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 }} />
|
* <AppSvgIcon name="add" sx={{ color: 'primary.main', fontSize: 30 }} />
|
||||||
*/
|
*/
|
||||||
export default function AppSvgIcon(props: AppSvgIconProps) {
|
export default function AppSvgIcon(props: AppSvgIconProps) {
|
||||||
@@ -46,8 +78,10 @@ export default function AppSvgIcon(props: AppSvgIconProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const iconPath = `/src/assets/svg${pointPath}/${name}.svg`;
|
const iconPath = `/src/assets/svg${pointPath}/${name}.svg`;
|
||||||
|
|
||||||
// 使用预定义的模块映射
|
// 根据类型获取对应的模块映射
|
||||||
|
const svgModules = getSvgModules(point);
|
||||||
const moduleLoader = svgModules[iconPath];
|
const moduleLoader = svgModules[iconPath];
|
||||||
|
|
||||||
if (moduleLoader) {
|
if (moduleLoader) {
|
||||||
const iconModule = await moduleLoader();
|
const iconModule = await moduleLoader();
|
||||||
setIcon(() => iconModule as React.FC<React.SVGProps<SVGSVGElement>>);
|
setIcon(() => iconModule as React.FC<React.SVGProps<SVGSVGElement>>);
|
||||||
@@ -63,10 +97,11 @@ export default function AppSvgIcon(props: AppSvgIconProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadIcon();
|
loadIcon();
|
||||||
}, [name, pointPath]);
|
}, [name, pointPath, point]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null; // 或者返回一个加载占位符
|
// 返回一个简单的占位符,避免布局跳动
|
||||||
|
return <SvgIcon {...rest} style={{ opacity: 0 }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Icon) {
|
if (!Icon) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import LanguageSwitcher from '../LanguageSwitcher';
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
import { useAuth } from '@/hooks/login-hooks';
|
import { useAuth } from '@/hooks/login-hooks';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { userInfo, logout } = useAuth();
|
const { userInfo, logout } = useAuth();
|
||||||
@@ -27,6 +28,8 @@ const Header = () => {
|
|||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
|
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
@@ -193,14 +196,14 @@ const Header = () => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PersonIcon fontSize="small" />
|
<PersonIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>个人资料</ListItemText>
|
<ListItemText>{t('setting.personalProfile')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/* 模型配置 */}
|
{/* 模型配置 */}
|
||||||
<MenuItem onClick={() => navigate('/setting/models')} sx={{ py: 1 }}>
|
<MenuItem onClick={() => navigate('/setting/models')} sx={{ py: 1 }}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon fontSize="small" />
|
<SettingsIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>模型配置</ListItemText>
|
<ListItemText>{t('setting.modelSettings')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -209,7 +212,7 @@ const Header = () => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LogoutIcon fontSize="small" sx={{ color: '#d32f2f' }} />
|
<LogoutIcon fontSize="small" sx={{ color: '#d32f2f' }} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>退出登录</ListItemText>
|
<ListItemText>{t('setting.logout')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const useApiKeyDialog = (onSuccess?: () => void) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Ollama 对话框管理
|
// Ollama 对话框管理
|
||||||
export const useOllamaDialog = () => {
|
export const useOllamaDialog = (onSuccess?: () => void) => {
|
||||||
const dialogState = useDialogState();
|
const dialogState = useDialogState();
|
||||||
const showMessage = useMessage();
|
const showMessage = useMessage();
|
||||||
|
|
||||||
@@ -179,6 +179,11 @@ export const useOllamaDialog = () => {
|
|||||||
});
|
});
|
||||||
showMessage.success('Ollama 模型添加成功');
|
showMessage.success('Ollama 模型添加成功');
|
||||||
dialogState.closeDialog();
|
dialogState.closeDialog();
|
||||||
|
|
||||||
|
// 调用成功回调
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ollama 模型添加失败:', error);
|
logger.error('Ollama 模型添加失败:', error);
|
||||||
showMessage.error('Ollama 模型添加失败');
|
showMessage.error('Ollama 模型添加失败');
|
||||||
@@ -186,7 +191,7 @@ export const useOllamaDialog = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
dialogState.setLoading(false);
|
dialogState.setLoading(false);
|
||||||
}
|
}
|
||||||
}, [dialogState, showMessage]);
|
}, [dialogState, showMessage, onSuccess]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dialogState,
|
...dialogState,
|
||||||
@@ -340,7 +345,7 @@ export const useSystemModelSetting = (onSuccess?: () => void) => {
|
|||||||
// 统一的模型对话框管理器
|
// 统一的模型对话框管理器
|
||||||
export const useModelDialogs = (onSuccess?: () => void) => {
|
export const useModelDialogs = (onSuccess?: () => void) => {
|
||||||
const apiKeyDialog = useApiKeyDialog(onSuccess);
|
const apiKeyDialog = useApiKeyDialog(onSuccess);
|
||||||
const ollamaDialog = useOllamaDialog();
|
const ollamaDialog = useOllamaDialog(onSuccess);
|
||||||
const configurationDialog = useConfigurationDialog(onSuccess);
|
const configurationDialog = useConfigurationDialog(onSuccess);
|
||||||
const systemDialog = useSystemModelSetting(onSuccess);
|
const systemDialog = useSystemModelSetting(onSuccess);
|
||||||
const deleteOps = useDeleteOperations(onSuccess);
|
const deleteOps = useDeleteOperations(onSuccess);
|
||||||
|
|||||||
@@ -88,6 +88,28 @@ function ModelsPage() {
|
|||||||
return filterFactory || [];
|
return filterFactory || [];
|
||||||
}, [llmFactory, myLlm]);
|
}, [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) => {
|
const handleConfigureFactory = useCallback((factory: IFactory) => {
|
||||||
if (factory == null) {
|
if (factory == null) {
|
||||||
@@ -126,7 +148,31 @@ function ModelsPage() {
|
|||||||
if (factoryName == null) {
|
if (factoryName == null) {
|
||||||
return;
|
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();
|
const dialog = useDialog();
|
||||||
@@ -247,7 +293,7 @@ function ModelsPage() {
|
|||||||
variant='contained' color='primary' startIcon={<EditIcon />}
|
variant='contained' color='primary' startIcon={<EditIcon />}
|
||||||
onClick={() => handleEditLlmFactory(factoryName)}
|
onClick={() => handleEditLlmFactory(factoryName)}
|
||||||
>
|
>
|
||||||
{t('setting.edit')}
|
{ showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined' color='primary' startIcon={<DeleteIcon />}
|
variant='outlined' color='primary' startIcon={<DeleteIcon />}
|
||||||
|
|||||||
@@ -129,43 +129,73 @@ request.interceptors.request.use(
|
|||||||
// 响应拦截器
|
// 响应拦截器
|
||||||
request.interceptors.response.use(
|
request.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(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]);
|
snackbar.error(RetcodeMessage[status as ResultCode]);
|
||||||
}
|
}
|
||||||
|
if (status == 401) {
|
||||||
|
logger.info('⚠️ Handling HTTP 401 unauthorized');
|
||||||
|
notification.error(i18n.t('message.401'));
|
||||||
|
redirectToLogin();
|
||||||
|
}
|
||||||
// 处理blob类型响应
|
// 处理blob类型响应
|
||||||
if (response.config.responseType === 'blob') {
|
if (response.config.responseType === 'blob') {
|
||||||
|
logger.info('📁 Blob response detected, skipping data processing');
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
console.log('interceptors.response ------ response', response);
|
logger.info('interceptors.response ------ response', response);
|
||||||
const data: ResponseType = response.data;
|
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'));
|
notification.error(data?.message, i18n.t('message.401'));
|
||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
} else if (data?.code !== 0) {
|
} 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'));
|
const error = new CustomError(data?.message || i18n.t('message.requestError'));
|
||||||
error.code = data?.code || -1;
|
error.code = data?.code || -1;
|
||||||
error.response = data;
|
error.response = data;
|
||||||
snackbar.warning(error.message);
|
snackbar.warning(error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
logger.info('✅ Response processed successfully');
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(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) {
|
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) {
|
} else if (error.response) {
|
||||||
const { status, statusText } = error.response;
|
const { status, statusText } = error.response;
|
||||||
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
||||||
|
logger.info('🚫 HTTP error response:', { status, statusText, errorText });
|
||||||
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
|
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('🔄 Rejecting promise with error');
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,14 +17,31 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
|
// 优化缓存策略,使用内容哈希,启用长期缓存
|
||||||
|
entryFileNames: 'assets/[name]-[hash].js',
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||||
manualChunks: (id) => {
|
manualChunks: (id) => {
|
||||||
// 将所有 SVG 文件打包到一个单独的 chunk 中
|
// SVG 文件分割策略
|
||||||
if (id.includes('.svg') && id.includes('?react')) {
|
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: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
|
|||||||
Reference in New Issue
Block a user