Dify接入初版
This commit is contained in:
+398
-398
@@ -1,399 +1,399 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from '@remix-run/react';
|
||||
import type { UserRole } from '~/root';
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
hideBreadcrumb?: boolean;
|
||||
requiredRole?: UserRole;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
onToggle: () => void;
|
||||
collapsed: boolean;
|
||||
userRole: UserRole;
|
||||
selectedApp?: string; // 添加所选应用模块参数
|
||||
}
|
||||
|
||||
// 定义不同应用模块下显示的菜单项ID
|
||||
const APP_MENU_MAP = {
|
||||
'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'],
|
||||
'record': ['home', 'file-management', 'rule-management', 'system-settings'],
|
||||
'model': ['chat-with-llm']
|
||||
};
|
||||
|
||||
// 应用模块名称映射
|
||||
const APP_NAME_MAP: Record<string, string> = {
|
||||
'contract': '合同管理',
|
||||
'record': '案卷智能评查',
|
||||
'model': '智慧法务大模型'
|
||||
};
|
||||
|
||||
// 应用模块图标映射
|
||||
const APP_ICON_MAP: Record<string, string> = {
|
||||
'contract': 'ri-file-list-2-fill',
|
||||
'record': 'ri-folder-shared-fill',
|
||||
'model': 'ri-robot-2-fill'
|
||||
};
|
||||
|
||||
export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract' }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
||||
const [currentApp, setCurrentApp] = useState<string>(selectedApp);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 组件挂载后从 sessionStorage 读取初始 reviewType
|
||||
useEffect(() => {
|
||||
try {
|
||||
const reviewType = sessionStorage.getItem('reviewType');
|
||||
// console.log('初始 reviewType:', reviewType);
|
||||
if (reviewType) {
|
||||
setCurrentApp(reviewType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取 reviewType 失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从 sessionStorage 获取 reviewType 并设置当前应用模块
|
||||
useEffect(() => {
|
||||
// 监听 sessionStorage 变化(主要用于多标签页情况)
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'reviewType' && e.newValue) {
|
||||
setCurrentApp(e.newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加事件监听器
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听路由变化,重新检查 reviewType
|
||||
useEffect(() => {
|
||||
try {
|
||||
const reviewType = sessionStorage.getItem('reviewType');
|
||||
// console.log('路由变化, 检查 reviewType:', reviewType, '路径:', location.pathname);
|
||||
if (reviewType) {
|
||||
setCurrentApp(reviewType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('路由变化时读取 reviewType 失败:', error);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
// 监听 selectedApp 属性变化
|
||||
useEffect(() => {
|
||||
if (selectedApp) {
|
||||
setCurrentApp(selectedApp);
|
||||
}
|
||||
}, [selectedApp]);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
id: 'home',
|
||||
title: '系统概览',
|
||||
path: '/home',
|
||||
icon: 'ri-home-line'
|
||||
},
|
||||
{
|
||||
id: 'chat-with-llm',
|
||||
title: 'AI对话',
|
||||
path: '/chat-with-llm',
|
||||
icon: 'ri-chat-smile-2-line'
|
||||
},
|
||||
{
|
||||
id: 'file-management',
|
||||
title: '文件管理',
|
||||
path: '/files',
|
||||
icon: 'ri-folder-line',
|
||||
children: [
|
||||
{
|
||||
id: 'file-upload',
|
||||
title: '文件上传',
|
||||
path: '/files/upload',
|
||||
icon: 'ri-upload-cloud-line'
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
title: '文档列表',
|
||||
path: '/documents',
|
||||
icon: 'ri-file-list-3-line'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'rule-management',
|
||||
title: '评查规则库',
|
||||
path: '/rules',
|
||||
icon: 'ri-book-3-line',
|
||||
children: [
|
||||
{
|
||||
id: 'rule-groups',
|
||||
title: '评查点分组',
|
||||
path: '/rule-groups',
|
||||
icon: 'ri-folder-open-line'
|
||||
},
|
||||
{
|
||||
id: 'rules-list',
|
||||
title: '评查点列表',
|
||||
path: '/rules',
|
||||
icon: 'ri-list-check-3'
|
||||
},
|
||||
{
|
||||
id: 'rules-file',
|
||||
title: '评查文件列表',
|
||||
path: '/rules-files',
|
||||
icon: 'ri-list-check-2'
|
||||
},
|
||||
// {
|
||||
// id: 'rule-new',
|
||||
// title: '新增评查点',
|
||||
// path: '/rules-new',
|
||||
// requiredRole: 'developer',
|
||||
// icon: 'ri-add-circle-line'
|
||||
// },
|
||||
// {
|
||||
// id: 'review-detail',
|
||||
// title: '评查详情',
|
||||
// path: '/reviews',
|
||||
// icon: 'ri-file-chart-line'
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'contract-template',
|
||||
title: '合同模板',
|
||||
path: '/contract-template',
|
||||
icon: 'ri-file-search-line',
|
||||
children: [
|
||||
{
|
||||
id: 'contract-search-ai',
|
||||
title: '智能搜索',
|
||||
path: '/contract-template/search',
|
||||
icon: 'ri-search-line'
|
||||
},
|
||||
{
|
||||
id: 'contract-list',
|
||||
title: '合同列表',
|
||||
path: '/contract-template/list',
|
||||
icon: 'ri-folder-line'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system-settings',
|
||||
title: '系统设置',
|
||||
path: '/settings',
|
||||
icon: 'ri-settings-4-line',
|
||||
requiredRole: 'developer',
|
||||
children: [
|
||||
{
|
||||
id: 'config-lists',
|
||||
title: '配置列表',
|
||||
path: '/config-lists',
|
||||
icon: 'ri-list-check-3',
|
||||
requiredRole: 'developer'
|
||||
},
|
||||
// {
|
||||
// id: 'basic-settings',
|
||||
// title: '基础设置',
|
||||
// path: '/settings',
|
||||
// icon: 'ri-equalizer-line'
|
||||
// },
|
||||
{
|
||||
id: 'document-types',
|
||||
title: '文档类型',
|
||||
path: '/document-types',
|
||||
icon: 'ri-file-list-line',
|
||||
requiredRole: 'developer'
|
||||
},
|
||||
{
|
||||
id: 'prompt-management',
|
||||
title: '提示词管理',
|
||||
path: '/prompts',
|
||||
icon: 'ri-chat-1-line',
|
||||
requiredRole: 'developer'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 初始化展开状态,默认全部展开
|
||||
useEffect(() => {
|
||||
const initialExpandedState: Record<string, boolean> = {};
|
||||
menuItems.forEach(item => {
|
||||
if (item.children) {
|
||||
initialExpandedState[item.id] = true;
|
||||
}
|
||||
});
|
||||
setExpandedMenus(initialExpandedState);
|
||||
}, []);
|
||||
|
||||
const toggleMenu = (id: string, e: React.MouseEvent) => {
|
||||
// 我们只防止事件冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
|
||||
// console.log('父菜单展开/折叠:', id);
|
||||
|
||||
setExpandedMenus(prev => ({
|
||||
...prev,
|
||||
[id]: !prev[id]
|
||||
}));
|
||||
};
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return location.pathname === path || location.pathname.startsWith(`${path}/`);
|
||||
};
|
||||
|
||||
// 处理侧边栏切换事件
|
||||
const handleToggleSidebar = (e: React.MouseEvent) => {
|
||||
// console.log('侧边栏折叠/展开');
|
||||
// 只防止事件冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
};
|
||||
|
||||
// 处理子菜单项点击事件
|
||||
const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => {
|
||||
// 只需要阻止冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
// console.log('子菜单点击:', child.title, '路径:', child.path);
|
||||
};
|
||||
|
||||
// 获取当前应用模式下应显示的菜单ID列表
|
||||
const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract'];
|
||||
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
|
||||
|
||||
// 根据用户角色和当前应用模式过滤菜单项
|
||||
const filteredMenuItems = menuItems.filter(item => {
|
||||
// 如果菜单项需要特定角色但用户没有
|
||||
if (item.requiredRole && item.requiredRole !== userRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查当前菜单是否在所选应用模式中显示
|
||||
if (!visibleMenuIds.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className="py-6 px-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<div className="flex items-center"
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src="/logo.svg" alt="智慧法务" className="w-12 h-12 mr-2" />
|
||||
{!collapsed && <h2 className="text-lg font-medium">智慧法务</h2>}
|
||||
</div>
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
onClick={handleToggleSidebar}
|
||||
aria-label={collapsed ? "展开侧边栏" : "折叠侧边栏"}
|
||||
type="button"
|
||||
>
|
||||
<i className={`${collapsed ? 'ri-menu-unfold-line' : 'ri-menu-fold-line'}`}></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<div className="flex items-center text-green-700">
|
||||
<i className={`${APP_ICON_MAP[currentApp] || 'ri-file-list-2-fill'} mr-2 text-xl`}></i>
|
||||
<span className="font-medium">{APP_NAME_MAP[currentApp] || '合同管理'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="py-4 px-[10px]">
|
||||
{filteredMenuItems.map((item) => (
|
||||
<div key={item.id} className={`${collapsed ? 'px-0' : ''}`}>
|
||||
{!item.children ? (
|
||||
<Link
|
||||
to={item.path}
|
||||
className={`sidebar-menu-item ${isActive(item.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||||
onClick={(e) => {
|
||||
// 只阻止冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
// console.log('单级菜单点击:', item.title, '路径:', item.path);
|
||||
}}
|
||||
>
|
||||
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{item.title}</span>}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer z-10`}
|
||||
onClick={(e) => {
|
||||
// console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title);
|
||||
toggleMenu(item.id, e);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={expandedMenus[item.id] || false}
|
||||
aria-controls={`submenu-${item.id}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleMenu(item.id, e as unknown as React.MouseEvent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{item.title}</span>}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<i className={`ri-arrow-${expandedMenus[item.id] ? 'down' : 'right'}-s-line`}></i>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(expandedMenus[item.id] || collapsed) && (
|
||||
<div
|
||||
className={`submenu-container ${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'} z-20`}
|
||||
id={`submenu-${item.id}`}
|
||||
>
|
||||
{item.children
|
||||
.filter(child => !child.requiredRole || child.requiredRole === userRole)
|
||||
.map((child) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
to={child.path}
|
||||
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||||
onClick={(e) => handleSubMenuClick(child, e)}
|
||||
>
|
||||
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{child.title}</span>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from '@remix-run/react';
|
||||
import type { UserRole } from '~/root';
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
hideBreadcrumb?: boolean;
|
||||
requiredRole?: UserRole;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
onToggle: () => void;
|
||||
collapsed: boolean;
|
||||
userRole: UserRole;
|
||||
selectedApp?: string; // 添加所选应用模块参数
|
||||
}
|
||||
|
||||
// 定义不同应用模块下显示的菜单项ID
|
||||
const APP_MENU_MAP = {
|
||||
'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'],
|
||||
'record': ['home', 'file-management', 'rule-management', 'system-settings'],
|
||||
'model': ['chat-with-llm']
|
||||
};
|
||||
|
||||
// 应用模块名称映射
|
||||
const APP_NAME_MAP: Record<string, string> = {
|
||||
'contract': '合同管理',
|
||||
'record': '案卷智能评查',
|
||||
'model': '智慧法务大模型'
|
||||
};
|
||||
|
||||
// 应用模块图标映射
|
||||
const APP_ICON_MAP: Record<string, string> = {
|
||||
'contract': 'ri-file-list-2-fill',
|
||||
'record': 'ri-folder-shared-fill',
|
||||
'model': 'ri-robot-2-fill'
|
||||
};
|
||||
|
||||
export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract' }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
||||
const [currentApp, setCurrentApp] = useState<string>(selectedApp);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 组件挂载后从 sessionStorage 读取初始 reviewType
|
||||
useEffect(() => {
|
||||
try {
|
||||
const reviewType = sessionStorage.getItem('reviewType');
|
||||
// console.log('初始 reviewType:', reviewType);
|
||||
if (reviewType) {
|
||||
setCurrentApp(reviewType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取 reviewType 失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从 sessionStorage 获取 reviewType 并设置当前应用模块
|
||||
useEffect(() => {
|
||||
// 监听 sessionStorage 变化(主要用于多标签页情况)
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'reviewType' && e.newValue) {
|
||||
setCurrentApp(e.newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加事件监听器
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听路由变化,重新检查 reviewType
|
||||
useEffect(() => {
|
||||
try {
|
||||
const reviewType = sessionStorage.getItem('reviewType');
|
||||
// console.log('路由变化, 检查 reviewType:', reviewType, '路径:', location.pathname);
|
||||
if (reviewType) {
|
||||
setCurrentApp(reviewType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('路由变化时读取 reviewType 失败:', error);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
// 监听 selectedApp 属性变化
|
||||
useEffect(() => {
|
||||
if (selectedApp) {
|
||||
setCurrentApp(selectedApp);
|
||||
}
|
||||
}, [selectedApp]);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
id: 'home',
|
||||
title: '系统概览',
|
||||
path: '/home',
|
||||
icon: 'ri-home-line'
|
||||
},
|
||||
{
|
||||
id: 'chat-with-llm',
|
||||
title: 'AI对话',
|
||||
path: '/chat-with-llm',
|
||||
icon: 'ri-chat-smile-2-line'
|
||||
},
|
||||
{
|
||||
id: 'file-management',
|
||||
title: '文件管理',
|
||||
path: '/files',
|
||||
icon: 'ri-folder-line',
|
||||
children: [
|
||||
{
|
||||
id: 'file-upload',
|
||||
title: '文件上传',
|
||||
path: '/files/upload',
|
||||
icon: 'ri-upload-cloud-line'
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
title: '文档列表',
|
||||
path: '/documents',
|
||||
icon: 'ri-file-list-3-line'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'rule-management',
|
||||
title: '评查规则库',
|
||||
path: '/rules',
|
||||
icon: 'ri-book-3-line',
|
||||
children: [
|
||||
{
|
||||
id: 'rule-groups',
|
||||
title: '评查点分组',
|
||||
path: '/rule-groups',
|
||||
icon: 'ri-folder-open-line'
|
||||
},
|
||||
{
|
||||
id: 'rules-list',
|
||||
title: '评查点列表',
|
||||
path: '/rules',
|
||||
icon: 'ri-list-check-3'
|
||||
},
|
||||
{
|
||||
id: 'rules-file',
|
||||
title: '评查文件列表',
|
||||
path: '/rules-files',
|
||||
icon: 'ri-list-check-2'
|
||||
},
|
||||
// {
|
||||
// id: 'rule-new',
|
||||
// title: '新增评查点',
|
||||
// path: '/rules-new',
|
||||
// requiredRole: 'developer',
|
||||
// icon: 'ri-add-circle-line'
|
||||
// },
|
||||
// {
|
||||
// id: 'review-detail',
|
||||
// title: '评查详情',
|
||||
// path: '/reviews',
|
||||
// icon: 'ri-file-chart-line'
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'contract-template',
|
||||
title: '合同模板',
|
||||
path: '/contract-template',
|
||||
icon: 'ri-file-search-line',
|
||||
children: [
|
||||
{
|
||||
id: 'contract-search-ai',
|
||||
title: '智能搜索',
|
||||
path: '/contract-template/search',
|
||||
icon: 'ri-search-line'
|
||||
},
|
||||
{
|
||||
id: 'contract-list',
|
||||
title: '合同列表',
|
||||
path: '/contract-template/list',
|
||||
icon: 'ri-folder-line'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system-settings',
|
||||
title: '系统设置',
|
||||
path: '/settings',
|
||||
icon: 'ri-settings-4-line',
|
||||
requiredRole: 'developer',
|
||||
children: [
|
||||
{
|
||||
id: 'config-lists',
|
||||
title: '配置列表',
|
||||
path: '/config-lists',
|
||||
icon: 'ri-list-check-3',
|
||||
requiredRole: 'developer'
|
||||
},
|
||||
// {
|
||||
// id: 'basic-settings',
|
||||
// title: '基础设置',
|
||||
// path: '/settings',
|
||||
// icon: 'ri-equalizer-line'
|
||||
// },
|
||||
{
|
||||
id: 'document-types',
|
||||
title: '文档类型',
|
||||
path: '/document-types',
|
||||
icon: 'ri-file-list-line',
|
||||
requiredRole: 'developer'
|
||||
},
|
||||
{
|
||||
id: 'prompt-management',
|
||||
title: '提示词管理',
|
||||
path: '/prompts',
|
||||
icon: 'ri-chat-1-line',
|
||||
requiredRole: 'developer'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 初始化展开状态,默认全部展开
|
||||
useEffect(() => {
|
||||
const initialExpandedState: Record<string, boolean> = {};
|
||||
menuItems.forEach(item => {
|
||||
if (item.children) {
|
||||
initialExpandedState[item.id] = true;
|
||||
}
|
||||
});
|
||||
setExpandedMenus(initialExpandedState);
|
||||
}, []);
|
||||
|
||||
const toggleMenu = (id: string, e: React.MouseEvent) => {
|
||||
// 我们只防止事件冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
|
||||
// console.log('父菜单展开/折叠:', id);
|
||||
|
||||
setExpandedMenus(prev => ({
|
||||
...prev,
|
||||
[id]: !prev[id]
|
||||
}));
|
||||
};
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return location.pathname === path || location.pathname.startsWith(`${path}/`);
|
||||
};
|
||||
|
||||
// 处理侧边栏切换事件
|
||||
const handleToggleSidebar = (e: React.MouseEvent) => {
|
||||
// console.log('侧边栏折叠/展开');
|
||||
// 只防止事件冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
};
|
||||
|
||||
// 处理子菜单项点击事件
|
||||
const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => {
|
||||
// 只需要阻止冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
// console.log('子菜单点击:', child.title, '路径:', child.path);
|
||||
};
|
||||
|
||||
// 获取当前应用模式下应显示的菜单ID列表
|
||||
const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract'];
|
||||
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
|
||||
|
||||
// 根据用户角色和当前应用模式过滤菜单项
|
||||
const filteredMenuItems = menuItems.filter(item => {
|
||||
// 如果菜单项需要特定角色但用户没有
|
||||
if (item.requiredRole && item.requiredRole !== userRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查当前菜单是否在所选应用模式中显示
|
||||
if (!visibleMenuIds.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className="py-6 px-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<div className="flex items-center"
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src="/logo.svg" alt="智慧法务" className="w-12 h-12 mr-2" />
|
||||
{!collapsed && <h2 className="text-lg font-medium">智慧法务</h2>}
|
||||
</div>
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
onClick={handleToggleSidebar}
|
||||
aria-label={collapsed ? "展开侧边栏" : "折叠侧边栏"}
|
||||
type="button"
|
||||
>
|
||||
<i className={`${collapsed ? 'ri-menu-unfold-line' : 'ri-menu-fold-line'}`}></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<div className="flex items-center text-green-700">
|
||||
<i className={`${APP_ICON_MAP[currentApp] || 'ri-file-list-2-fill'} mr-2 text-xl`}></i>
|
||||
<span className="font-medium">{APP_NAME_MAP[currentApp] || '合同管理'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="py-4 px-[10px]">
|
||||
{filteredMenuItems.map((item) => (
|
||||
<div key={item.id} className={`${collapsed ? 'px-0' : ''}`}>
|
||||
{!item.children ? (
|
||||
<Link
|
||||
to={item.path}
|
||||
className={`sidebar-menu-item ${isActive(item.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||||
onClick={(e) => {
|
||||
// 只阻止冒泡,不阻止默认行为
|
||||
e.stopPropagation();
|
||||
// console.log('单级菜单点击:', item.title, '路径:', item.path);
|
||||
}}
|
||||
>
|
||||
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{item.title}</span>}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer z-10`}
|
||||
onClick={(e) => {
|
||||
// console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title);
|
||||
toggleMenu(item.id, e);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={expandedMenus[item.id] || false}
|
||||
aria-controls={`submenu-${item.id}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleMenu(item.id, e as unknown as React.MouseEvent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{item.title}</span>}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<i className={`ri-arrow-${expandedMenus[item.id] ? 'down' : 'right'}-s-line`}></i>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(expandedMenus[item.id] || collapsed) && (
|
||||
<div
|
||||
className={`submenu-container ${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'} z-20`}
|
||||
id={`submenu-${item.id}`}
|
||||
>
|
||||
{item.children
|
||||
.filter(child => !child.requiredRole || child.requiredRole === userRole)
|
||||
.map((child) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
to={child.path}
|
||||
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||||
onClick={(e) => handleSubMenuClick(child, e)}
|
||||
>
|
||||
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{child.title}</span>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+270
-270
@@ -1,271 +1,271 @@
|
||||
// import React from 'react';
|
||||
import {
|
||||
Links,
|
||||
// LiveReload, // 不再需要,使用Vite时会与内置HMR冲突
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
isRouteErrorResponse,
|
||||
useRouteError,
|
||||
type MetaFunction,
|
||||
useLoaderData
|
||||
} from "@remix-run/react";
|
||||
import {
|
||||
LoaderFunctionArgs,
|
||||
redirect,
|
||||
createCookieSessionStorage,
|
||||
ActionFunctionArgs
|
||||
} from "@remix-run/node";
|
||||
import { Layout } from "~/components/layout/Layout";
|
||||
import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary";
|
||||
import { MessageModalProvider } from "~/components/ui/MessageModal";
|
||||
import { ToastProvider } from "~/components/ui/Toast";
|
||||
import "remixicon/fonts/remixicon.css";
|
||||
// 导入样式
|
||||
import styles from "~/styles/main.css?url";
|
||||
import messageModalStyles from "~/styles/components/message-modal.css?url";
|
||||
import toastStyles from "~/styles/components/toast.css?url";
|
||||
import LoadingBarContainer from "~/components/ui/LoadingBar";
|
||||
import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
|
||||
// import { useState, useEffect } from "react";
|
||||
|
||||
// 定义用户角色类型
|
||||
export type UserRole = 'common' | 'developer';
|
||||
|
||||
// 定义需要高级权限的路径
|
||||
export const developerOnlyPaths = [
|
||||
'/settings',
|
||||
'/config-lists',
|
||||
'/document-types',
|
||||
'/prompts'
|
||||
];
|
||||
|
||||
// 创建基于Cookie的会话存储
|
||||
// 在实际应用中,应该使用环境变量来设置密钥
|
||||
const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__session",
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secrets: ["s3cr3t"], // 应该从环境变量读取
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
});
|
||||
|
||||
// 获取会话对象
|
||||
export async function getSession(request: Request) {
|
||||
const cookie = request.headers.get("Cookie");
|
||||
return sessionStorage.getSession(cookie);
|
||||
}
|
||||
|
||||
// 获取用户登录状态
|
||||
export async function getUserSession(request: Request) {
|
||||
const session = await getSession(request);
|
||||
return {
|
||||
isAuthenticated: session.get("isAuthenticated") === true,
|
||||
userRole: session.get("userRole") || 'common' as UserRole
|
||||
};
|
||||
}
|
||||
|
||||
// 创建登录会话
|
||||
export async function createUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", isAuthenticated);
|
||||
session.set("userRole", userRole);
|
||||
console.log("session-----", session.get("userRole"));
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 销毁会话(登出)
|
||||
export async function logout(request: Request) {
|
||||
const session = await getSession(request);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.destroySession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 添加action处理登录/登出请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "logout") {
|
||||
return logout(request);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加loader函数进行全局认证检查并传递环境变量给客户端
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取当前路径
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 排除不需要登录验证的路径
|
||||
const publicPaths = ['/login', '/favicon.ico'];
|
||||
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
||||
|
||||
// 获取用户会话
|
||||
const { isAuthenticated, userRole } = await getUserSession(request);
|
||||
// console.log("Auth status:", { isAuthenticated, userRole, pathname });
|
||||
|
||||
// 如果访问需要认证的路径但未登录,重定向到登录页
|
||||
if (!isPublicPath && !isAuthenticated) {
|
||||
// 保存请求的URL,以便登录后重定向回来
|
||||
const session = await getSession(request);
|
||||
|
||||
// 如果路径是/home,则将重定向目标设置为/
|
||||
const redirectTarget = pathname === "/home" ? "/" : pathname;
|
||||
// 保存重定向目标
|
||||
session.set("redirectTo", redirectTarget);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 如果已登录且访问登录页,重定向到首页
|
||||
if (pathname === "/login" && isAuthenticated) {
|
||||
// console.log("Already authenticated, redirecting from login to /");
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
// 检查访问权限 - 如果是common用户访问了开发者专属页面,重定向到首页
|
||||
if (userRole === 'common' && developerOnlyPaths.some(path => pathname.startsWith(path))) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
// 向组件传递认证状态、当前路径和环境变量
|
||||
return Response.json({
|
||||
isAuthenticated,
|
||||
userRole,
|
||||
pathname,
|
||||
ENV: {
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
NEXT_PUBLIC_APP_ID: process.env.NEXT_PUBLIC_APP_ID,
|
||||
NEXT_PUBLIC_APP_KEY: process.env.NEXT_PUBLIC_APP_KEY,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ charSet: "utf-8" },
|
||||
{ name: "viewport", content: "width=device-width,initial-scale=1" },
|
||||
{ title: "中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "专业的AI合同及卷宗评查系统,提供智能审核、风险评估和规范化建议" },
|
||||
{ name: "robots", content: "noindex,nofollow" } // 内部系统,防止被搜索引擎索引
|
||||
];
|
||||
};
|
||||
|
||||
// 使用links函数为应用加载CSS和其他资源
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: styles },
|
||||
{ rel: "stylesheet", href: messageModalStyles },
|
||||
{ rel: "stylesheet", href: toastStyles },
|
||||
// 添加 Antd 样式
|
||||
{ rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" },
|
||||
{ rel: "icon", type: "image/svg+xml", href: "/logo.svg" },
|
||||
// { rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
// { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
||||
// { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" }
|
||||
];
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { userRole, ENV } = useLoaderData<typeof loader>();
|
||||
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
--color-primary: #00684a;
|
||||
--color-primary-hover: #005a3f;
|
||||
--color-primary-light: rgba(0, 104, 74, 0.1);
|
||||
--primary-color: #00684a;
|
||||
|
||||
/* 成功、警告、错误颜色 */
|
||||
--color-success: #52c41a;
|
||||
--color-warning: #faad14;
|
||||
--color-error: #f5222d;
|
||||
}
|
||||
` }} />
|
||||
<Meta />
|
||||
<Links />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.__ENV = ${JSON.stringify(ENV)}`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans">
|
||||
<MessageModalProvider>
|
||||
<ToastProvider>
|
||||
<Layout userRole={userRole}>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
<RouteChangeLoader />
|
||||
</ToastProvider>
|
||||
</MessageModalProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LoadingBarContainer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
// 为错误页面设置标题和描述
|
||||
let title = "发生错误";
|
||||
let message = "发生了一个未知错误,请稍后重试";
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
title = `错误 ${error.status}`;
|
||||
message = error.data?.message || "发生了一个错误,请稍后重试";
|
||||
} else {
|
||||
title = "意外错误";
|
||||
message = "服务器发生了意外错误,请稍后重试";
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content={message} />
|
||||
<title>{title}</title>
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<AppErrorBoundary
|
||||
status={isRouteErrorResponse(error) ? error.status : 500}
|
||||
statusText={isRouteErrorResponse(error) ? error.statusText : "服务器错误"}
|
||||
message={message}
|
||||
/>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
// import React from 'react';
|
||||
import {
|
||||
Links,
|
||||
// LiveReload, // 不再需要,使用Vite时会与内置HMR冲突
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
isRouteErrorResponse,
|
||||
useRouteError,
|
||||
type MetaFunction,
|
||||
useLoaderData
|
||||
} from "@remix-run/react";
|
||||
import {
|
||||
LoaderFunctionArgs,
|
||||
redirect,
|
||||
createCookieSessionStorage,
|
||||
ActionFunctionArgs
|
||||
} from "@remix-run/node";
|
||||
import { Layout } from "~/components/layout/Layout";
|
||||
import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary";
|
||||
import { MessageModalProvider } from "~/components/ui/MessageModal";
|
||||
import { ToastProvider } from "~/components/ui/Toast";
|
||||
import "remixicon/fonts/remixicon.css";
|
||||
// 导入样式
|
||||
import styles from "~/styles/main.css?url";
|
||||
import messageModalStyles from "~/styles/components/message-modal.css?url";
|
||||
import toastStyles from "~/styles/components/toast.css?url";
|
||||
import LoadingBarContainer from "~/components/ui/LoadingBar";
|
||||
import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
|
||||
// import { useState, useEffect } from "react";
|
||||
|
||||
// 定义用户角色类型
|
||||
export type UserRole = 'common' | 'developer';
|
||||
|
||||
// 定义需要高级权限的路径
|
||||
export const developerOnlyPaths = [
|
||||
'/settings',
|
||||
'/config-lists',
|
||||
'/document-types',
|
||||
'/prompts'
|
||||
];
|
||||
|
||||
// 创建基于Cookie的会话存储
|
||||
// 在实际应用中,应该使用环境变量来设置密钥
|
||||
const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__session",
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secrets: ["s3cr3t"], // 应该从环境变量读取
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
});
|
||||
|
||||
// 获取会话对象
|
||||
export async function getSession(request: Request) {
|
||||
const cookie = request.headers.get("Cookie");
|
||||
return sessionStorage.getSession(cookie);
|
||||
}
|
||||
|
||||
// 获取用户登录状态
|
||||
export async function getUserSession(request: Request) {
|
||||
const session = await getSession(request);
|
||||
return {
|
||||
isAuthenticated: session.get("isAuthenticated") === true,
|
||||
userRole: session.get("userRole") || 'common' as UserRole
|
||||
};
|
||||
}
|
||||
|
||||
// 创建登录会话
|
||||
export async function createUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", isAuthenticated);
|
||||
session.set("userRole", userRole);
|
||||
console.log("session-----", session.get("userRole"));
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 销毁会话(登出)
|
||||
export async function logout(request: Request) {
|
||||
const session = await getSession(request);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.destroySession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 添加action处理登录/登出请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "logout") {
|
||||
return logout(request);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加loader函数进行全局认证检查并传递环境变量给客户端
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取当前路径
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 排除不需要登录验证的路径
|
||||
const publicPaths = ['/login', '/favicon.ico'];
|
||||
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
||||
|
||||
// 获取用户会话
|
||||
const { isAuthenticated, userRole } = await getUserSession(request);
|
||||
// console.log("Auth status:", { isAuthenticated, userRole, pathname });
|
||||
|
||||
// 如果访问需要认证的路径但未登录,重定向到登录页
|
||||
if (!isPublicPath && !isAuthenticated) {
|
||||
// 保存请求的URL,以便登录后重定向回来
|
||||
const session = await getSession(request);
|
||||
|
||||
// 如果路径是/home,则将重定向目标设置为/
|
||||
const redirectTarget = pathname === "/home" ? "/" : pathname;
|
||||
// 保存重定向目标
|
||||
session.set("redirectTo", redirectTarget);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 如果已登录且访问登录页,重定向到首页
|
||||
if (pathname === "/login" && isAuthenticated) {
|
||||
// console.log("Already authenticated, redirecting from login to /");
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
// 检查访问权限 - 如果是common用户访问了开发者专属页面,重定向到首页
|
||||
if (userRole === 'common' && developerOnlyPaths.some(path => pathname.startsWith(path))) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
// 向组件传递认证状态、当前路径和环境变量
|
||||
return Response.json({
|
||||
isAuthenticated,
|
||||
userRole,
|
||||
pathname,
|
||||
ENV: {
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
NEXT_PUBLIC_APP_ID: process.env.NEXT_PUBLIC_APP_ID,
|
||||
NEXT_PUBLIC_APP_KEY: process.env.NEXT_PUBLIC_APP_KEY,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ charSet: "utf-8" },
|
||||
{ name: "viewport", content: "width=device-width,initial-scale=1" },
|
||||
{ title: "中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "专业的AI合同及卷宗评查系统,提供智能审核、风险评估和规范化建议" },
|
||||
{ name: "robots", content: "noindex,nofollow" } // 内部系统,防止被搜索引擎索引
|
||||
];
|
||||
};
|
||||
|
||||
// 使用links函数为应用加载CSS和其他资源
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: styles },
|
||||
{ rel: "stylesheet", href: messageModalStyles },
|
||||
{ rel: "stylesheet", href: toastStyles },
|
||||
// 添加 Antd 样式
|
||||
{ rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" },
|
||||
{ rel: "icon", type: "image/svg+xml", href: "/logo.svg" },
|
||||
// { rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
// { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
||||
// { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" }
|
||||
];
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { userRole, ENV } = useLoaderData<typeof loader>();
|
||||
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
--color-primary: #00684a;
|
||||
--color-primary-hover: #005a3f;
|
||||
--color-primary-light: rgba(0, 104, 74, 0.1);
|
||||
--primary-color: #00684a;
|
||||
|
||||
/* 成功、警告、错误颜色 */
|
||||
--color-success: #52c41a;
|
||||
--color-warning: #faad14;
|
||||
--color-error: #f5222d;
|
||||
}
|
||||
` }} />
|
||||
<Meta />
|
||||
<Links />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.__ENV = ${JSON.stringify(ENV)}`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans">
|
||||
<MessageModalProvider>
|
||||
<ToastProvider>
|
||||
<Layout userRole={userRole}>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
<RouteChangeLoader />
|
||||
</ToastProvider>
|
||||
</MessageModalProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LoadingBarContainer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
// 为错误页面设置标题和描述
|
||||
let title = "发生错误";
|
||||
let message = "发生了一个未知错误,请稍后重试";
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
title = `错误 ${error.status}`;
|
||||
message = error.data?.message || "发生了一个错误,请稍后重试";
|
||||
} else {
|
||||
title = "意外错误";
|
||||
message = "服务器发生了意外错误,请稍后重试";
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content={message} />
|
||||
<title>{title}</title>
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<AppErrorBoundary
|
||||
status={isRouteErrorResponse(error) ? error.status : 500}
|
||||
statusText={isRouteErrorResponse(error) ? error.statusText : "服务器错误"}
|
||||
message={message}
|
||||
/>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
+190
-190
@@ -1,191 +1,191 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, Form, useLoaderData } from '@remix-run/react';
|
||||
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import styles from "~/styles/pages/home.css?url";
|
||||
import dayjs from 'dayjs';
|
||||
import { getUserSession, logout } from "~/root";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
||||
{ name: "description", content: "中国烟草AI合同及卷宗审核系统首页" },
|
||||
];
|
||||
};
|
||||
|
||||
// 处理登出请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "logout") {
|
||||
return logout(request);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证用户登录状态
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { isAuthenticated, userRole } = await getUserSession(request);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return redirect("/login");
|
||||
}
|
||||
return Response.json({ userRole });
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const navigate = useNavigate();
|
||||
const { userRole } = useLoaderData<typeof loader>();
|
||||
const [currentDateTime, setCurrentDateTime] = useState({
|
||||
date: '',
|
||||
time: ''
|
||||
});
|
||||
|
||||
// 打印服务器端传递的用户角色
|
||||
useEffect(() => {
|
||||
console.log('_index 服务器返回的用户角色:', userRole);
|
||||
}, [userRole]);
|
||||
|
||||
// 更新日期时间
|
||||
useEffect(() => {
|
||||
const updateDateTime = () => {
|
||||
const now = dayjs();
|
||||
// 格式化日期: YYYY/MM/DD
|
||||
setCurrentDateTime({
|
||||
date: now.format('YYYY/MM/DD'),
|
||||
time: now.format('HH:mm:ss')
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化时间
|
||||
updateDateTime();
|
||||
|
||||
// 每秒更新一次
|
||||
const timerID = setInterval(updateDateTime, 1000);
|
||||
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
|
||||
// 处理模块点击
|
||||
const handleModuleClick = (path: string, reviewType: string) => {
|
||||
// 将reviewType存入sessionStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('reviewType', reviewType);
|
||||
}
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (path: string, reviewType: string, e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleModuleClick(path, reviewType);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = () => {
|
||||
// 清除sessionStorage中的所有数据
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
// 使用Form组件提交登出请求
|
||||
const form = document.getElementById('logout-form') as HTMLFormElement;
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
// 如果找不到表单,直接导航到登录页
|
||||
navigate('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="home-page">
|
||||
{/* 登出表单 - 隐藏 */}
|
||||
<Form method="post" id="logout-form" className="hidden">
|
||||
<input type="hidden" name="intent" value="logout" />
|
||||
</Form>
|
||||
|
||||
{/* 头部 */}
|
||||
<header className="header">
|
||||
<div className="logo-container">
|
||||
<img src="/logo.svg" alt="中国烟草" className="logo" />
|
||||
<div className="flex flex-col">
|
||||
<span className="logo-text ">中国烟草</span>
|
||||
<span className="logo-text-en">CHINA TOBACCO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-info">
|
||||
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
|
||||
<div className="user">
|
||||
<img src="/avatar.png" alt="用户头像" className="avatar" />
|
||||
<span className="username">{userRole === 'developer' ? '系统管理员' : '普通用户'}</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="logout-button"
|
||||
aria-label="登出"
|
||||
>
|
||||
<i className="ri-logout-box-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主要内容 */}
|
||||
<main className="index-main-content">
|
||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||
|
||||
<div className="modules-container">
|
||||
{/* 合同管理模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
||||
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="合同管理"
|
||||
>
|
||||
<i className="ri-file-list-2-fill text-[3rem] text-[#269b6c]"></i>
|
||||
<span className="module-name">合同管理</span>
|
||||
</div>
|
||||
|
||||
{/* 案卷智能评查模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/home', 'record')}
|
||||
onKeyDown={(e) => handleKeyDown('/home', 'record', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="案卷智能评查"
|
||||
>
|
||||
<i className="ri-folder-shared-fill text-[3rem] text-[#269b6c]"></i>
|
||||
<span className="module-name">案卷智能评查</span>
|
||||
</div>
|
||||
|
||||
{/* 智慧法务大模型模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/chat-with-llm', 'model')}
|
||||
onKeyDown={(e) => handleKeyDown('/chat-with-llm', 'model', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="智慧法务大模型"
|
||||
>
|
||||
<i className="ri-robot-2-fill text-[3rem] text-[#269b6c]"></i>
|
||||
<span className="module-name">智慧法务大模型</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/* 底部山水背景 */}
|
||||
<footer className="footer">
|
||||
<div className="mountains-bg"></div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
);
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, Form, useLoaderData } from '@remix-run/react';
|
||||
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import styles from "~/styles/pages/home.css?url";
|
||||
import dayjs from 'dayjs';
|
||||
import { getUserSession, logout } from "~/root";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
||||
{ name: "description", content: "中国烟草AI合同及卷宗审核系统首页" },
|
||||
];
|
||||
};
|
||||
|
||||
// 处理登出请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "logout") {
|
||||
return logout(request);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证用户登录状态
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { isAuthenticated, userRole } = await getUserSession(request);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return redirect("/login");
|
||||
}
|
||||
return Response.json({ userRole });
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const navigate = useNavigate();
|
||||
const { userRole } = useLoaderData<typeof loader>();
|
||||
const [currentDateTime, setCurrentDateTime] = useState({
|
||||
date: '',
|
||||
time: ''
|
||||
});
|
||||
|
||||
// 打印服务器端传递的用户角色
|
||||
useEffect(() => {
|
||||
console.log('_index 服务器返回的用户角色:', userRole);
|
||||
}, [userRole]);
|
||||
|
||||
// 更新日期时间
|
||||
useEffect(() => {
|
||||
const updateDateTime = () => {
|
||||
const now = dayjs();
|
||||
// 格式化日期: YYYY/MM/DD
|
||||
setCurrentDateTime({
|
||||
date: now.format('YYYY/MM/DD'),
|
||||
time: now.format('HH:mm:ss')
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化时间
|
||||
updateDateTime();
|
||||
|
||||
// 每秒更新一次
|
||||
const timerID = setInterval(updateDateTime, 1000);
|
||||
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
|
||||
// 处理模块点击
|
||||
const handleModuleClick = (path: string, reviewType: string) => {
|
||||
// 将reviewType存入sessionStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('reviewType', reviewType);
|
||||
}
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (path: string, reviewType: string, e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleModuleClick(path, reviewType);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = () => {
|
||||
// 清除sessionStorage中的所有数据
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
// 使用Form组件提交登出请求
|
||||
const form = document.getElementById('logout-form') as HTMLFormElement;
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
// 如果找不到表单,直接导航到登录页
|
||||
navigate('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="home-page">
|
||||
{/* 登出表单 - 隐藏 */}
|
||||
<Form method="post" id="logout-form" className="hidden">
|
||||
<input type="hidden" name="intent" value="logout" />
|
||||
</Form>
|
||||
|
||||
{/* 头部 */}
|
||||
<header className="header">
|
||||
<div className="logo-container">
|
||||
<img src="/logo.svg" alt="中国烟草" className="logo" />
|
||||
<div className="flex flex-col">
|
||||
<span className="logo-text ">中国烟草</span>
|
||||
<span className="logo-text-en">CHINA TOBACCO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-info">
|
||||
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
|
||||
<div className="user">
|
||||
<img src="/avatar.png" alt="用户头像" className="avatar" />
|
||||
<span className="username">{userRole === 'developer' ? '系统管理员' : '普通用户'}</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="logout-button"
|
||||
aria-label="登出"
|
||||
>
|
||||
<i className="ri-logout-box-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主要内容 */}
|
||||
<main className="index-main-content">
|
||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||
|
||||
<div className="modules-container">
|
||||
{/* 合同管理模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
||||
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="合同管理"
|
||||
>
|
||||
<i className="ri-file-list-2-fill text-[3rem] text-[#269b6c]"></i>
|
||||
<span className="module-name">合同管理</span>
|
||||
</div>
|
||||
|
||||
{/* 案卷智能评查模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/home', 'record')}
|
||||
onKeyDown={(e) => handleKeyDown('/home', 'record', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="案卷智能评查"
|
||||
>
|
||||
<i className="ri-folder-shared-fill text-[3rem] text-[#269b6c]"></i>
|
||||
<span className="module-name">案卷智能评查</span>
|
||||
</div>
|
||||
|
||||
{/* 智慧法务大模型模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/chat-with-llm', 'model')}
|
||||
onKeyDown={(e) => handleKeyDown('/chat-with-llm', 'model', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="智慧法务大模型"
|
||||
>
|
||||
<i className="ri-robot-2-fill text-[3rem] text-[#269b6c]"></i>
|
||||
<span className="module-name">智慧法务大模型</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/* 底部山水背景 */}
|
||||
<footer className="footer">
|
||||
<div className="mountains-bg"></div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
/* 聊天输入区域 */
|
||||
.chat-input-container {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 16px 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-input-container {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-input-container {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
/* 聊天输入区域 */
|
||||
.chat-input-container {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 16px 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-input-container {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-input-container {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +1,132 @@
|
||||
/* 消息项样式 */
|
||||
.chat-message {
|
||||
margin-bottom: 20px;
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 消息卡片 */
|
||||
.message-card {
|
||||
max-width: 85%;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-card.user {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message-card.assistant {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* 流式文本效果 */
|
||||
.streaming-text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.streaming-text::after {
|
||||
content: '';
|
||||
/* 移除光标 */
|
||||
animation: blink 1s infinite;
|
||||
color: #1890ff;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 消息动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应指示器 */
|
||||
.responding-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 反馈按钮容器 */
|
||||
.feedback-buttons {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 建议问题 */
|
||||
.suggested-questions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.question-button {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.question-button:hover {
|
||||
border-color: #1890ff;
|
||||
background: #f0f9ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 消息时间戳 */
|
||||
.message-timestamp {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 消息图片 */
|
||||
.message-images {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.message-images .ant-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.message-card {
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.message-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
/* 消息项样式 */
|
||||
.chat-message {
|
||||
margin-bottom: 20px;
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 消息卡片 */
|
||||
.message-card {
|
||||
max-width: 85%;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-card.user {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message-card.assistant {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* 流式文本效果 */
|
||||
.streaming-text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.streaming-text::after {
|
||||
content: '';
|
||||
/* 移除光标 */
|
||||
animation: blink 1s infinite;
|
||||
color: #1890ff;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 消息动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应指示器 */
|
||||
.responding-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 反馈按钮容器 */
|
||||
.feedback-buttons {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 建议问题 */
|
||||
.suggested-questions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.question-button {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.question-button:hover {
|
||||
border-color: #1890ff;
|
||||
background: #f0f9ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 消息时间戳 */
|
||||
.message-timestamp {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 消息图片 */
|
||||
.message-images {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.message-images .ant-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.message-card {
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.message-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,246 +1,246 @@
|
||||
/* 聊天布局样式 */
|
||||
|
||||
/* 聊天容器 - 自适应布局 */
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* 聊天头部 */
|
||||
.chat-header {
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 聊天消息列表容器 */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
background: #f9fafb;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* 新对话欢迎界面 */
|
||||
.chat-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.chat-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-loading .ant-spin {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.chat-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-error h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-error p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 确保聊天容器在主内容区域中占满全部空间 */
|
||||
.main-content .chat-container {
|
||||
height: calc(89vh - 0px);
|
||||
/* 减去任何顶部导航栏的高度 */
|
||||
}
|
||||
|
||||
/* 如果有面包屑导航,需要调整高度 */
|
||||
.main-content .breadcrumb+.chat-container {
|
||||
height: calc(100vh - 60px);
|
||||
/* 减去面包屑的高度 */
|
||||
}
|
||||
|
||||
/* 侧边栏滚动区域样式 */
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* Firefox 滚动条样式 */
|
||||
.h-full.overflow-y-auto {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d1d5db #f8f9fa;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-messages {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-welcome p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局按钮主题色统一 */
|
||||
.ant-btn-primary {
|
||||
background-color: rgb(0, 104, 74) !important;
|
||||
border-color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:hover {
|
||||
background-color: rgba(0, 104, 74, 0.8) !important;
|
||||
border-color: rgba(0, 104, 74, 0.8) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:focus {
|
||||
background-color: rgb(0, 104, 74) !important;
|
||||
border-color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:active {
|
||||
background-color: rgba(0, 104, 74, 0.9) !important;
|
||||
border-color: rgba(0, 104, 74, 0.9) !important;
|
||||
}
|
||||
|
||||
/* 禁用状态保持原样 */
|
||||
.ant-btn-primary:disabled {
|
||||
background-color: rgba(0, 0, 0, 0.04) !important;
|
||||
border-color: #d9d9d9 !important;
|
||||
color: rgba(0, 0, 0, 0.25) !important;
|
||||
}
|
||||
|
||||
/* 链接按钮主题色 */
|
||||
.ant-btn-link {
|
||||
color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:hover {
|
||||
color: rgba(0, 104, 74, 0.8) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:focus {
|
||||
color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:active {
|
||||
color: rgba(0, 104, 74, 0.9) !important;
|
||||
/* 聊天布局样式 */
|
||||
|
||||
/* 聊天容器 - 自适应布局 */
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* 聊天头部 */
|
||||
.chat-header {
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 聊天消息列表容器 */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
background: #f9fafb;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* 新对话欢迎界面 */
|
||||
.chat-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.chat-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-loading .ant-spin {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.chat-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-error h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-error p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 确保聊天容器在主内容区域中占满全部空间 */
|
||||
.main-content .chat-container {
|
||||
height: calc(89vh - 0px);
|
||||
/* 减去任何顶部导航栏的高度 */
|
||||
}
|
||||
|
||||
/* 如果有面包屑导航,需要调整高度 */
|
||||
.main-content .breadcrumb+.chat-container {
|
||||
height: calc(100vh - 60px);
|
||||
/* 减去面包屑的高度 */
|
||||
}
|
||||
|
||||
/* 侧边栏滚动区域样式 */
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* Firefox 滚动条样式 */
|
||||
.h-full.overflow-y-auto {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d1d5db #f8f9fa;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-messages {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-welcome p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局按钮主题色统一 */
|
||||
.ant-btn-primary {
|
||||
background-color: rgb(0, 104, 74) !important;
|
||||
border-color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:hover {
|
||||
background-color: rgba(0, 104, 74, 0.8) !important;
|
||||
border-color: rgba(0, 104, 74, 0.8) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:focus {
|
||||
background-color: rgb(0, 104, 74) !important;
|
||||
border-color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:active {
|
||||
background-color: rgba(0, 104, 74, 0.9) !important;
|
||||
border-color: rgba(0, 104, 74, 0.9) !important;
|
||||
}
|
||||
|
||||
/* 禁用状态保持原样 */
|
||||
.ant-btn-primary:disabled {
|
||||
background-color: rgba(0, 0, 0, 0.04) !important;
|
||||
border-color: #d9d9d9 !important;
|
||||
color: rgba(0, 0, 0, 0.25) !important;
|
||||
}
|
||||
|
||||
/* 链接按钮主题色 */
|
||||
.ant-btn-link {
|
||||
color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:hover {
|
||||
color: rgba(0, 104, 74, 0.8) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:focus {
|
||||
color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:active {
|
||||
color: rgba(0, 104, 74, 0.9) !important;
|
||||
}
|
||||
@@ -1,136 +1,136 @@
|
||||
/* Markdown 样式 */
|
||||
.markdown-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
.markdown-content p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
.markdown-content code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 3px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-content table th,
|
||||
.markdown-content table td {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-content table th {
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-content table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
.markdown-content a {
|
||||
color: #0969da;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
.markdown-content blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
}
|
||||
|
||||
/* 水平线样式 */
|
||||
.markdown-content hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e1e4e8;
|
||||
border: 0;
|
||||
/* Markdown 样式 */
|
||||
.markdown-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
.markdown-content p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
.markdown-content code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 3px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-content table th,
|
||||
.markdown-content table td {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-content table th {
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-content table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
.markdown-content a {
|
||||
color: #0969da;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
.markdown-content blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
}
|
||||
|
||||
/* 水平线样式 */
|
||||
.markdown-content hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e1e4e8;
|
||||
border: 0;
|
||||
}
|
||||
@@ -1,74 +1,74 @@
|
||||
/* 聊天侧边栏样式 */
|
||||
.chat-sidebar-menu .ant-menu-item {
|
||||
margin: 4px 0;
|
||||
border-radius: 6px;
|
||||
height: auto;
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu .ant-menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu .ant-menu-item-selected {
|
||||
background-color: rgba(0, 104, 74, 0.1);
|
||||
border-color: rgb(0, 104, 74);
|
||||
}
|
||||
|
||||
.chat-sidebar-menu .ant-menu-item-selected::after {
|
||||
border-right: 3px solid rgb(0, 104, 74);
|
||||
}
|
||||
|
||||
/* 会话项样式 */
|
||||
.chat-sidebar-menu .ant-menu-item .ant-menu-title-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-sider {
|
||||
position: fixed !important;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ant-layout-sider.ant-layout-sider-collapsed {
|
||||
left: -200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.chat-sidebar-menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 确保侧边栏布局正确 */
|
||||
.ant-layout-sider {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* 侧边栏内容区域样式 */
|
||||
.ant-layout-sider .ant-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
/* 聊天侧边栏样式 */
|
||||
.chat-sidebar-menu .ant-menu-item {
|
||||
margin: 4px 0;
|
||||
border-radius: 6px;
|
||||
height: auto;
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu .ant-menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu .ant-menu-item-selected {
|
||||
background-color: rgba(0, 104, 74, 0.1);
|
||||
border-color: rgb(0, 104, 74);
|
||||
}
|
||||
|
||||
.chat-sidebar-menu .ant-menu-item-selected::after {
|
||||
border-right: 3px solid rgb(0, 104, 74);
|
||||
}
|
||||
|
||||
/* 会话项样式 */
|
||||
.chat-sidebar-menu .ant-menu-item .ant-menu-title-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-sider {
|
||||
position: fixed !important;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ant-layout-sider.ant-layout-sider-collapsed {
|
||||
left: -200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.chat-sidebar-menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chat-sidebar-menu::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 确保侧边栏布局正确 */
|
||||
.ant-layout-sider {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* 侧边栏内容区域样式 */
|
||||
.ant-layout-sider .ant-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,57 +1,57 @@
|
||||
/* 思考过程样式 */
|
||||
.thought-process-card {
|
||||
margin-bottom: 12px;
|
||||
border-left: 4px solid #1890ff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.thought-process-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.thought-process-tool-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.thought-process-content {
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.thought-process-collapsed {
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thought-process-collapsed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: linear-gradient(to bottom, rgba(240, 248, 255, 0), rgba(240, 248, 255, 1));
|
||||
}
|
||||
|
||||
.thought-process-observation {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: #f0fff0;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.thought-process-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
color: #1890ff;
|
||||
/* 思考过程样式 */
|
||||
.thought-process-card {
|
||||
margin-bottom: 12px;
|
||||
border-left: 4px solid #1890ff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.thought-process-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.thought-process-tool-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.thought-process-content {
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.thought-process-collapsed {
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thought-process-collapsed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: linear-gradient(to bottom, rgba(240, 248, 255, 0), rgba(240, 248, 255, 1));
|
||||
}
|
||||
|
||||
.thought-process-observation {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: #f0fff0;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.thought-process-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
+263
-263
@@ -1,264 +1,264 @@
|
||||
// 应用信息类型
|
||||
export interface AppInfo {
|
||||
title: string;
|
||||
description: string;
|
||||
copyright: string;
|
||||
privacy_policy: string;
|
||||
default_language: string;
|
||||
}
|
||||
|
||||
// 会话项类型
|
||||
export interface ConversationItem {
|
||||
id: string;
|
||||
name: string;
|
||||
inputs?: Record<string, any>;
|
||||
introduction?: string;
|
||||
}
|
||||
|
||||
// 聊天消息类型
|
||||
export interface ChatItem {
|
||||
id: string;
|
||||
content: string;
|
||||
isAnswer: boolean;
|
||||
feedback?: Feedbacktype;
|
||||
agent_thoughts?: ThoughtItem[];
|
||||
message_files?: VisionFile[];
|
||||
isError?: boolean;
|
||||
workflow_run_id?: string;
|
||||
workflowProcess?: WorkflowProcess;
|
||||
more?: MessageMore;
|
||||
useCurrentUserAvatar?: boolean;
|
||||
isOpeningStatement?: boolean;
|
||||
suggestedQuestions?: string[];
|
||||
}
|
||||
|
||||
// 消息更多信息类型
|
||||
export interface MessageMore {
|
||||
time: string;
|
||||
tokens: number;
|
||||
latency: number | string;
|
||||
}
|
||||
|
||||
// 反馈类型
|
||||
export type Feedbacktype = {
|
||||
rating: 'like' | 'dislike' | null;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// 思考过程类型
|
||||
export interface ThoughtItem {
|
||||
id?: string;
|
||||
chain_id?: string;
|
||||
thought?: string;
|
||||
observation?: string;
|
||||
message_files?: VisionFile[];
|
||||
tool_name?: string;
|
||||
tool_input?: string;
|
||||
tool_output?: string;
|
||||
tool_finished?: boolean;
|
||||
parent_id?: string;
|
||||
children_ids?: string[];
|
||||
sort?: number;
|
||||
message_id?: string;
|
||||
tool?: string;
|
||||
position?: number;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
// 文本类型表单项
|
||||
export interface TextTypeFormItem {
|
||||
label: string;
|
||||
variable: string;
|
||||
required: boolean;
|
||||
max_length: number;
|
||||
}
|
||||
|
||||
// 选择类型表单项
|
||||
export interface SelectTypeFormItem {
|
||||
label: string;
|
||||
variable: string;
|
||||
required: boolean;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
// 用户输入表单项
|
||||
export type UserInputFormItem = {
|
||||
'text-input': TextTypeFormItem;
|
||||
} | {
|
||||
'select': SelectTypeFormItem;
|
||||
} | {
|
||||
'paragraph': TextTypeFormItem;
|
||||
}
|
||||
|
||||
// 提示配置类型
|
||||
export interface PromptConfig {
|
||||
prompt_template: string;
|
||||
prompt_variables: PromptVariable[];
|
||||
}
|
||||
|
||||
// 提示变量类型
|
||||
export interface PromptVariable {
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
max_length?: number;
|
||||
allowed_file_extensions?: string[];
|
||||
allowed_file_types?: string[];
|
||||
allowed_file_upload_methods?: TransferMethod[];
|
||||
}
|
||||
|
||||
// 视觉文件类型
|
||||
export interface VisionFile {
|
||||
id?: string;
|
||||
type: string;
|
||||
transfer_method: TransferMethod;
|
||||
url?: string;
|
||||
upload_file_id?: string;
|
||||
belongs_to?: string;
|
||||
usage?: string;
|
||||
result?: any;
|
||||
detail?: Resolution;
|
||||
}
|
||||
|
||||
// 图片文件类型
|
||||
export interface ImageFile {
|
||||
type: TransferMethod;
|
||||
_id: string;
|
||||
fileId: string;
|
||||
file?: File;
|
||||
progress: number;
|
||||
url: string;
|
||||
base64Url?: string;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
// 视觉设置类型
|
||||
export interface VisionSettings {
|
||||
enabled: boolean;
|
||||
detail?: Resolution;
|
||||
number_limits?: number;
|
||||
transfer_methods?: TransferMethod[];
|
||||
image_file_size_limit?: number | string;
|
||||
}
|
||||
|
||||
// 分辨率枚举
|
||||
export enum Resolution {
|
||||
low = 'low',
|
||||
high = 'high',
|
||||
}
|
||||
|
||||
// 传输方法枚举
|
||||
export enum TransferMethod {
|
||||
local_file = 'local_file',
|
||||
remote_url = 'remote_url',
|
||||
all = 'all',
|
||||
}
|
||||
|
||||
// 工作流运行状态枚举
|
||||
export enum WorkflowRunningStatus {
|
||||
init = 'init',
|
||||
running = 'running',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
waiting = 'waiting',
|
||||
succeeded = 'succeeded',
|
||||
failed = 'failed',
|
||||
stopped = 'stopped',
|
||||
}
|
||||
|
||||
// 节点运行状态枚举
|
||||
export enum NodeRunningStatus {
|
||||
NotStart = 'not-start',
|
||||
Waiting = 'waiting',
|
||||
Running = 'running',
|
||||
Succeeded = 'succeeded',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
// 块类型枚举
|
||||
export enum BlockEnum {
|
||||
Start = 'start',
|
||||
End = 'end',
|
||||
Answer = 'answer',
|
||||
LLM = 'llm',
|
||||
KnowledgeRetrieval = 'knowledge-retrieval',
|
||||
QuestionClassifier = 'question-classifier',
|
||||
IfElse = 'if-else',
|
||||
Code = 'code',
|
||||
TemplateTransform = 'template-transform',
|
||||
HttpRequest = 'http-request',
|
||||
VariableAssigner = 'variable-assigner',
|
||||
Tool = 'tool',
|
||||
}
|
||||
|
||||
// 节点追踪类型
|
||||
export interface NodeTracing {
|
||||
id: string;
|
||||
index: number;
|
||||
predecessor_node_id: string;
|
||||
node_id: string;
|
||||
node_type: BlockEnum;
|
||||
title: string;
|
||||
inputs: any;
|
||||
process_data: any;
|
||||
outputs?: any;
|
||||
status: string;
|
||||
error?: string;
|
||||
elapsed_time: number;
|
||||
execution_metadata: {
|
||||
total_tokens: number;
|
||||
total_price: number;
|
||||
currency: string;
|
||||
};
|
||||
created_at: number;
|
||||
created_by: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
finished_at: number;
|
||||
extras?: any;
|
||||
expand?: boolean; // for UI
|
||||
}
|
||||
|
||||
// 工作流进程类型
|
||||
export interface WorkflowProcess {
|
||||
status: WorkflowRunningStatus;
|
||||
tracing: NodeTracing[];
|
||||
expand?: boolean; // for UI
|
||||
}
|
||||
|
||||
// 代码语言枚举
|
||||
export enum CodeLanguage {
|
||||
python3 = 'python3',
|
||||
javascript = 'javascript',
|
||||
json = 'json',
|
||||
}
|
||||
|
||||
// 消息事件类型
|
||||
export interface MessageEvent {
|
||||
event: string;
|
||||
task_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// 消息替换类型
|
||||
export interface MessageReplace {
|
||||
event: string;
|
||||
task_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// 消息结束类型
|
||||
export interface MessageEnd {
|
||||
event: string;
|
||||
task_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
// 应用信息类型
|
||||
export interface AppInfo {
|
||||
title: string;
|
||||
description: string;
|
||||
copyright: string;
|
||||
privacy_policy: string;
|
||||
default_language: string;
|
||||
}
|
||||
|
||||
// 会话项类型
|
||||
export interface ConversationItem {
|
||||
id: string;
|
||||
name: string;
|
||||
inputs?: Record<string, any>;
|
||||
introduction?: string;
|
||||
}
|
||||
|
||||
// 聊天消息类型
|
||||
export interface ChatItem {
|
||||
id: string;
|
||||
content: string;
|
||||
isAnswer: boolean;
|
||||
feedback?: Feedbacktype;
|
||||
agent_thoughts?: ThoughtItem[];
|
||||
message_files?: VisionFile[];
|
||||
isError?: boolean;
|
||||
workflow_run_id?: string;
|
||||
workflowProcess?: WorkflowProcess;
|
||||
more?: MessageMore;
|
||||
useCurrentUserAvatar?: boolean;
|
||||
isOpeningStatement?: boolean;
|
||||
suggestedQuestions?: string[];
|
||||
}
|
||||
|
||||
// 消息更多信息类型
|
||||
export interface MessageMore {
|
||||
time: string;
|
||||
tokens: number;
|
||||
latency: number | string;
|
||||
}
|
||||
|
||||
// 反馈类型
|
||||
export type Feedbacktype = {
|
||||
rating: 'like' | 'dislike' | null;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// 思考过程类型
|
||||
export interface ThoughtItem {
|
||||
id?: string;
|
||||
chain_id?: string;
|
||||
thought?: string;
|
||||
observation?: string;
|
||||
message_files?: VisionFile[];
|
||||
tool_name?: string;
|
||||
tool_input?: string;
|
||||
tool_output?: string;
|
||||
tool_finished?: boolean;
|
||||
parent_id?: string;
|
||||
children_ids?: string[];
|
||||
sort?: number;
|
||||
message_id?: string;
|
||||
tool?: string;
|
||||
position?: number;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
// 文本类型表单项
|
||||
export interface TextTypeFormItem {
|
||||
label: string;
|
||||
variable: string;
|
||||
required: boolean;
|
||||
max_length: number;
|
||||
}
|
||||
|
||||
// 选择类型表单项
|
||||
export interface SelectTypeFormItem {
|
||||
label: string;
|
||||
variable: string;
|
||||
required: boolean;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
// 用户输入表单项
|
||||
export type UserInputFormItem = {
|
||||
'text-input': TextTypeFormItem;
|
||||
} | {
|
||||
'select': SelectTypeFormItem;
|
||||
} | {
|
||||
'paragraph': TextTypeFormItem;
|
||||
}
|
||||
|
||||
// 提示配置类型
|
||||
export interface PromptConfig {
|
||||
prompt_template: string;
|
||||
prompt_variables: PromptVariable[];
|
||||
}
|
||||
|
||||
// 提示变量类型
|
||||
export interface PromptVariable {
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
max_length?: number;
|
||||
allowed_file_extensions?: string[];
|
||||
allowed_file_types?: string[];
|
||||
allowed_file_upload_methods?: TransferMethod[];
|
||||
}
|
||||
|
||||
// 视觉文件类型
|
||||
export interface VisionFile {
|
||||
id?: string;
|
||||
type: string;
|
||||
transfer_method: TransferMethod;
|
||||
url?: string;
|
||||
upload_file_id?: string;
|
||||
belongs_to?: string;
|
||||
usage?: string;
|
||||
result?: any;
|
||||
detail?: Resolution;
|
||||
}
|
||||
|
||||
// 图片文件类型
|
||||
export interface ImageFile {
|
||||
type: TransferMethod;
|
||||
_id: string;
|
||||
fileId: string;
|
||||
file?: File;
|
||||
progress: number;
|
||||
url: string;
|
||||
base64Url?: string;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
// 视觉设置类型
|
||||
export interface VisionSettings {
|
||||
enabled: boolean;
|
||||
detail?: Resolution;
|
||||
number_limits?: number;
|
||||
transfer_methods?: TransferMethod[];
|
||||
image_file_size_limit?: number | string;
|
||||
}
|
||||
|
||||
// 分辨率枚举
|
||||
export enum Resolution {
|
||||
low = 'low',
|
||||
high = 'high',
|
||||
}
|
||||
|
||||
// 传输方法枚举
|
||||
export enum TransferMethod {
|
||||
local_file = 'local_file',
|
||||
remote_url = 'remote_url',
|
||||
all = 'all',
|
||||
}
|
||||
|
||||
// 工作流运行状态枚举
|
||||
export enum WorkflowRunningStatus {
|
||||
init = 'init',
|
||||
running = 'running',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
waiting = 'waiting',
|
||||
succeeded = 'succeeded',
|
||||
failed = 'failed',
|
||||
stopped = 'stopped',
|
||||
}
|
||||
|
||||
// 节点运行状态枚举
|
||||
export enum NodeRunningStatus {
|
||||
NotStart = 'not-start',
|
||||
Waiting = 'waiting',
|
||||
Running = 'running',
|
||||
Succeeded = 'succeeded',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
// 块类型枚举
|
||||
export enum BlockEnum {
|
||||
Start = 'start',
|
||||
End = 'end',
|
||||
Answer = 'answer',
|
||||
LLM = 'llm',
|
||||
KnowledgeRetrieval = 'knowledge-retrieval',
|
||||
QuestionClassifier = 'question-classifier',
|
||||
IfElse = 'if-else',
|
||||
Code = 'code',
|
||||
TemplateTransform = 'template-transform',
|
||||
HttpRequest = 'http-request',
|
||||
VariableAssigner = 'variable-assigner',
|
||||
Tool = 'tool',
|
||||
}
|
||||
|
||||
// 节点追踪类型
|
||||
export interface NodeTracing {
|
||||
id: string;
|
||||
index: number;
|
||||
predecessor_node_id: string;
|
||||
node_id: string;
|
||||
node_type: BlockEnum;
|
||||
title: string;
|
||||
inputs: any;
|
||||
process_data: any;
|
||||
outputs?: any;
|
||||
status: string;
|
||||
error?: string;
|
||||
elapsed_time: number;
|
||||
execution_metadata: {
|
||||
total_tokens: number;
|
||||
total_price: number;
|
||||
currency: string;
|
||||
};
|
||||
created_at: number;
|
||||
created_by: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
finished_at: number;
|
||||
extras?: any;
|
||||
expand?: boolean; // for UI
|
||||
}
|
||||
|
||||
// 工作流进程类型
|
||||
export interface WorkflowProcess {
|
||||
status: WorkflowRunningStatus;
|
||||
tracing: NodeTracing[];
|
||||
expand?: boolean; // for UI
|
||||
}
|
||||
|
||||
// 代码语言枚举
|
||||
export enum CodeLanguage {
|
||||
python3 = 'python3',
|
||||
javascript = 'javascript',
|
||||
json = 'json',
|
||||
}
|
||||
|
||||
// 消息事件类型
|
||||
export interface MessageEvent {
|
||||
event: string;
|
||||
task_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// 消息替换类型
|
||||
export interface MessageReplace {
|
||||
event: string;
|
||||
task_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// 消息结束类型
|
||||
export interface MessageEnd {
|
||||
event: string;
|
||||
task_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
}
|
||||
Generated
+15810
-15810
File diff suppressed because it is too large
Load Diff
+68
-68
@@ -1,68 +1,68 @@
|
||||
{
|
||||
"name": "remix_docreview",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "remix vite:build",
|
||||
"dev": "remix vite:dev",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "remix-serve ./build/server/index.js",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@react-pdf-viewer/core": "^3.12.0",
|
||||
"@react-pdf-viewer/highlight": "^3.12.0",
|
||||
"@react-pdf-viewer/search": "^3.12.0",
|
||||
"@remix-run/node": "^2.16.2",
|
||||
"@remix-run/react": "^2.16.2",
|
||||
"@remix-run/serve": "^2.16.2",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"ahooks": "^3.8.5",
|
||||
"antd": "^5.25.4",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"diff": "^7.0.0",
|
||||
"docx-preview": "^0.3.5",
|
||||
"html-docx-js": "^0.3.1",
|
||||
"immer": "^10.1.1",
|
||||
"isbot": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"mammoth": "^1.9.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"pg": "^8.14.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-pdf": "^5.7.2",
|
||||
"remixicon": "^4.6.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.16.2",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-pdf": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.1",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^6.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "remix_docreview",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "remix vite:build",
|
||||
"dev": "remix vite:dev",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "remix-serve ./build/server/index.js",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@react-pdf-viewer/core": "^3.12.0",
|
||||
"@react-pdf-viewer/highlight": "^3.12.0",
|
||||
"@react-pdf-viewer/search": "^3.12.0",
|
||||
"@remix-run/node": "^2.16.2",
|
||||
"@remix-run/react": "^2.16.2",
|
||||
"@remix-run/serve": "^2.16.2",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"ahooks": "^3.8.5",
|
||||
"antd": "^5.25.4",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"diff": "^7.0.0",
|
||||
"docx-preview": "^0.3.5",
|
||||
"html-docx-js": "^0.3.1",
|
||||
"immer": "^10.1.1",
|
||||
"isbot": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"mammoth": "^1.9.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"pg": "^8.14.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-pdf": "^5.7.2",
|
||||
"remixicon": "^4.6.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.16.2",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-pdf": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.1",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^6.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+47
-47
@@ -1,47 +1,47 @@
|
||||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
declare module "@remix-run/node" {
|
||||
interface Future {
|
||||
v3_singleFetch: true;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
v3_singleFetch: true,
|
||||
v3_lazyRouteDiscovery: true,
|
||||
},
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
define: {
|
||||
// 在构建时为客户端代码提供 process.env.NODE_ENV 变量
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
open: true,
|
||||
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表1
|
||||
cors: true,
|
||||
// HMR配置
|
||||
hmr: {
|
||||
// 控制HMR更新时行为
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
// 优化依赖预构建配置
|
||||
optimizeDeps: {
|
||||
// 防止依赖预构建时触发页面刷新导致路由中断
|
||||
force: false,
|
||||
// 预构建这些依赖,避免首次加载时出现重新构建
|
||||
include: ['react-pdf', 'pdfjs-dist', 'dayjs', '@remix-run/node', 'react-dom', 'axios', 'dayjs/plugin/utc', 'react-router-dom', 'jszip', 'ahooks', 'antd', 'immer', '@ant-design/icons'],
|
||||
},
|
||||
});
|
||||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
declare module "@remix-run/node" {
|
||||
interface Future {
|
||||
v3_singleFetch: true;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
v3_singleFetch: true,
|
||||
v3_lazyRouteDiscovery: true,
|
||||
},
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
define: {
|
||||
// 在构建时为客户端代码提供 process.env.NODE_ENV 变量
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
open: true,
|
||||
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表1
|
||||
cors: true,
|
||||
// HMR配置
|
||||
hmr: {
|
||||
// 控制HMR更新时行为
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
// 优化依赖预构建配置
|
||||
optimizeDeps: {
|
||||
// 防止依赖预构建时触发页面刷新导致路由中断
|
||||
force: false,
|
||||
// 预构建这些依赖,避免首次加载时出现重新构建
|
||||
include: ['react-pdf', 'pdfjs-dist', 'dayjs', '@remix-run/node', 'react-dom', 'axios', 'dayjs/plugin/utc', 'react-router-dom', 'jszip', 'ahooks', 'antd', 'immer', '@ant-design/icons'],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user