Dify接入初版

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