Просмотр исходного кода

♻️ 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
Apple\Apple 9 месяцев назад
Родитель
Сommit
cc3f3cf033

+ 1 - 1
web/src/App.js

@@ -272,7 +272,7 @@ function App() {
           }
         />
         <Route
-          path='/chat/:id?'
+          path='/console/chat/:id?'
           element={
             <Suspense fallback={<Loading></Loading>} key={location.pathname}>
               <Chat />

+ 4 - 5
web/src/components/HeaderBar.js

@@ -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);

+ 0 - 2
web/src/components/LogsTable.js

@@ -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);

+ 13 - 16
web/src/components/PageLayout.js

@@ -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',

+ 9 - 21
web/src/components/SiderBar.js

@@ -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));

+ 3 - 2
web/src/components/playground/ChatArea.js

@@ -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 ? (

+ 1 - 2
web/src/components/playground/ConfigManager.js

@@ -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>
 

+ 3 - 2
web/src/components/playground/DebugPanel.js

@@ -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

+ 2 - 10
web/src/components/playground/MessageContent.js

@@ -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>
     );
   }

+ 2 - 1
web/src/components/playground/SettingsPanel.js

@@ -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%',

+ 202 - 92
web/src/context/Style/index.js

@@ -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',
+};
 
-export const StyleProvider = ({ children }) => {
-  const location = useLocation();
-  const initialIsMobile = getIsMobile();
+// 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,
+  };
+};
 
-  const initialPathname = location.pathname;
-  let initialShowSiderValue = false;
-  let initialInnerPaddingValue = 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,
+      };
 
-  if (initialPathname.includes('/console')) {
-    initialShowSiderValue = !initialIsMobile;
-    initialInnerPaddingValue = 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;
   }
+};
 
-  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 }));
-    }
-  }, []);
+// Context (内部使用,不导出)
+const StyleContext = createContext(null);
 
+/**
+ * 自定义 Hook - 处理窗口大小变化
+ * @param {Function} dispatch - dispatch 函数
+ * @param {Object} state - 当前状态
+ * @param {string} pathname - 当前路径
+ */
+const useWindowResize = (dispatch, state, pathname) => {
   useEffect(() => {
-    const updateMobileStatus = () => {
-      const currentIsMobile = getIsMobile();
-      if (!currentIsMobile &&
-        (location.pathname === '/console' || location.pathname.startsWith('/console/'))) {
-        dispatch({ type: 'SET_SIDER', payload: true, manual: false });
+    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
+        });
       }
-      dispatch({ type: 'SET_MOBILE', payload: currentIsMobile });
     };
-    window.addEventListener('resize', updateMobileStatus);
-    return () => window.removeEventListener('resize', updateMobileStatus);
-  }, [dispatch, location.pathname]);
 
+    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(() => {
-    if (state.isMobile && state.showSider && !state.manualSiderControl) {
-      dispatch({ type: 'SET_SIDER', payload: false });
-    }
-  }, [state.isMobile, state.showSider, state.manualSiderControl, dispatch]);
+    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(() => {
-    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 });
+    // 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
+    if (state.isMobile && state.showSider && !state.isManualSiderControl) {
+      dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
     }
-  }, [location.pathname, dispatch]);
+  }, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
+};
 
-  useEffect(() => {
-    const isCollapsed =
-      localStorage.getItem('default_collapse_sidebar') === 'true';
-    dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
-  }, [dispatch]);
+/**
+ * Style Provider 组件
+ */
+export const StyleProvider = ({ children }) => {
+  const location = useLocation();
+  const pathname = location.pathname;
+
+  const [state, dispatch] = useReducer(
+    styleReducer,
+    pathname,
+    getInitialState
+  );
+
+  useWindowResize(dispatch, state, pathname);
+  useRouteChange(dispatch, pathname);
+  useMobileSiderAutoHide(state, 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
+  }),
+};

+ 0 - 4
web/src/pages/Detail/index.js

@@ -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 - 6
web/src/pages/Playground/index.js

@@ -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}

+ 3 - 13
web/src/pages/Setup/index.js

@@ -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({

+ 4 - 4
web/src/utils/constants.js

@@ -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: '',