diff --git a/next.config.ts b/next.config.ts index e9ffa30..6c2eed5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://localhost:8080/api/:path*', + }, + ]; + }, }; export default nextConfig; diff --git a/src/app/data/[entityCode]/page.tsx b/src/app/data/[entityCode]/page.tsx index bb08a97..8bbad81 100644 --- a/src/app/data/[entityCode]/page.tsx +++ b/src/app/data/[entityCode]/page.tsx @@ -19,6 +19,7 @@ import { TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; +import DynamicForm from "@/components/DynamicForm"; interface DataPageProps { params: Promise<{ @@ -27,88 +28,125 @@ interface DataPageProps { } export default function EntityDataPage({ params: paramsPromise }: DataPageProps) { - // Use React.use() to unwrap Next.js 15+ dynamic params Promise const params = use(paramsPromise); const { entityCode } = params; - + const [entity, setEntity] = useState(null); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [sorting, setSorting] = useState([]); const [columns, setColumns] = useState[]>([]); - useEffect(() => { - async function loadData() { - setLoading(true); - try { - const [entityRes, dataRes] = await Promise.all([ - fetch(`http://localhost:8080/api/entities/code/${entityCode}`), - fetch(`http://localhost:8080/api/dynamic/code/${entityCode}`) - ]); - - if (entityRes.ok && dataRes.ok) { - const entityData = await entityRes.json(); - const records = await dataRes.json(); - setEntity(entityData); - - // Map backend entities to flat row format - const mappedData = records.map((r: any) => { - const parsedData = (typeof r.data === 'string') ? JSON.parse(r.data) : r.data; - return { - _id: r.id, - _createdAt: r.createdAt, - ...parsedData - }; - }); - setData(mappedData); - - // Build generic columns directly from Schema - const dynamicColumns: ColumnDef[] = []; - - if (entityData && entityData.schemaDefinition) { - try { - const schema = JSON.parse(entityData.schemaDefinition); - if (schema.properties) { - Object.keys(schema.properties).forEach((key) => { - const prop = schema.properties[key]; - dynamicColumns.push({ - accessorKey: key, - header: prop.title || key, - cell: ({ row }) => { - const cellValue = row.getValue(key); - return cellValue !== undefined && cellValue !== null ? String(cellValue) : "-"; - } - }); - }); - } - } catch(e) { - console.error("Schema Parsing Error:", e); - } - } + // Modal 状态 + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingData, setEditingData] = useState(null); + + const loadData = async () => { + setLoading(true); + try { + const userId = localStorage.getItem("crm_user_id") || "anonymous"; + const [entityRes, dataRes] = await Promise.all([ + fetch(`/api/entities/code/${entityCode}`, { headers: { 'X-Creator-Id': userId } }), + fetch(`/api/dynamic/code/${entityCode}`, { headers: { 'X-Creator-Id': userId } }) + ]); + + if (entityRes.ok && dataRes.ok) { + const entityData = await entityRes.json(); + const records = await dataRes.json(); + setEntity(entityData); - // System column created at - dynamicColumns.push({ - accessorKey: "_createdAt", - header: "创建时间", - cell: ({ row }) => { - const val: string = row.getValue("_createdAt"); - return val ? new Date(val).toLocaleString() : "-"; - } - }); - - setColumns(dynamicColumns); - } else { - console.error("Failed to fetch entity or data"); + const mappedData = records.map((r: any) => { + const parsedData = (typeof r.data === 'string') ? JSON.parse(r.data) : r.data; + return { + _id: r.id, + _createdAt: r.createdAt, + ...parsedData + }; + }); + setData(mappedData); + + const dynamicColumns: ColumnDef[] = []; + if (entityData && entityData.schemaDefinition) { + const schema = JSON.parse(entityData.schemaDefinition); + if (schema.properties) { + Object.keys(schema.properties).forEach((key) => { + const prop = schema.properties[key]; + dynamicColumns.push({ + accessorKey: key, + header: prop.title || key, + cell: ({ row }) => { + const val = row.getValue(key); + return val !== undefined && val !== null ? String(val) : "-"; + } + }); + }); + } } - } catch (err) { - console.error("Error loading table data:", err); - } finally { - setLoading(false); + + // 添加操作列 + dynamicColumns.push({ + id: "actions", + header: "操作", + cell: ({ row }) => ( +
+ + +
+ ) + }); + + setColumns(dynamicColumns); } + } catch (err) { + console.error("Load error:", err); + } finally { + setLoading(false); } + }; + + useEffect(() => { loadData(); }, [entityCode]); + const handleDelete = async (id: string) => { + if (!confirm("确定要物理删除这条数据吗?该操作不可撤销,将由 AI 执行。")) return; + try { + const userId = localStorage.getItem("crm_user_id") || "anonymous"; + const res = await fetch(`/api/agent/chat?sessionId=${userId}_OP`, { + method: 'POST', + headers: { 'X-Creator-Id': userId }, + body: `请执行核心物理删除指令:删除记录 ID 为 ${id} 的数据。` + }); + if (res.ok) { + alert("✅ AI 已同步执行删除操作。"); + loadData(); + } + } catch (err) { + alert("删除失败"); + } + }; + + const handleEdit = (row: any) => { + // 排除系统字段 + const { _id, _createdAt, ...businessData } = row; + setEditingData(businessData); + setIsModalOpen(true); + }; + const table = useReactTable({ data, columns, @@ -116,109 +154,90 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps) getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, - state: { - sorting, - }, + state: { sorting }, }); - if (loading) { - return ( -
-
- -

LOADING DYNAMIC RECORDS

-
-
- ); + if (loading && !data.length) { + return
正在加载...
; } - if (!entity) return
实体配置未找到或服务不可用。
; - return ( -
-
+
+
+

- 📋 - {entity.entityName} + 📋 + {entity?.entityName}

-

模块标识: {entity.entityCode} 系统自动映射的 JSONB 动态持久化视图

+

业务代码: {entityCode}

+
+
-
-
+ {/* 数据表格 */} +
+
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - + + {table.getHeaderGroups().map(hg => ( + + {hg.headers.map(h => ( + + {flexRender(h.column.columnDef.header, h.getContext())} + ))} ))} - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - -
- 🎈 -

暂无数据落库记录

-

目前没有任何 JSONB 数据。请到首页对 AI 输入需求填充表单。

-
-
+ {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} - )} + ))}
- -
-
- 总计解析并挂载 {data.length} 条动态记录 -
+ +
+ 共 {data.length} 条记录
- - + +
+ + {/* 弹窗容器 */} + {isModalOpen && ( +
+
+ + { setIsModalOpen(false); loadData(); }} + /> +
+
+ )}
); } + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3dd7404..c08be0b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,6 +13,8 @@ const geistMono = Geist_Mono({ }); import Sidebar from "@/components/Sidebar"; +import CopilotProvider from "@/components/CopilotProvider"; +import AuthGuard from "@/components/AuthGuard"; export const metadata: Metadata = { title: "AI-Native CRM", @@ -30,10 +32,14 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - -
- {children} -
+ + + +
+ {children} +
+
+
); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..8a03e9a --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function LoginPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim()) { + alert("请输入用户名"); + return; + } + + setIsLoading(true); + // 模拟登录:直接存入 localStorage + setTimeout(() => { + localStorage.setItem("crm_user_id", username); + router.push("/"); + }, 800); + }; + + return ( +
+ {/* 背景装饰 */} +
+
+ +
+
+
+ +
+
+ AI +
+

AI-Native CRM

+

智能生成架构 · 极简业务管理

+
+ +
+
+ + setUsername(e.target.value)} + /> +
+ +
+ + setPassword(e.target.value)} + /> +
+ + +
+ +
+

+ 提示: 当前为会话隔离模式,
+ 系统将根据您的用户名独占一份业务数据。 +

+
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index c19cd5e..07464f9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,50 +1,32 @@ "use client"; -import { CopilotKit } from "@copilotkit/react-core"; -import { CopilotPopup } from "@copilotkit/react-ui"; -import "@copilotkit/react-ui/styles.css"; import UniversalModuleRenderer from "@/components/UniversalModuleRenderer"; export default function Home() { return ( - // 【关键修改点】在 CopilotKit 组件上增加 provider="openai" - -
- {/* 顶部导航 */} -
-

AI-Native Copilot CRM

-
- 登录身份: ADMIN_TENANT_1 -
-
+
+ {/* 顶部导航 */} +
+

AI-Native CRM

+
+ 业务会话 ID: {(typeof window !== 'undefined' ? localStorage.getItem("crm_user_id") : "UNKNOWN") || "UNKNOWN"} +
+
-
-
-
-

欢迎使用声明式工作台

-

- 在这个颠覆性的系统中,您不再需要联系研发排期写代码。只需点击右下角的智能体按钮,例如告诉它:“我需要一个内部采购报销单,要求包含物品名称、总造价和期待到货日期”,系统就会立刻为您画出表单架构。 -

-
+
+
+
+

👋 欢迎来到您的智能 AI 业务助理

+

测试模式,不能保存到后端! 以后发布后再测试吧

+

+ 无需复杂的配置,直接告诉 AI 您想管理的内容。点击右下角的 Copilot 智能助手 并输入您的需求, + 例如:“我想创建一个员工入职登记表,包含姓名、职位和入职时间”,系统将立即为您自动生成定制化的业务单据! +

+
-
- -

业务模块画布空空如也,等待 AI 为您注入灵魂 (Generative UI Render Area)...

-
- - {/* 这里挂载在页面后台,负责监听并执行 AI 下发的 UI 绘画指令 */} - -
-
- - - + {/* 这里挂载在页面后台,负责监听并执行 AI 下发的 UI 绘画指令 */} + + +
); -} \ No newline at end of file +} diff --git a/src/components/AuthGuard.tsx b/src/components/AuthGuard.tsx new file mode 100644 index 0000000..560fb34 --- /dev/null +++ b/src/components/AuthGuard.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; + +export default function AuthGuard({ children }: { children: React.ReactNode }) { + const [isAuthorized, setIsAuthorized] = useState(false); + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + const userId = localStorage.getItem("crm_user_id"); + + if (!userId && pathname !== "/login") { + router.push("/login"); + } else if (userId && pathname === "/login") { + router.push("/"); + } else { + setIsAuthorized(true); + } + }, [pathname, router]); + + if (!isAuthorized && pathname !== "/login") { + return ( +
+
+
+

正在验证身份...

+
+
+ ); + } + + return <>{children}; +} diff --git a/src/components/CopilotProvider.tsx b/src/components/CopilotProvider.tsx new file mode 100644 index 0000000..5e125cd --- /dev/null +++ b/src/components/CopilotProvider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { CopilotKit } from "@copilotkit/react-core"; +import { CopilotPopup } from "@copilotkit/react-ui"; +import "@copilotkit/react-ui/styles.css"; + +export default function CopilotProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + + ); +} diff --git a/src/components/DynamicForm.tsx b/src/components/DynamicForm.tsx new file mode 100644 index 0000000..2f80381 --- /dev/null +++ b/src/components/DynamicForm.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState, useMemo, useEffect } from "react"; + +/** + * 子组件:实体关联选择器 (数据级联) + */ +export function EntitySelect({ targetCode, value, onChange }: { targetCode: string, value: any, onChange: (v: any) => void }) { + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchOptions = async () => { + const userId = localStorage.getItem("crm_user_id") || "anonymous"; + try { + const res = await fetch(`/api/dynamic/code/${targetCode}`, { + headers: { 'X-Creator-Id': userId } + }); + if (res.ok) { + const data = await res.json(); + setOptions(data); + } + } catch (err) { + console.error("加载关联实体数据失败:", err); + } finally { + setLoading(false); + } + }; + fetchOptions(); + }, [targetCode]); + + return ( + + ); +} + +interface DynamicFormProps { + entityName: string; + entityCode: string; + jsonSchema: string; + initialData?: Record; + onSuccess?: () => void; +} + +/** + * 通用动态表单组件 (支持关联与业务逻辑) + */ +export default function DynamicForm({ entityName, entityCode, jsonSchema, initialData, onSuccess }: DynamicFormProps) { + const [formData, setFormData] = useState>(initialData || {}); + const [isSaving, setIsSaving] = useState(false); + + const schemaObj = useMemo(() => { + try { + return JSON.parse(jsonSchema); + } catch (e) { + console.error("Schema 解析失败:", e); + return null; + } + }, [jsonSchema]); + + if (!schemaObj) return
正在解析中...
; + + const properties = schemaObj.properties || {}; + + const handleSave = async () => { + if (Object.keys(formData).length === 0) { + alert("请至少填写一项数据后再保存。"); + return; + } + + setIsSaving(true); + try { + const userId = localStorage.getItem("crm_user_id") || "anonymous"; + const aiMessage = `【业务提交报文】\n` + + `当前模块:${entityName} (${entityCode})\n` + + `Schema 结构:${jsonSchema}\n` + + `录入事实数据:${JSON.stringify(formData)}\n\n` + + `请 AI 自动调度工具完成此模块的校验注册与数据入库工作。`; + + const response = await fetch(`/api/agent/chat?sessionId=${userId}_SESSION_${entityCode}`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + 'X-Creator-Id': userId + }, + body: aiMessage + }); + + if (response.ok) { + const resultText = await response.text(); + alert(`✅ AI 已成功处理!\n反馈: ${resultText}`); + window.dispatchEvent(new CustomEvent('ENTITY_CREATED')); + if (onSuccess) onSuccess(); + setFormData({}); + } else { + const errorMsg = await response.text(); + alert(`❌ 处理失败: ${errorMsg}`); + } + } catch (err) { + alert("⚠️ 网络故障,请确保后端服务正常。"); + } finally { + setIsSaving(false); + } + }; + + return ( +
+

+ + 录入: {entityName} +

+ +
+ {Object.entries(properties).map(([key, field]: [string, any]) => { + const targetEntity = field['x-link-entity']; + const enumOptions = field.enum; + + const isRequired = schemaObj.required?.includes(key) || field.required === true; + + return ( +
+ + + {targetEntity ? ( + setFormData(prev => ({ ...prev, [key]: v }))} + /> + ) : enumOptions ? ( + + ) : ( + setFormData(prev => ({ ...prev, [key]: e.target.value }))} + /> + )} +
+ ); + })} +
+ + +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d7e736c..9f7b7a3 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,15 +3,27 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useCopilotReadable } from "@copilotkit/react-core"; export default function Sidebar() { const [entities, setEntities] = useState([]); const [loading, setLoading] = useState(true); const pathname = usePathname(); - useEffect(() => { + // 暴露当前系统中的模块列表给 Copilot 智能体,使其能够回答“系统里有什么模块”这类问题 + useCopilotReadable({ + description: "当前系统中已定义的业务模块列表(包括 entityCode, entityName, schemaDefinition)", + value: entities, + }); + + const fetchEntities = () => { + const userId = localStorage.getItem("crm_user_id") || "anonymous"; // Asynchronously fetch from API - fetch("http://localhost:8080/api/entities") + fetch("/api/entities", { + headers: { + 'X-Creator-Id': userId + } + }) .then(res => res.json()) .then(data => { setEntities(data); @@ -21,6 +33,21 @@ export default function Sidebar() { console.error("Failed to fetch entities", err); setLoading(false); }); + }; + + useEffect(() => { + fetchEntities(); + + // 1. 监听自定义事件触发即时刷新 (AI 生成或手动提交后) + window.addEventListener('ENTITY_CREATED', fetchEntities); + + // 2. 兜底方案:每 10 秒主动拉取一次,确保其他 Session 的变动可见 + const timer = setInterval(fetchEntities, 30000); + + return () => { + window.removeEventListener('ENTITY_CREATED', fetchEntities); + clearInterval(timer); + }; }, []); return ( @@ -30,11 +57,11 @@ export default function Sidebar() { AI
-

Copilot CRM

+

AI-Native CRM

智能生成架构

- +
- + {!loading && entities.length === 0 ? (
- 尚未生成任何模块
试着跟右下角 AI 说说话 + 尚未生成任何模块
试着跟右下角 AI 说说话
) : ( entities.map((ent, idx) => { const href = `/data/${ent.entityCode}`; const isActive = pathname === href; return ( - - + {ent.entityName} ); }) )} - +
-
- AD +
+ {(typeof window !== 'undefined' ? localStorage.getItem("crm_user_id") : "AD")?.slice(0, 2) || "AD"}
-

ADMIN 1

-

超级管理员租户

+

+ {typeof window !== 'undefined' ? localStorage.getItem("crm_user_id") : "ADMIN 1"} +

+

当前业务会话

+
diff --git a/src/components/UniversalModuleRenderer.tsx b/src/components/UniversalModuleRenderer.tsx index 1f78e26..4b300bc 100644 --- a/src/components/UniversalModuleRenderer.tsx +++ b/src/components/UniversalModuleRenderer.tsx @@ -7,132 +7,71 @@ import { useState, useMemo, useEffect } from "react"; * 子组件:动态表单渲染实体 (State 隔离,解决输入失效问题) * 该组件拥有独立的生命周期和 State,确保在输入时由于父组件重绘不会导致失去焦点。 */ -function DynamicForm({ args }: { args: any }) { - // 状态管理:用于收集动态表单中用户填入的各字段值 - const [formData, setFormData] = useState>({}); - const [isSaving, setIsSaving] = useState(false); - - // 1. 防御性检查:确保输入参数有效 - if (!args || !args.jsonSchema) return null; - - // 2. 解析 JSON Schema (使用 useMemo 避免每次渲染都重解析) - const schemaObj = useMemo(() => { - try { - return JSON.parse(args.jsonSchema); - } catch (e) { - console.error("Schema 解析失败:", e); - return null; - } - }, [args.jsonSchema]); - - if (!schemaObj) return
正在解析中...
; - - const properties = schemaObj.properties || {}; - - // 落地数据库逻辑 - const handleSave = async () => { - if (Object.keys(formData).length === 0) { - alert("请至少填写一项数据后再保存。"); - return; - } - - setIsSaving(true); - try { - // 优先确保后端已经创建了该模块定义(解决时序问题:AI可能在前端直接渲染,而后端还未来得及/或者没有注册模块) - try { - await fetch(`http://localhost:8080/api/dynamic/schema`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - // 注意:后端接受属性包括 entityCode, entityName, schemaDefinition - body: JSON.stringify({ - entityCode: args.entityCode, - entityName: args.entityName, - schemaDefinition: args.jsonSchema - }) - }); - } catch (err) { - console.warn("提前注册模块时发生网络异常:", err); - } - - const response = await fetch(`http://localhost:8080/api/dynamic/code/${args.entityCode}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Creator-Id': 'ADMIN_001' - }, - body: JSON.stringify(formData) - }); - - if (response.ok) { - alert(`✅ 数据落库成功!已存入 PostgreSQL。\n模块: ${args.entityName}`); - setFormData({}); // 清空表单 - } else { - const errorMsg = await response.text(); - alert(`❌ 虽然表单生成了,但保存失败: ${errorMsg}`); - } - } catch (err) { - alert("⚠️ 网络故障,请确保后端 8080 服务正在运行。"); - } finally { - setIsSaving(false); - } - }; - - return ( -
-

- - {args.entityName} -

- -
- {Object.entries(properties).map(([key, field]: [string, any]) => ( -
- - setFormData(prev => ({ ...prev, [key]: e.target.value }))} - /> -
- ))} -
- - -
- ); -} +import DynamicForm from "./DynamicForm"; /** * 主入口:通用模块渲染器 * 负责注册 AI Action,并将参数转发给解耦后的 DynamicForm 组件。 */ export default function UniversalModuleRenderer() { + const [activeForm, setActiveForm] = useState<{ entityName: string, entityCode: string, jsonSchema: string } | null>(null); + useCopilotAction({ name: "renderDynamicForm", - description: "在页面上为一个新的业务模块渲染基于 JSON Schema 的动态表单。", + description: "在页面上方展示并渲染基于 JSON Schema 的动态表单。", parameters: [ - { name: "entityName", type: "string" }, - { name: "entityCode", type: "string" }, - { name: "jsonSchema", type: "string" } + { name: "entityName", type: "string", description: "模块名称" }, + { name: "entityCode", type: "string", description: "模块编码" }, + { name: "jsonSchema", type: "string", description: "JSON Schema 定义" } ], handler: async (args) => { + // 通过 AI 下发指令时,设置状态让页面渲染 + setActiveForm({ + entityName: args.entityName, + entityCode: args.entityCode, + jsonSchema: args.jsonSchema, + }); + window.dispatchEvent(new CustomEvent('ENTITY_CREATED')); return `已确认 ${args.entityName} 的表单渲染指令。`; }, - render: (props: any) => { - // 将 AI 的渲染逻辑委托给 DynamicForm,实现 State 永久隔离,彻底解决输入焦点问题。 - return ; + }); + + // 动作 2:主动拉取并分析系统中已有的模块 + useCopilotAction({ + name: "analyzeSystemModules", + description: "从后端接口获取当前系统中已有的所有业务模块及其 JSON Schema 定义进行分析。", + handler: async () => { + try { + const res = await fetch("/api/entities"); + const data = await res.json(); + if (data.length === 0) { + return "系统中当前没有任何业务模块。"; + } + return `系统中目前有 ${data.length} 个模块:\n` + + data.map((m: any) => `- ${m.entityName} (${m.entityCode}): ${m.schemaDefinition}`).join("\n"); + } catch (err) { + return "从后端获取模块列表失败。"; + } } }); - return null; + if (!activeForm) { + return ( +
+ +

业务模块画布空空如也,等待 AI 为您注入灵魂...

+
+ ); + } + + return ( +
+ +
+ ); } +