♻️ refactor(StyleContext): modernize context architecture and eliminate route transition flicker
## Breaking Changes - Remove backward compatibility layer for old action types - StyleContext is no longer exported, use useStyle hook instead ## Major Improvements - **Architecture**: Replace useState with useReducer for complex state management - **Performance**: Add debounced resize handling and batch updates via BATCH_UPDATE action - **DX**: Export useStyle hook and styleActions for type-safe usage - **Memory**: Use useMemo to cache context value and prevent unnecessary re-renders ## Bug Fixes - **UI**: Eliminate padding flicker when navigating to /console/chat* and /console/playground routes - **Logic**: Remove redundant localStorage operations and state synchronization ## Implementation Details - Define ACTION_TYPES and ROUTE_PATTERNS constants for better maintainability - Add comprehensive JSDoc documentation for all functions - Extract custom hooks: useWindowResize, useRouteChange, useMobileSiderAutoHide - Calculate shouldInnerPadding directly in PageLayout based on pathname to prevent async updates - Integrate localStorage saving logic into SET_SIDER_COLLAPSED reducer case - Remove SET_INNER_PADDING action as it's no longer needed ## Updated Components - PageLayout.js: Direct padding calculation based on route - HeaderBar.js: Use new useStyle hook and styleActions - SiderBar.js: Remove redundant localStorage calls - LogsTable.js: Remove unused StyleContext import - Playground/index.js: Migrate to new API ## Performance Impact - Reduced component re-renders through optimized context structure - Eliminated unnecessary effect dependencies and state updates - Improved route transition smoothness with synchronous padding calculation
This commit is contained in:
@@ -272,7 +272,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/chat/:id?'
|
||||
path='/console/chat/:id?'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||
<Chat />
|
||||
|
||||
@@ -31,13 +31,13 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
|
||||
const HeaderBar = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
let navigate = useNavigate();
|
||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||
@@ -152,8 +152,7 @@ const HeaderBar = () => {
|
||||
|
||||
const handleNavLinkClick = (itemKey) => {
|
||||
if (itemKey === 'home') {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: false });
|
||||
styleDispatch(styleActions.setSider(false));
|
||||
}
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
@@ -383,7 +382,7 @@ const HeaderBar = () => {
|
||||
onClick={() => {
|
||||
if (isConsoleRoute) {
|
||||
// 控制侧边栏的显示/隐藏,无论是否移动设备
|
||||
styleDispatch({ type: 'TOGGLE_SIDER' });
|
||||
styleDispatch(styleActions.toggleSider());
|
||||
} else {
|
||||
// 控制HeaderBar自己的移动菜单
|
||||
setMobileMenuOpen(!mobileMenuOpen);
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
} from '../helpers/render';
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { getLogOther } from '../helpers/other.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSetting,
|
||||
@@ -765,7 +764,6 @@ const LogsTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [expandData, setExpandData] = useState({});
|
||||
const [showStat, setShowStat] = useState(false);
|
||||
|
||||
@@ -5,7 +5,7 @@ import App from '../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useStyle } from '../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
@@ -17,11 +17,15 @@ const { Sider, Content, Header, Footer } = Layout;
|
||||
const PageLayout = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState } = useStyle();
|
||||
const { i18n } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const isPlaygroundRoute = location.pathname === '/console/playground';
|
||||
const shouldHideFooter = location.pathname === '/console/playground' || location.pathname.startsWith('/console/chat');
|
||||
|
||||
const shouldInnerPadding = location.pathname.includes('/console') &&
|
||||
!location.pathname.startsWith('/console/chat') &&
|
||||
location.pathname !== '/console/playground';
|
||||
|
||||
const loadUser = () => {
|
||||
let user = localStorage.getItem('user');
|
||||
@@ -65,15 +69,8 @@ const PageLayout = () => {
|
||||
if (savedLang) {
|
||||
i18n.changeLanguage(savedLang);
|
||||
}
|
||||
|
||||
// 默认显示侧边栏
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
}, [i18n]);
|
||||
|
||||
// 获取侧边栏折叠状态
|
||||
const isSidebarCollapsed =
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
|
||||
return (
|
||||
<Layout
|
||||
style={{
|
||||
@@ -99,8 +96,8 @@ const PageLayout = () => {
|
||||
</Header>
|
||||
<Layout
|
||||
style={{
|
||||
marginTop: '56px',
|
||||
height: 'calc(100vh - 56px)',
|
||||
marginTop: '64px',
|
||||
height: 'calc(100vh - 64px)',
|
||||
overflow: styleState.isMobile ? 'visible' : 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -111,11 +108,11 @@ const PageLayout = () => {
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: '56px',
|
||||
top: '64px',
|
||||
zIndex: 99,
|
||||
border: 'none',
|
||||
paddingRight: '0',
|
||||
height: 'calc(100vh - 56px)',
|
||||
height: 'calc(100vh - 64px)',
|
||||
}}
|
||||
>
|
||||
<SiderBar />
|
||||
@@ -141,14 +138,14 @@ const PageLayout = () => {
|
||||
flex: '1 0 auto',
|
||||
overflowY: styleState.isMobile ? 'visible' : 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
padding: styleState.shouldInnerPadding ? '24px' : '0',
|
||||
padding: shouldInnerPadding ? '24px' : '0',
|
||||
position: 'relative',
|
||||
marginTop: styleState.isMobile ? '2px' : '0',
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
{!isPlaygroundRoute && (
|
||||
{!shouldHideFooter && (
|
||||
<Layout.Footer
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { stringToColor } from '../helpers/render.js';
|
||||
import { useSetTheme, useTheme } from '../context/Theme/index.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
// 自定义侧边栏按钮样式
|
||||
@@ -95,13 +95,11 @@ const routerMap = {
|
||||
|
||||
const SiderBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const defaultIsCollapsed =
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
||||
const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
|
||||
const [chatItems, setChatItems] = useState([]);
|
||||
const [openedKeys, setOpenedKeys] = useState([]);
|
||||
const theme = useTheme();
|
||||
@@ -270,7 +268,7 @@ const SiderBar = () => {
|
||||
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
newRouterMap['chat' + i] = '/chat/' + i;
|
||||
newRouterMap['chat' + i] = '/console/chat/' + i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +289,7 @@ const SiderBar = () => {
|
||||
for (let key in chats[i]) {
|
||||
chat.text = key;
|
||||
chat.itemKey = 'chat' + i;
|
||||
chat.to = '/chat/' + i;
|
||||
chat.to = '/console/chat/' + i;
|
||||
}
|
||||
chatItems.push(chat);
|
||||
}
|
||||
@@ -315,7 +313,7 @@ const SiderBar = () => {
|
||||
);
|
||||
|
||||
// Handle chat routes
|
||||
if (!matchingKey && currentPath.startsWith('/chat/')) {
|
||||
if (!matchingKey && currentPath.startsWith('/console/chat/')) {
|
||||
const chatIndex = currentPath.split('/').pop();
|
||||
if (!isNaN(chatIndex)) {
|
||||
matchingKey = 'chat' + chatIndex;
|
||||
@@ -365,15 +363,11 @@ const SiderBar = () => {
|
||||
overflowY: 'auto',
|
||||
WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
|
||||
}}
|
||||
defaultIsCollapsed={
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true'
|
||||
}
|
||||
defaultIsCollapsed={styleState.siderCollapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapseChange={(collapsed) => {
|
||||
setIsCollapsed(collapsed);
|
||||
// styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
styleDispatch({ type: 'SET_SIDER_COLLAPSED', payload: collapsed });
|
||||
localStorage.setItem('default_collapse_sidebar', collapsed);
|
||||
styleDispatch(styleActions.setSiderCollapsed(collapsed));
|
||||
|
||||
// 确保在收起侧边栏时有选中的项目,避免不必要的计算
|
||||
if (selectedKeys.length === 0) {
|
||||
@@ -384,7 +378,7 @@ const SiderBar = () => {
|
||||
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
} else if (currentPath.startsWith('/chat/')) {
|
||||
} else if (currentPath.startsWith('/console/chat/')) {
|
||||
setSelectedKeys(['chat']);
|
||||
} else {
|
||||
setSelectedKeys(['detail']); // 默认选中首页
|
||||
@@ -406,12 +400,6 @@ const SiderBar = () => {
|
||||
);
|
||||
}}
|
||||
onSelect={(key) => {
|
||||
if (key.itemKey.toString().startsWith('chat')) {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
|
||||
// 如果点击的是已经展开的子菜单的父项,则收起子菜单
|
||||
if (openedKeys.includes(key.itemKey)) {
|
||||
setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
|
||||
|
||||
@@ -38,8 +38,9 @@ const ChatArea = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl h-full"
|
||||
bodyStyle={{ padding: 0, height: 'calc(100vh - 108px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
className="h-full"
|
||||
bordered={false}
|
||||
bodyStyle={{ padding: 0, height: 'calc(100vh - 66px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
>
|
||||
{/* 聊天头部 */}
|
||||
{styleState.isMobile ? (
|
||||
|
||||
@@ -217,8 +217,7 @@ const ConfigManager = ({
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
onClick={handleReset}
|
||||
className="!rounded-lg !text-xs !h-7 !px-2"
|
||||
style={{ minWidth: 'auto' }}
|
||||
className="!rounded-full !text-xs !px-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@ const DebugPanel = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl h-full flex flex-col"
|
||||
className="h-full flex flex-col"
|
||||
bordered={false}
|
||||
bodyStyle={{
|
||||
padding: styleState.isMobile ? '16px' : '24px',
|
||||
height: '100%',
|
||||
@@ -159,7 +160,7 @@ const DebugPanel = ({
|
||||
<TabPane tab={
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap size={16} />
|
||||
{t('响应内容')}
|
||||
{t('响应')}
|
||||
</div>
|
||||
} itemKey="response">
|
||||
<CodeViewer
|
||||
|
||||
@@ -136,18 +136,10 @@ const MessageContent = ({
|
||||
!finalExtractedThinkingContent &&
|
||||
(!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
||||
return (
|
||||
<div className={`${className} flex items-center gap-2 sm:gap-4 p-4 sm:p-6 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl sm:rounded-2xl`}>
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
||||
<div className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl sm:rounded-2xl`}>
|
||||
<div className="w-4 h-4 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
||||
<Loader2 className="animate-spin text-white" size={styleState.isMobile ? 16 : 20} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text strong className="text-gray-800 text-sm sm:text-base">
|
||||
{t('正在思考...')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-gray-500 text-xs sm:text-sm">
|
||||
AI 正在分析您的问题
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ const SettingsPanel = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`!rounded-2xl h-full flex flex-col ${styleState.isMobile ? 'rounded-none border-none shadow-none' : ''}`}
|
||||
className={`h-full flex flex-col ${styleState.isMobile ? 'rounded-none border-none shadow-none' : ''}`}
|
||||
bordered={false}
|
||||
bodyStyle={{
|
||||
padding: styleState.isMobile ? '24px' : '24px 24px 16px 24px',
|
||||
height: '100%',
|
||||
|
||||
@@ -1,117 +1,227 @@
|
||||
// contexts/User/index.jsx
|
||||
// contexts/Style/index.js
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useReducer, useEffect, useMemo, createContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMobile as getIsMobile } from '../../helpers/index.js';
|
||||
|
||||
export const StyleContext = React.createContext({
|
||||
dispatch: () => null,
|
||||
});
|
||||
// Action Types
|
||||
const ACTION_TYPES = {
|
||||
TOGGLE_SIDER: 'TOGGLE_SIDER',
|
||||
SET_SIDER: 'SET_SIDER',
|
||||
SET_MOBILE: 'SET_MOBILE',
|
||||
SET_SIDER_COLLAPSED: 'SET_SIDER_COLLAPSED',
|
||||
BATCH_UPDATE: 'BATCH_UPDATE',
|
||||
};
|
||||
|
||||
// Constants
|
||||
const STORAGE_KEYS = {
|
||||
SIDEBAR_COLLAPSED: 'default_collapse_sidebar',
|
||||
};
|
||||
|
||||
const ROUTE_PATTERNS = {
|
||||
CONSOLE: '/console',
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断路径是否为控制台路由
|
||||
* @param {string} pathname - 路由路径
|
||||
* @returns {boolean} 是否为控制台路由
|
||||
*/
|
||||
const isConsoleRoute = (pathname) => {
|
||||
return pathname === ROUTE_PATTERNS.CONSOLE ||
|
||||
pathname.startsWith(ROUTE_PATTERNS.CONSOLE + '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取初始状态
|
||||
* @param {string} pathname - 当前路由路径
|
||||
* @returns {Object} 初始状态对象
|
||||
*/
|
||||
const getInitialState = (pathname) => {
|
||||
const isMobile = getIsMobile();
|
||||
const isConsole = isConsoleRoute(pathname);
|
||||
const isCollapsed = localStorage.getItem(STORAGE_KEYS.SIDEBAR_COLLAPSED) === 'true';
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
showSider: isConsole && !isMobile,
|
||||
siderCollapsed: isCollapsed,
|
||||
isManualSiderControl: false,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Style reducer
|
||||
* @param {Object} state - 当前状态
|
||||
* @param {Object} action - action 对象
|
||||
* @returns {Object} 新状态
|
||||
*/
|
||||
const styleReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.TOGGLE_SIDER:
|
||||
return {
|
||||
...state,
|
||||
showSider: !state.showSider,
|
||||
isManualSiderControl: true,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.SET_SIDER:
|
||||
return {
|
||||
...state,
|
||||
showSider: action.payload,
|
||||
isManualSiderControl: action.isManualControl ?? false,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.SET_MOBILE:
|
||||
return {
|
||||
...state,
|
||||
isMobile: action.payload,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.SET_SIDER_COLLAPSED:
|
||||
// 自动保存到 localStorage
|
||||
localStorage.setItem(STORAGE_KEYS.SIDEBAR_COLLAPSED, action.payload.toString());
|
||||
return {
|
||||
...state,
|
||||
siderCollapsed: action.payload,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.BATCH_UPDATE:
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Context (内部使用,不导出)
|
||||
const StyleContext = createContext(null);
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 处理窗口大小变化
|
||||
* @param {Function} dispatch - dispatch 函数
|
||||
* @param {Object} state - 当前状态
|
||||
* @param {string} pathname - 当前路径
|
||||
*/
|
||||
const useWindowResize = (dispatch, state, pathname) => {
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const isMobile = getIsMobile();
|
||||
dispatch({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile });
|
||||
|
||||
// 只有在非手动控制的情况下,才根据屏幕大小自动调整侧边栏
|
||||
if (!state.isManualSiderControl && isConsoleRoute(pathname)) {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.SET_SIDER,
|
||||
payload: !isMobile,
|
||||
isManualControl: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let timeoutId;
|
||||
const debouncedResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(handleResize, 150);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [dispatch, state.isManualSiderControl, pathname]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 处理路由变化
|
||||
* @param {Function} dispatch - dispatch 函数
|
||||
* @param {string} pathname - 当前路径
|
||||
*/
|
||||
const useRouteChange = (dispatch, pathname) => {
|
||||
useEffect(() => {
|
||||
const isMobile = getIsMobile();
|
||||
const isConsole = isConsoleRoute(pathname);
|
||||
|
||||
dispatch({
|
||||
type: ACTION_TYPES.BATCH_UPDATE,
|
||||
payload: {
|
||||
showSider: isConsole && !isMobile,
|
||||
isManualSiderControl: false,
|
||||
},
|
||||
});
|
||||
}, [pathname, dispatch]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 处理移动设备侧边栏自动收起
|
||||
* @param {Object} state - 当前状态
|
||||
* @param {Function} dispatch - dispatch 函数
|
||||
*/
|
||||
const useMobileSiderAutoHide = (state, dispatch) => {
|
||||
useEffect(() => {
|
||||
// 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
|
||||
if (state.isMobile && state.showSider && !state.isManualSiderControl) {
|
||||
dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
|
||||
}
|
||||
}, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Style Provider 组件
|
||||
*/
|
||||
export const StyleProvider = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const initialIsMobile = getIsMobile();
|
||||
const pathname = location.pathname;
|
||||
|
||||
const initialPathname = location.pathname;
|
||||
let initialShowSiderValue = false;
|
||||
let initialInnerPaddingValue = false;
|
||||
const [state, dispatch] = useReducer(
|
||||
styleReducer,
|
||||
pathname,
|
||||
getInitialState
|
||||
);
|
||||
|
||||
if (initialPathname.includes('/console')) {
|
||||
initialShowSiderValue = !initialIsMobile;
|
||||
initialInnerPaddingValue = true;
|
||||
}
|
||||
useWindowResize(dispatch, state, pathname);
|
||||
useRouteChange(dispatch, pathname);
|
||||
useMobileSiderAutoHide(state, dispatch);
|
||||
|
||||
const [state, setState] = useState({
|
||||
isMobile: initialIsMobile,
|
||||
showSider: initialShowSiderValue,
|
||||
siderCollapsed: false,
|
||||
shouldInnerPadding: initialInnerPaddingValue,
|
||||
manualSiderControl: false,
|
||||
});
|
||||
|
||||
const dispatch = useCallback((action) => {
|
||||
if ('type' in action) {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_SIDER':
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
showSider: !prev.showSider,
|
||||
manualSiderControl: true
|
||||
}));
|
||||
break;
|
||||
case 'SET_SIDER':
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
showSider: action.payload,
|
||||
manualSiderControl: action.manual || false
|
||||
}));
|
||||
break;
|
||||
case 'SET_MOBILE':
|
||||
setState((prev) => ({ ...prev, isMobile: action.payload }));
|
||||
break;
|
||||
case 'SET_SIDER_COLLAPSED':
|
||||
setState((prev) => ({ ...prev, siderCollapsed: action.payload }));
|
||||
break;
|
||||
case 'SET_INNER_PADDING':
|
||||
setState((prev) => ({ ...prev, shouldInnerPadding: action.payload }));
|
||||
break;
|
||||
default:
|
||||
setState((prev) => ({ ...prev, ...action }));
|
||||
}
|
||||
} else {
|
||||
setState((prev) => ({ ...prev, ...action }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateMobileStatus = () => {
|
||||
const currentIsMobile = getIsMobile();
|
||||
if (!currentIsMobile &&
|
||||
(location.pathname === '/console' || location.pathname.startsWith('/console/'))) {
|
||||
dispatch({ type: 'SET_SIDER', payload: true, manual: false });
|
||||
}
|
||||
dispatch({ type: 'SET_MOBILE', payload: currentIsMobile });
|
||||
};
|
||||
window.addEventListener('resize', updateMobileStatus);
|
||||
return () => window.removeEventListener('resize', updateMobileStatus);
|
||||
}, [dispatch, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isMobile && state.showSider && !state.manualSiderControl) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
}
|
||||
}, [state.isMobile, state.showSider, state.manualSiderControl, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPathname = location.pathname;
|
||||
const currentlyMobile = getIsMobile();
|
||||
|
||||
if (currentPathname === '/console' || currentPathname.startsWith('/console/')) {
|
||||
dispatch({
|
||||
type: 'SET_SIDER',
|
||||
payload: !currentlyMobile,
|
||||
manual: false
|
||||
});
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SET_SIDER',
|
||||
payload: false,
|
||||
manual: false
|
||||
});
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
}
|
||||
}, [location.pathname, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const isCollapsed =
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
|
||||
}, [dispatch]);
|
||||
const contextValue = useMemo(
|
||||
() => ({ state, dispatch }),
|
||||
[state]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyleContext.Provider value={[state, dispatch]}>
|
||||
<StyleContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</StyleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 使用 StyleContext
|
||||
* @returns {{state: Object, dispatch: Function}} context value
|
||||
*/
|
||||
export const useStyle = () => {
|
||||
const context = React.useContext(StyleContext);
|
||||
if (!context) {
|
||||
throw new Error('useStyle must be used within StyleProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// 导出 action creators 以便外部使用
|
||||
export const styleActions = {
|
||||
toggleSider: () => ({ type: ACTION_TYPES.TOGGLE_SIDER }),
|
||||
setSider: (show, isManualControl = false) => ({
|
||||
type: ACTION_TYPES.SET_SIDER,
|
||||
payload: show,
|
||||
isManualControl
|
||||
}),
|
||||
setMobile: (isMobile) => ({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile }),
|
||||
setSiderCollapsed: (collapsed) => ({
|
||||
type: ACTION_TYPES.SET_SIDER_COLLAPSED,
|
||||
payload: collapsed
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Card,
|
||||
Form,
|
||||
Spin,
|
||||
Typography,
|
||||
IconButton,
|
||||
Modal,
|
||||
Avatar,
|
||||
@@ -40,17 +39,14 @@ import {
|
||||
modelToColor,
|
||||
} from '../../helpers/render';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Detail = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { Text } = Typography;
|
||||
const formRef = useRef();
|
||||
let now = new Date();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
|
||||
|
||||
// Context
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
|
||||
// Utils and hooks
|
||||
import { getLogo } from '../../helpers/index.js';
|
||||
@@ -60,10 +60,9 @@ const generateAvatarDataUrl = (username) => {
|
||||
const Playground = () => {
|
||||
const { t } = useTranslation();
|
||||
const [userState] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// 使用自定义hooks
|
||||
const state = usePlaygroundState();
|
||||
const {
|
||||
inputs,
|
||||
@@ -323,7 +322,7 @@ const Playground = () => {
|
||||
const handleResize = () => {
|
||||
const mobile = window.innerWidth < 768;
|
||||
if (styleState.isMobile !== mobile) {
|
||||
styleDispatch({ type: 'SET_IS_MOBILE', payload: mobile });
|
||||
styleDispatch(styleActions.setMobile(mobile));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -363,7 +362,7 @@ const Playground = () => {
|
||||
flexShrink: 0,
|
||||
minWidth: styleState.isMobile ? '100%' : 320,
|
||||
maxWidth: styleState.isMobile ? '100%' : 320,
|
||||
height: styleState.isMobile ? 'auto' : 'calc(100vh - 106px)',
|
||||
height: styleState.isMobile ? 'auto' : 'calc(100vh - 64px)',
|
||||
overflow: 'auto',
|
||||
position: styleState.isMobile ? 'fixed' : 'relative',
|
||||
zIndex: styleState.isMobile ? 1000 : 1,
|
||||
@@ -400,7 +399,7 @@ const Playground = () => {
|
||||
)}
|
||||
|
||||
<Layout.Content className="relative flex-1 overflow-hidden">
|
||||
<div className="sm:px-4 overflow-hidden flex flex-col lg:flex-row gap-2 sm:gap-4 h-[calc(100vh-106px)]">
|
||||
<div className="overflow-hidden flex flex-col lg:flex-row h-[calc(100vh-64px)]">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ChatArea
|
||||
chatRef={chatRef}
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Row,
|
||||
Form,
|
||||
Button,
|
||||
Typography,
|
||||
Space,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Modal,
|
||||
Banner,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { API, showError, showNotice } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IconHelpCircle,
|
||||
@@ -24,9 +16,7 @@ import {
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const Setup = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
|
||||
const [setupStatus, setSetupStatus] = useState({
|
||||
|
||||
@@ -51,7 +51,7 @@ export const DEFAULT_CONFIG = {
|
||||
group: '',
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
max_tokens: 2048,
|
||||
max_tokens: 4096,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
seed: null,
|
||||
@@ -61,10 +61,10 @@ export const DEFAULT_CONFIG = {
|
||||
},
|
||||
parameterEnabled: {
|
||||
temperature: true,
|
||||
top_p: false,
|
||||
top_p: true,
|
||||
max_tokens: false,
|
||||
frequency_penalty: false,
|
||||
presence_penalty: false,
|
||||
frequency_penalty: true,
|
||||
presence_penalty: true,
|
||||
seed: false,
|
||||
},
|
||||
systemPrompt: '',
|
||||
|
||||
Reference in New Issue
Block a user