feat: 重构主布局并优化动态表单功能
重构主布局组件,将侧边栏和 Copilot 提供者逻辑提取到 MainLayout 组件 优化动态表单,支持编辑模式并完善错误处理 移除测试提示信息并更新环境变量配置
This commit is contained in:
@@ -5,8 +5,9 @@
|
|||||||
- **核心理念**:通过 AI 驱动的消息流实现动态 UI 生成和业务处理。
|
- **核心理念**:通过 AI 驱动的消息流实现动态 UI 生成和业务处理。
|
||||||
- **技术栈**:Next.js 15 (App Router) + Tailwind CSS + Lucide Icons。
|
- **技术栈**:Next.js 15 (App Router) + Tailwind CSS + Lucide Icons。
|
||||||
- **关键组件**:
|
- **关键组件**:
|
||||||
- `UniversalModuleRenderer.tsx`: 后端 AI 下发 UI 指令的监听与执行枢纽,负责实时绘制界面。
|
- `UniversalModuleRenderer.tsx`: 后端 AI 下发 UI 指令的监听与执行枢纽。
|
||||||
- `DynamicForm.tsx`: 基于 JSON Schema 的动态表单渲染器,支持实体关联选择 (`x-link-entity`)。
|
- `CopilotProvider.tsx`: **已更新**,取消本地 AI 运行时,改为直连后端 `/api/copilotkit` 端口。
|
||||||
|
- `DynamicForm.tsx`: 基于 JSON Schema 的动态表单渲染器。
|
||||||
- `EntityDataPage`: 动态列表展示页面,通过 AI 代理执行编辑和删除操作。
|
- `EntityDataPage`: 动态列表展示页面,通过 AI 代理执行编辑和删除操作。
|
||||||
|
|
||||||
## 💼 业务逻辑 (Business Logic)
|
## 💼 业务逻辑 (Business Logic)
|
||||||
@@ -16,17 +17,16 @@
|
|||||||
## 📈 当前开发进度 (Current Progress)
|
## 📈 当前开发进度 (Current Progress)
|
||||||
- [x] **AI 动态 UI 渲染**:`UniversalModuleRenderer` 基础架构就绪。
|
- [x] **AI 动态 UI 渲染**:`UniversalModuleRenderer` 基础架构就绪。
|
||||||
- [x] **动态表单与关联**:支持枚举、模式匹配及实体级联选择。
|
- [x] **动态表单与关联**:支持枚举、模式匹配及实体级联选择。
|
||||||
- [x] **会话隔离适配**:前端已实现 Header 注入逻辑。
|
- [x] **安全优化**:移除了前端 API Key,所有 AI 请求通过后端转发。
|
||||||
- [x] **CORS 联调**:已完成与后端的跨域联调。
|
- [x] **业务模块共享**:适配后端变更,侧边栏现可按公共定义渲染所有活跃模块。
|
||||||
|
|
||||||
## ⚠️ 尚未解决的隐患 (Known Issues/Risks)
|
## ⚠️ 尚未解决的隐患 (Known Issues/Risks)
|
||||||
|
- **共享冲突提示**:缺乏对公共模块并发编辑的 UI 级冲突提示。
|
||||||
- **状态同步**:多组件间由于 AI 异步返回导致的 UI 闪烁问题。
|
- **状态同步**:多组件间由于 AI 异步返回导致的 UI 闪烁问题。
|
||||||
- **离线能力**:目前完全依赖实时 AI 交互,缺乏离线暂存机制。
|
|
||||||
|
|
||||||
## 🚀 下一步计划 (Next Steps)
|
## 🚀 下一步计划 (Next Steps)
|
||||||
1. **AI 智能搜索界面**:对接后端的 AI 增强搜索功能。
|
1. **数据权限展示**:在 UI 层面区分公共数据与私人数据。
|
||||||
2. **动态看板 (Dashboard)**:实现可视化图表的 AI 渲染支持。
|
2. **AI 智能搜索界面**:对接后端的 AI 增强搜索功能。
|
||||||
3. **引导流程优化**:提升用户通过对话创建新模块的体验。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last Updated: 2026-03-25 by Antigravity*
|
*Last Updated: 2026-03-25 by Antigravity*
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import {
|
|
||||||
CopilotRuntime,
|
|
||||||
GoogleGenerativeAIAdapter,
|
|
||||||
copilotRuntimeNextJSAppRouterEndpoint
|
|
||||||
} from "@copilotkit/runtime";
|
|
||||||
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
|
||||||
// 1. 初始化 Google Gemini 客户端
|
|
||||||
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY || "");
|
|
||||||
|
|
||||||
// 2. 选择模型 (使用 3.1 Pro)
|
|
||||||
const model = genAI.getGenerativeModel({
|
|
||||||
model: "gemini-3.1-pro",
|
|
||||||
// 如果需要启用思维模型等高级参数,可以在这里或 adapter 层级配置
|
|
||||||
});
|
|
||||||
|
|
||||||
const runtime = new CopilotRuntime();
|
|
||||||
|
|
||||||
// 3. 使用 GoogleGenerativeAIAdapter
|
|
||||||
const serviceAdapter = new GoogleGenerativeAIAdapter({
|
|
||||||
model: "gemini-1.5-pro"
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
|
||||||
runtime,
|
|
||||||
serviceAdapter,
|
|
||||||
endpoint: "/api/copilotkit",
|
|
||||||
});
|
|
||||||
|
|
||||||
return handleRequest(req);
|
|
||||||
};
|
|
||||||
@@ -123,27 +123,32 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
|||||||
}, [entityCode]);
|
}, [entityCode]);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (!confirm("确定要物理删除这条数据吗?该操作不可撤销,将由 AI 执行。")) return;
|
if (!confirm("确定要删除这条数据吗?该操作不可撤销。")) return;
|
||||||
try {
|
try {
|
||||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||||
const res = await fetch(`/api/agent/chat?sessionId=${userId}_OP`, {
|
const res = await fetch(`/api/agent/chat?sessionId=${userId}_OP`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-Creator-Id': userId },
|
headers: { 'X-Creator-Id': userId },
|
||||||
body: `请执行核心物理删除指令:删除记录 ID 为 ${id} 的数据。`
|
body: `请执行删除操作:物理删除记录 ID 为 ${id} 的数据。`
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert("✅ AI 已同步执行删除操作。");
|
const result = await res.json();
|
||||||
|
if (result.code === 200) {
|
||||||
loadData();
|
loadData();
|
||||||
|
} else {
|
||||||
|
alert(`删除失败: ${result.msg || "未知错误"}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("删除失败: 请求异常");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("删除失败");
|
alert("删除失败: 网络故障");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (row: any) => {
|
const handleEdit = (row: any) => {
|
||||||
// 排除系统字段
|
const { _createdAt, ...editData } = row;
|
||||||
const { _id, _createdAt, ...businessData } = row;
|
setEditingData(editData);
|
||||||
setEditingData(businessData);
|
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -15,6 +15,7 @@ const geistMono = Geist_Mono({
|
|||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import CopilotProvider from "@/components/CopilotProvider";
|
import CopilotProvider from "@/components/CopilotProvider";
|
||||||
import AuthGuard from "@/components/AuthGuard";
|
import AuthGuard from "@/components/AuthGuard";
|
||||||
|
import MainLayout from "@/components/MainLayout";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "AI-Native CRM",
|
title: "AI-Native CRM",
|
||||||
@@ -33,12 +34,9 @@ export default function RootLayout({
|
|||||||
>
|
>
|
||||||
<body className="flex h-screen overflow-hidden bg-slate-50 relative">
|
<body className="flex h-screen overflow-hidden bg-slate-50 relative">
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<CopilotProvider>
|
<MainLayout>
|
||||||
<Sidebar />
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</MainLayout>
|
||||||
</CopilotProvider>
|
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default function Home() {
|
|||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 relative overflow-hidden">
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-bl-full -z-10 opacity-50"></div>
|
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-bl-full -z-10 opacity-50"></div>
|
||||||
<h2 className="text-xl font-bold mb-2">👋 欢迎来到您的智能 AI 业务助理</h2>
|
<h2 className="text-xl font-bold mb-2">👋 欢迎来到您的智能 AI 业务助理</h2>
|
||||||
<h2 className="text-xl font-bold mb-2"> 测试模式,不能保存到后端! 以后发布后再测试吧</h2>
|
|
||||||
<p className="text-gray-600 text-sm leading-relaxed max-w-2xl">
|
<p className="text-gray-600 text-sm leading-relaxed max-w-2xl">
|
||||||
无需复杂的配置,直接告诉 AI 您想管理的内容。点击右下角的 <span className="text-blue-600 font-bold">Copilot 智能助手</span> 并输入您的需求,
|
无需复杂的配置,直接告诉 AI 您想管理的内容。点击右下角的 <span className="text-blue-600 font-bold">Copilot 智能助手</span> 并输入您的需求,
|
||||||
例如:<b>“我想创建一个员工入职登记表,包含姓名、职位和入职时间”</b>,系统将立即为您自动生成定制化的业务单据!
|
例如:<b>“我想创建一个员工入职登记表,包含姓名、职位和入职时间”</b>,系统将立即为您自动生成定制化的业务单据!
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "@copilotkit/react-ui/styles.css";
|
|||||||
|
|
||||||
export default function CopilotProvider({ children }: { children: React.ReactNode }) {
|
export default function CopilotProvider({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<CopilotKit runtimeUrl="/api/copilotkit">
|
<CopilotKit runtimeUrl={(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080") + "/api/copilotkit"}>
|
||||||
{children}
|
{children}
|
||||||
<CopilotPopup
|
<CopilotPopup
|
||||||
instructions="你是一个嵌入在高级企业 CRM 系统中的智能架构师。你可以通过 renderDynamicForm 为用户渲染新模块。当用户询问系统中有哪些模块或需要分析现有 schema 时,你可以调用 analyzeSystemModules 获取当前系统中已定义的业务模块及其 JSON Schema 进行分析并回答用户。"
|
instructions="你是一个嵌入在高级企业 CRM 系统中的智能架构师。你可以通过 renderDynamicForm 为用户渲染新模块。当用户询问系统中有哪些模块或需要分析现有 schema 时,你可以调用 analyzeSystemModules 获取当前系统中已定义的业务模块及其 JSON Schema 进行分析并回答用户。"
|
||||||
|
|||||||
@@ -53,11 +53,17 @@ interface DynamicFormProps {
|
|||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用动态表单组件 (支持关联与业务逻辑)
|
|
||||||
*/
|
|
||||||
export default function DynamicForm({ entityName, entityCode, jsonSchema, initialData, onSuccess }: DynamicFormProps) {
|
export default function DynamicForm({ entityName, entityCode, jsonSchema, initialData, onSuccess }: DynamicFormProps) {
|
||||||
const [formData, setFormData] = useState<Record<string, any>>(initialData || {});
|
const recordId = initialData?._id;
|
||||||
|
const isEdit = !!recordId;
|
||||||
|
|
||||||
|
const buildInitial = () => {
|
||||||
|
if (!initialData) return {};
|
||||||
|
const { _id, ...rest } = initialData;
|
||||||
|
return rest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<Record<string, any>>(buildInitial);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const schemaObj = useMemo(() => {
|
const schemaObj = useMemo(() => {
|
||||||
@@ -82,11 +88,19 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||||
const aiMessage = `【业务提交报文】\n` +
|
|
||||||
|
const aiMessage = isEdit
|
||||||
|
? `【业务更新报文】\n` +
|
||||||
|
`操作类型:更新已有记录\n` +
|
||||||
|
`记录 ID:${recordId}\n` +
|
||||||
`当前模块:${entityName} (${entityCode})\n` +
|
`当前模块:${entityName} (${entityCode})\n` +
|
||||||
`Schema 结构:${jsonSchema}\n` +
|
`更新后数据:${JSON.stringify(formData)}\n\n` +
|
||||||
`录入事实数据:${JSON.stringify(formData)}\n\n` +
|
`请调用 updateDynamicData 工具,使用上述 ID 和数据完成更新。`
|
||||||
`请 AI 自动调度工具完成此模块的校验注册与数据入库工作。`;
|
: `【业务提交报文】\n` +
|
||||||
|
`操作类型:新增记录\n` +
|
||||||
|
`当前模块:${entityName} (${entityCode})\n` +
|
||||||
|
`录入数据:${JSON.stringify(formData)}\n\n` +
|
||||||
|
`请调用 insertDynamicData 工具完成数据入库。`;
|
||||||
|
|
||||||
const response = await fetch(`/api/agent/chat?sessionId=${userId}_SESSION_${entityCode}`, {
|
const response = await fetch(`/api/agent/chat?sessionId=${userId}_SESSION_${entityCode}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -98,17 +112,20 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const resultText = await response.text();
|
const result = await response.json();
|
||||||
alert(`✅ AI 已成功处理!\n反馈: ${resultText}`);
|
if (result.code === 200) {
|
||||||
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
|
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
setFormData({});
|
setFormData({});
|
||||||
} else {
|
} else {
|
||||||
const errorMsg = await response.text();
|
alert(`操作失败: ${result.msg || "未知错误"}`);
|
||||||
alert(`❌ 处理失败: ${errorMsg}`);
|
}
|
||||||
|
} else {
|
||||||
|
const error = await response.json().catch(() => ({ msg: "请求失败" }));
|
||||||
|
alert(`操作失败: ${error.msg || "未知错误"}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("⚠️ 网络故障,请确保后端服务正常。");
|
alert("网络故障,请确保后端服务正常。");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
@@ -117,8 +134,8 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
|||||||
return (
|
return (
|
||||||
<div className="p-8 bg-white rounded-3xl">
|
<div className="p-8 bg-white rounded-3xl">
|
||||||
<h3 className="text-2xl font-black mb-8 border-b pb-6 text-slate-800 flex items-center gap-3">
|
<h3 className="text-2xl font-black mb-8 border-b pb-6 text-slate-800 flex items-center gap-3">
|
||||||
<span className="p-2 bg-indigo-600 rounded-xl text-white">✨</span>
|
<span className="p-2 bg-indigo-600 rounded-xl text-white">{isEdit ? "✏️" : "✨"}</span>
|
||||||
录入: {entityName}
|
{isEdit ? "编辑" : "录入"}: {entityName}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -173,7 +190,7 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
|||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
className="w-full mt-10 p-5 bg-gradient-to-br from-indigo-600 to-blue-700 text-white rounded-2xl font-black text-lg shadow-xl"
|
className="w-full mt-10 p-5 bg-gradient-to-br from-indigo-600 to-blue-700 text-white rounded-2xl font-black text-lg shadow-xl"
|
||||||
>
|
>
|
||||||
{isSaving ? "AI 处理中..." : "🚀 提交业务记录"}
|
{isSaving ? "AI 处理中..." : (isEdit ? "💾 保存修改" : "🚀 提交业务记录")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import Sidebar from "./Sidebar";
|
||||||
|
import CopilotProvider from "./CopilotProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主布局组件,根据当前路径决定是否渲染侧边栏和 AI 助理
|
||||||
|
*/
|
||||||
|
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isLoginPage = pathname === "/login";
|
||||||
|
|
||||||
|
if (isLoginPage) {
|
||||||
|
return <main className="w-full h-full">{children}</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CopilotProvider>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CopilotProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user