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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
46
frontend/src/components/PlaceholderPage/PlaceholderPage.css
Normal file
46
frontend/src/components/PlaceholderPage/PlaceholderPage.css
Normal 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;
|
||||
}
|
||||
43
frontend/src/components/PlaceholderPage/PlaceholderPage.jsx
Normal file
43
frontend/src/components/PlaceholderPage/PlaceholderPage.jsx
Normal 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;
|
||||
1
frontend/src/components/PlaceholderPage/index.js
Normal file
1
frontend/src/components/PlaceholderPage/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './PlaceholderPage';
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './PageModeToggle';
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user