Add agent workflow engine foundation and theme-style page switcher.

Introduce WorkflowEngine with state machine, tool registry, and builtin chat template; migrate stream chat to engine callbacks. Move page mode switching to TopBar actions cluster as a ThemeToggle-style dropdown (聊天/工作室/爽文/房间).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 02:30:43 +08:00
parent d6745b45a5
commit bc130d98f4
24 changed files with 1164 additions and 614 deletions

View File

@@ -4,6 +4,7 @@ import TopBar from './components/TopBar';
import { ChatBox } from './components/Mid';
import SideBarLeft from './components/SideBarLeft';
import SideBarRight from './components/SideBarRight';
import PlaceholderPage from './components/PlaceholderPage';
import useAppLayoutStore from './Store/AppLayoutSlice'; // ✅ 新增
import useApiConfigStore from './Store/SideBarLeft/ApiConfigSlice'; // ✅ 引入 API 配置 Store
import usePresetStore from './Store/SideBarLeft/PresetSlice'; // ✅ 引入预设 Store
@@ -18,6 +19,7 @@ function App() {
sidebarMode,
isSidebarHovered,
colorTheme,
activePage,
setLayoutMode,
setSidebarMode,
setSidebarHovered,
@@ -193,28 +195,34 @@ function App() {
<TopBar />
{/* 主内容容器 */}
<div className="main-container">
{/* 左侧栏 - 智能模式下悬停展开 */}
<div
className={`sidebar-left-wrapper sidebar-mode-${sidebarMode} ${sidebarMode === 'smart' && isSidebarHovered ? 'sidebar-expanded' : ''}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<SideBarLeft />
</div>
{activePage === 'chat' ? (
<div className="main-container">
{/* 左侧栏 - 智能模式下悬停展开 */}
<div
className={`sidebar-left-wrapper sidebar-mode-${sidebarMode} ${sidebarMode === 'smart' && isSidebarHovered ? 'sidebar-expanded' : ''}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<SideBarLeft />
</div>
{/* 中间栏:聊天框 */}
<div className="chat-area-wrapper">
<div className="chat-area">
<ChatBox />
{/* 中间栏:聊天框 */}
<div className="chat-area-wrapper">
<div className="chat-area">
<ChatBox />
</div>
</div>
{/* 右侧栏 */}
<div className="sidebar-right-wrapper">
<SideBarRight />
</div>
</div>
{/* 右侧栏 */}
<div className="sidebar-right-wrapper">
<SideBarRight />
) : (
<div className="main-container placeholder-container">
<PlaceholderPage page={activePage} />
</div>
</div>
)}
</div>
);
}

View File

@@ -22,6 +22,9 @@ const useAppLayoutStore = create(
// 颜色主题:'light' | 'dark'
colorTheme: 'dark',
// 当前页面:'chat' | 'studio' | 'novel' | 'room'
activePage: 'chat',
// ==================== Actions ====================
/**
@@ -48,6 +51,14 @@ const useAppLayoutStore = create(
set({ isSidebarHovered: hovered });
},
/**
* 设置当前页面
* @param {string} page - 'chat' | 'studio' | 'novel' | 'room'
*/
setActivePage: (page) => {
set({ activePage: page });
},
/**
* 设置颜色主题
* @param {string} theme - 主题名
@@ -78,7 +89,8 @@ const useAppLayoutStore = create(
layoutMode: 'chat',
sidebarMode: 'both',
isSidebarHovered: false,
colorTheme: 'dark'
colorTheme: 'dark',
activePage: 'chat',
});
}
}),
@@ -87,7 +99,8 @@ const useAppLayoutStore = create(
partialize: (state) => ({
layoutMode: state.layoutMode,
sidebarMode: state.sidebarMode,
colorTheme: state.colorTheme
colorTheme: state.colorTheme,
activePage: state.activePage,
})
}
)

View File

@@ -0,0 +1,46 @@
.placeholder-page {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
padding: var(--spacing-xl);
background: var(--color-bg-primary);
}
.placeholder-card {
text-align: center;
max-width: 420px;
padding: var(--spacing-xl) var(--spacing-lg);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border-light);
background: var(--color-bg-secondary);
box-shadow: var(--shadow-sm);
}
.placeholder-icon {
font-size: 3rem;
display: block;
margin-bottom: var(--spacing-md);
opacity: 0.85;
}
.placeholder-title {
margin: 0 0 var(--spacing-sm);
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
}
.placeholder-subtitle {
margin: 0 0 var(--spacing-md);
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.placeholder-message {
margin: 0;
font-size: 0.95rem;
color: var(--color-text-tertiary, var(--color-text-secondary));
letter-spacing: 0.02em;
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import './PlaceholderPage.css';
const PLACEHOLDER_META = {
studio: {
title: '角色 / 世界书工作室',
subtitle: '用途1 · Character & Worldbook Studio',
icon: '🎭',
},
novel: {
title: '爽文模式',
subtitle: '用途3 · Novel Mode',
icon: '📖',
},
room: {
title: '多角色房间',
subtitle: '用途4 · Multi-Character Room',
icon: '🏠',
},
};
function PlaceholderPage({ page }) {
const meta = PLACEHOLDER_META[page] || {
title: 'Coming Soon',
subtitle: '',
icon: '🚧',
};
return (
<div className="placeholder-page">
<div className="placeholder-card">
<span className="placeholder-icon" aria-hidden="true">{meta.icon}</span>
<h1 className="placeholder-title">{meta.title}</h1>
{meta.subtitle && (
<p className="placeholder-subtitle">{meta.subtitle}</p>
)}
<p className="placeholder-message">Coming soon</p>
</div>
</div>
);
}
export default PlaceholderPage;

View File

@@ -0,0 +1 @@
export { default } from './PlaceholderPage';

View File

@@ -4,6 +4,7 @@ import useAppLayoutStore from '../../Store/AppLayoutSlice'; // ✅ 新增
import useUserStore from '../../Store/UserSlice'; // ✅ 新增 - 用户角色
import useWorldBookStore from '../../Store/SideBarLeft/WorldBookSlice';
import useApiConfigStore from '../../Store/SideBarLeft/ApiConfigSlice';
import PageModeToggle from './items/PageModeToggle';
import ThemeToggle from './items/ThemeToggle';
import RegexPanel from '../SideBarLeft/tabs/Regex/RegexPanel'; // ✅ 新增:导入正则管理组件
import './TopBar.css';
@@ -17,7 +18,7 @@ const Toolbar = () => {
sidebarMode,
colorTheme,
setSidebarMode,
setColorTheme
setColorTheme,
} = useAppLayoutStore();
// ✅ 从 UserStore 获取用户角色和方法
@@ -207,6 +208,9 @@ const Toolbar = () => {
</button>
{/* 页面模式 */}
<PageModeToggle />
{/* 主题切换 */}
<ThemeToggle />
</div>

View File

@@ -0,0 +1,88 @@
.page-mode-toggle-wrapper {
position: relative;
}
.page-mode-toggle {
/* inherits from .action-btn */
}
.page-mode-selector-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
min-width: 160px;
z-index: var(--z-popover);
animation: pageModeFadeInScale var(--transition-fast);
}
@keyframes pageModeFadeInScale {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.page-mode-selector-header {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-light);
}
.page-mode-selector-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-ui);
}
.page-mode-list {
padding: var(--spacing-xs);
display: flex;
flex-direction: column;
gap: 2px;
}
.page-mode-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: transparent;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
text-align: left;
width: 100%;
}
.page-mode-item:hover {
background-color: var(--color-bg-tertiary);
}
.page-mode-item.active {
background-color: var(--color-accent-light);
border: 1px solid var(--color-accent);
}
.page-mode-item-icon {
font-size: 1.3rem;
line-height: 1;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.page-mode-item-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-ui);
}

View File

@@ -0,0 +1,79 @@
import React, { useState, useEffect, useRef } from 'react';
import useAppLayoutStore from '../../../../Store/AppLayoutSlice';
import './PageModeToggle.css';
const PAGE_MODES = [
{ id: 'chat', label: '聊天', icon: '💬' },
{ id: 'studio', label: '工作室', icon: '🎭' },
{ id: 'novel', label: '爽文', icon: '📖' },
{ id: 'room', label: '房间', icon: '🏠' },
];
const PageModeToggle = () => {
const [isOpen, setIsOpen] = useState(false);
const panelRef = useRef(null);
const { activePage, setActivePage } = useAppLayoutStore();
useEffect(() => {
const handleClickOutside = (event) => {
if (panelRef.current && !panelRef.current.contains(event.target)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const handleModeSelect = (modeId) => {
setActivePage(modeId);
setIsOpen(false);
};
const currentMode = PAGE_MODES.find((m) => m.id === activePage) || PAGE_MODES[0];
return (
<div className="page-mode-toggle-wrapper" ref={panelRef}>
<button
className="action-btn page-mode-toggle"
onClick={() => setIsOpen(!isOpen)}
title={`当前模式:${currentMode.label}`}
aria-haspopup="listbox"
aria-expanded={isOpen}
>
{currentMode.icon}
</button>
{isOpen && (
<div className="page-mode-selector-panel" role="listbox" aria-label="应用模式">
<div className="page-mode-selector-header">
<span className="page-mode-selector-title">应用模式</span>
</div>
<div className="page-mode-list">
{PAGE_MODES.map((mode) => (
<button
key={mode.id}
type="button"
role="option"
aria-selected={activePage === mode.id}
className={`page-mode-item ${activePage === mode.id ? 'active' : ''}`}
onClick={() => handleModeSelect(mode.id)}
>
<span className="page-mode-item-icon">{mode.icon}</span>
<span className="page-mode-item-label">{mode.label}</span>
</button>
))}
</div>
</div>
)}
</div>
);
};
export default PageModeToggle;

View File

@@ -0,0 +1 @@
export { default } from './PageModeToggle';

View File

@@ -27,6 +27,10 @@
margin-top: 0; /* Ensure panels start from top */
}
.main-container.placeholder-container {
flex-direction: column;
}
/* Smooth transitions for theme changes - only apply to specific properties */
.app {
transition: background-color var(--transition-normal),