Browse Source

feat: node render

Carson 2 months ago
parent
commit
c1fed0f117

+ 6 - 4
app/layout.tsx

@@ -3,7 +3,7 @@ import { Inter } from "next/font/google";
 import "./globals.css";
 import { TrpcContextProvider } from "@/components/providers/TrpcContextProvider";
 import { SessionProvider } from "@/components/providers/SessionProvider";
-
+import { JotaiProvider } from '@/components/providers/JotaiProvider'
 
 const inter = Inter({ subsets: ["latin"] });
 
@@ -23,9 +23,11 @@ export default function RootLayout({
     <html lang="en" data-theme={theme}>
       <body className={inter.className}>
         <SessionProvider>
-          <TrpcContextProvider>
-            {children}
-          </TrpcContextProvider>
+          <JotaiProvider>
+            <TrpcContextProvider>
+              {children}
+            </TrpcContextProvider>
+          </JotaiProvider>
         </SessionProvider>
       </body>
     </html>

+ 1 - 1
app/run-agent-flow/components/ASide.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 const ASide = () => {
   return (
-    <div className="  shadow-xl rounded-box min-w-[350px] p-2">aside</div>
+    <div className="shadow-xl rounded-box shrink-0 basis-[350px] p-2">aside</div>
   )
 }
 export default React.memo(ASide)

+ 2 - 2
app/run-agent-flow/components/Header.tsx

@@ -32,7 +32,7 @@ export default function Header() {
   }, [username, password, org])
 
   return (
-    <div className="navbar bg-base-100 shadow-xl rounded-box justify-center min-h-4 relative">
+    <div className="navbar shrink-0 bg-base-100 shadow-xl rounded-box justify-center min-h-4 relative">
       <div className="dropdown">
         <div tabIndex={0} role="button" className="btn btn-sm btn-wide btn-ghost">选择对话<BsCaretDownFill /></div>
         <ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
@@ -50,7 +50,7 @@ export default function Header() {
           </div>
         ) : (
           <>
-            <dialog className="modal modal-open" onCancel={event => event.preventDefault()}>
+            <dialog className="modal modal-close" onCancel={event => event.preventDefault()}>
               <div className="modal-box">
                 <h3 className="font-bold text-lg">您需要先登录</h3>
                 <div className="w-full flex flex-col items-center gap-2 py-2">

+ 44 - 12
app/run-agent-flow/components/NodeRender.tsx

@@ -1,34 +1,66 @@
 'use client';
 
-import React, { useMemo } from 'react'
+import React, { useEffect, useMemo } from 'react'
 import { BsArrowLeft, BsArrowRight } from "react-icons/bs";
 import Agent from './NodeType/Agent';
 import Form from './NodeType/Form';
 import Unsupport from './NodeType/Unsupport';
 import * as R from 'ramda'
+import { curNodeAtom, curStepAtom, arrowStateAtom, cardInstantAtomFamily, cardInstantAtomsAtom } from '../store';
+import { useAtom, useAtomValue } from 'jotai';
 
-const NodeRender = ({ node }) => {
+const NodeRender = () => {
+  const [curStep, setCurStep] = useAtom(curStepAtom)
+  const node = useAtomValue(curNodeAtom)
+  const arrowState = useAtomValue(arrowStateAtom)
   const Comp = useMemo(() => {
     return R.cond([
       [R.propEq('form_card', 'type'), R.always(Form)],
       [R.propEq('UserTask', 'type'), R.always(Agent)],
       [R.T, R.always(Unsupport)],
     ])(node)
-  }, [node?.id])
+  }, [node, node?.id])
+  const nodeName = useMemo(() => {
+    return R.cond([
+      [R.propEq('form_card', 'type'), R.always('表单填写')],
+      [R.propEq('UserTask', 'type'), R.pathOr('Unknown Agent', ['properties', 'item', 'assistantName'])],
+      [R.T, R.always('Unknown')],
+    ])(node)
+  }, [node, node?.id])
+
+  // 动态注册当前节点的Atom,并将其放进一个atom集合的atom,方便后续格式化所有节点实例
+  const cardInstantAtom = cardInstantAtomFamily(node?.id)
+  const [, dispatchCardInstantAtoms] = useAtom(cardInstantAtomsAtom)
+  useEffect(() => {
+    if (node?.id) {
+      dispatchCardInstantAtoms({ [node.id]: cardInstantAtom })
+    }
+  }, [cardInstantAtom])
+
+
+  const onNextStep = () => {
+    setCurStep(prev => prev + 1)
+    // TODO
+  }
+
+  const onPrevStep = () => {
+    // TODO
+    setCurStep(prev => prev - 1)
+  }
+
   return (
-    // <div className="flex-1 shadow-xl rounded-box p-2">node render</div>
-    <div className="card card-compact shadow-xl flex-1">
-      <div className="card-body">
+    <div className="card card-compact shadow-xl flex-1 overflow-hidden">
+      <div className="card-body overflow-hidden">
         <div className="card-title rounded-box bg-slate-200 p-2">
           <h2 className="flex-1">
-            Card title!
+            {curStep + 1}: {nodeName}
           </h2>
-          <button className='btn btn-circle btn-sm'><BsArrowLeft /></button>
-          <button className='btn btn-circle btn-sm'><BsArrowRight /></button>
+          <button className='btn btn-circle btn-sm' disabled={!arrowState.prev} onClick={onPrevStep}><BsArrowLeft /></button>
+          <button className='btn btn-circle btn-sm' disabled={!arrowState.next} onClick={onNextStep}><BsArrowRight /></button>
+        </div>
+        <div className="flex-1 flex flex-col items-stretch overflow-auto">
+          <Comp key={node?.id} node={node} cardInstantAtom={cardInstantAtom}></Comp>
         </div>
-        <Comp node={node}></Comp>
-        {/* TODO */}
-
         {/* <div className="card-actions justify-end">
           <button className="btn btn-primary">确认,下一步</button>
         </div> */}

+ 178 - 3
app/run-agent-flow/components/NodeType/Agent.tsx

@@ -1,7 +1,182 @@
-import React from 'react'
-const Agent = ({node}) => {
+import { useReducerAtom } from 'jotai/utils'
+import { EditorProvider, EditorContent, useCurrentEditor, BubbleMenu, useEditor } from "@tiptap/react";
+import StarterKit from "@tiptap/starter-kit";
+import React, { useMemo } from 'react'
+
+
+const MenuBar = ({ editor }) => {
   return (
-    <div className="">agent card {JSON.stringify(node)}</div>
+    <div className="join bubble-menu">
+      <button
+        onClick={() => editor.chain().focus().toggleBold().run()}
+        disabled={!editor.can().chain().focus().toggleBold().run()}
+        className={editor.isActive("bold") ? "is-active" : ""}
+      >
+        bold
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleItalic().run()}
+        disabled={!editor.can().chain().focus().toggleItalic().run()}
+        className={editor.isActive("italic") ? "is-active" : ""}
+      >
+        italic
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleStrike().run()}
+        disabled={!editor.can().chain().focus().toggleStrike().run()}
+        className={editor.isActive("strike") ? "is-active" : ""}
+      >
+        strike
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleCode().run()}
+        disabled={!editor.can().chain().focus().toggleCode().run()}
+        className={editor.isActive("code") ? "is-active" : ""}
+      >
+        code
+      </button>
+      <button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
+        clear marks
+      </button>
+      <button onClick={() => editor.chain().focus().clearNodes().run()}>
+        clear nodes
+      </button>
+      <button
+        onClick={() => editor.chain().focus().setParagraph().run()}
+        className={editor.isActive("paragraph") ? "is-active" : ""}
+      >
+        paragraph
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
+        className={editor.isActive("heading", { level: 1 }) ? "is-active" : ""}
+      >
+        h1
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
+        className={editor.isActive("heading", { level: 2 }) ? "is-active" : ""}
+      >
+        h2
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
+        className={editor.isActive("heading", { level: 3 }) ? "is-active" : ""}
+      >
+        h3
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
+        className={editor.isActive("heading", { level: 4 }) ? "is-active" : ""}
+      >
+        h4
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
+        className={editor.isActive("heading", { level: 5 }) ? "is-active" : ""}
+      >
+        h5
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
+        className={editor.isActive("heading", { level: 6 }) ? "is-active" : ""}
+      >
+        h6
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleBulletList().run()}
+        className={editor.isActive("bulletList") ? "is-active" : ""}
+      >
+        bullet list
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleOrderedList().run()}
+        className={editor.isActive("orderedList") ? "is-active" : ""}
+      >
+        ordered list
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleCodeBlock().run()}
+        className={editor.isActive("codeBlock") ? "is-active" : ""}
+      >
+        code block
+      </button>
+      <button
+        onClick={() => editor.chain().focus().toggleBlockquote().run()}
+        className={editor.isActive("blockquote") ? "is-active" : ""}
+      >
+        blockquote
+      </button>
+      <button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
+        horizontal rule
+      </button>
+      <button onClick={() => editor.chain().focus().setHardBreak().run()}>
+        hard break
+      </button>
+      <button
+        onClick={() => editor.chain().focus().undo().run()}
+        disabled={!editor.can().chain().focus().undo().run()}
+      >
+        undo
+      </button>
+      <button
+        onClick={() => editor.chain().focus().redo().run()}
+        disabled={!editor.can().chain().focus().redo().run()}
+      >
+        redo
+      </button>
+      {/* <button
+        onClick={() => editor.chain().focus().setColor("#958DF1").run()}
+        className={
+          editor.isActive("textStyle", { color: "#958DF1" }) ? "is-active" : ""
+        }
+      >
+        purple
+      </button> */}
+    </div>
+  );
+};
+
+const Agent = ({ node, cardInstantAtom }) => {
+
+  const dataReducer = (prev, payload) => {
+    return { ...prev, ...payload }
+  }
+
+  const [cardInstantData, dispatchCardInstantData] = useReducerAtom(cardInstantAtom, dataReducer)
+
+  const content = useMemo(() => {
+    return cardInstantData?.content
+  }, [cardInstantData, cardInstantData?.content])
+
+  const editor = useEditor({
+    extensions: [
+      StarterKit,
+    ],
+    editorProps: {
+      attributes: {
+        class: 'prose prose-sm sm:prose mx-auto focus:outline-none',
+      },
+    },
+    content,
+    onUpdate: () => {
+      dispatchCardInstantData({ content: editor?.getHTML() })
+    }
+  })
+
+  if (!editor) {
+    return null;
+  }
+  return (
+    <div>
+      {/* {editor && <BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
+        <MenuBar editor={editor} />
+      </BubbleMenu>} */}
+      <EditorContent
+        editor={editor}
+      >
+      </EditorContent>
+    </div>
   )
 }
 export default React.memo(Agent)

+ 26 - 3
app/run-agent-flow/components/NodeType/Form.tsx

@@ -1,7 +1,30 @@
-import React from 'react'
-const Form = ({node}) => {
+import React, { useEffect, useMemo } from 'react'
+import { cardInstantAtomFamily, cardInstantAtomsAtom } from '../../store'
+import { atomWithReducer, useReducerAtom } from 'jotai/utils'
+import { useAtom } from 'jotai'
+
+const Form = ({ node, cardInstantAtom }) => {
+  const fields = useMemo(() => {
+    return node?.properties?.fields
+  }, [node, node.id])
+
+  const dataReducer = (prev, payload) => {
+    return { ...prev, ...payload }
+  }
+
+  const [cardInstantData, dispatchCardInstantData] = useReducerAtom(cardInstantAtom, dataReducer)
+
   return (
-    <div className="">form card {JSON.stringify(node)}</div>
+    <div className="flex flex-col items-center w-full">
+      {fields.map((field, i) => (
+        <label key={i} className="form-control w-full max-w-xs">
+          <div className="label">
+            <span className="label-text">{field.value}</span>
+          </div>
+          <input type="text" placeholder={field.value} className="input input-bordered w-full max-w-xs" value={cardInstantData?.[field.value]} onChange={(e) => dispatchCardInstantData({ [field.value]: e.target.value })} />
+        </label>
+      ))}
+    </div>
   )
 }
 export default React.memo(Form)

+ 19 - 10
app/run-agent-flow/page.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import React from "react";
+import React, { useEffect } from "react";
 import ASide from "./components/ASide";
 import Flow from "./components/Flow";
 import Header from "./components/Header";
@@ -9,20 +9,29 @@ import { trpc } from "@/lib/trpc";
 import { useQuery } from '@tanstack/react-query'
 import { useSearchParam } from 'react-use';
 import { useSession, signIn, signOut } from "next-auth/react"
+import { useAtom, useSetAtom, useAtomValue } from "jotai";
+import { flowModelAtom, flowModelRecordAtom } from "./store";
 
 
 const RunAgentFlow = () => {
-  const { data: session, status } = useSession()
-  // const multiAgentId = useSearchParam('multiAgentId')
-  // const dialogsQuery = useQuery({queryKey: ['dialogs', multiAgentId], queryFn: () => {
-  //   fetch({})
-  // }})
-  
+  const multiAgentId = useSearchParam('multiAgentId')
+  if (!multiAgentId) {
+    // FIXME redirect to 404
+  }
+  const flowModelRecordQuery = trpc.flowModel.byId.useQuery({ multiAgentId })
+  if (flowModelRecordQuery.isError) {
+    // FIXME redirect to 500
+  }
+  const setFlowModelRecord = useSetAtom(flowModelRecordAtom)
+  useEffect(() => {
+    setFlowModelRecord(flowModelRecordQuery.data)
+  }, [flowModelRecordQuery.data])
+
   return (
-    <main className="flex min-h-screen flex-col items-center justify-between p-2 gap-2">
+    <main className="flex h-screen overflow-hidden flex-col items-stretch justify-between p-2 gap-2">
       <Header></Header>
-      <div className="flex align-stretch flex-1 w-full gap-2">
-        <div className="flex-1 flex flex-col align-stretch gap-2">
+      <div className="flex align-stretch flex-1 gap-2">
+        <div className="flex-1 flex flex-col align-stretch min-w-0 gap-2">
           <Flow></Flow>
           <NodeRender></NodeRender>
         </div>

+ 112 - 0
app/run-agent-flow/store.tsx

@@ -0,0 +1,112 @@
+'use client';
+import { atom } from "jotai";
+import { atomFamily, atomWithReducer } from 'jotai/utils'
+import * as R from 'ramda'
+
+export const flowModelRecordAtom = atom()
+flowModelRecordAtom.debugLabel = 'flowModelRecordAtom'
+
+// LF nodes model
+export const flowModelAtom = atom(get => {
+  const record = get(flowModelRecordAtom)
+  const { nodes, edges } = record?.content ?? {}
+  const chainNodes = []
+  let curNode = R.find(R.propEq('node_id_1', 'id'), nodes ?? [])
+  if (curNode) {
+    chainNodes.push(curNode)
+    const cached = new Set()
+    while (true) {
+      const nextNode = R.find(
+        R.propEq(
+          R.prop(
+            'targetNodeId',
+            R.find(R.propEq(curNode.id, 'sourceNodeId'), edges)
+          ),
+          'id'
+        ),
+        nodes
+      )
+      if (!nextNode || nextNode.id === 'node_id_2') {
+        break;
+      }
+      if (cached.has(nextNode.id)) {
+        break;
+      }
+      cached.add(nextNode.id)
+      curNode = nextNode
+      chainNodes.push(nextNode)
+    }
+  }
+  return chainNodes
+})
+flowModelAtom.debugLabel = 'flowModelAtom'
+
+// react flow nodes model
+export const flowModelTranslatedAtom = atom(get => {
+  // TODO translated to react flow model
+  return get(flowModelAtom)
+})
+flowModelTranslatedAtom.debugLabel = 'flowModelTranslatedAtom'
+
+// filtered for steps nodes model
+export const stepsNodesAtom = atom(get => {
+  const nodes = get(flowModelTranslatedAtom)
+  return R.filter(
+    R.propSatisfies(
+      R.includes(
+        R.__,
+        ['form_card', 'UserTask']
+      ),
+      'type'
+    ),
+    nodes
+  )
+})
+stepsNodesAtom.debugLabel = 'stepsNodesAtom'
+
+export const curStepAtom = atom(0)
+curStepAtom.debugLabel = 'curStepAtom'
+
+export const viewedStepAtom = atom(0)
+viewedStepAtom.debugLabel = 'viewedStepAtom'
+
+export const curNodeAtom = atom(get => {
+  const stepsNodes = get(stepsNodesAtom)
+  const curStep = get(curStepAtom)
+  return R.prop(curStep, stepsNodes)
+})
+curNodeAtom.debugLabel = 'curNodeAtom'
+
+export const viewedNodeAtom = atom(get => {
+  const stepsNodes = get(stepsNodesAtom)
+  const viewedStep = get(viewedStepAtom)
+  return R.prop(viewedStep, stepsNodes)
+})
+viewedNodeAtom.debugLabel = 'viewedNodeAtom'
+
+export const arrowStateAtom = atom(get => {
+  const stepsNodes = get(stepsNodesAtom)
+  const curStep = get(curStepAtom)
+  return {
+    prev: stepsNodes && curStep > 0,
+    next: stepsNodes && curStep < stepsNodes?.length - 1
+  }
+})
+arrowStateAtom.debugLabel = 'arrowStateAtom'
+
+
+export const cardInstantAtomFamily = atomFamily((cardId) => atom({ id: cardId }))
+
+export const cardInstantAtomsAtom = atomWithReducer({}, (prev, payload: { id: string; } & { [K in any]?: any }) => {
+  return { ...prev, ...payload }
+})
+cardInstantAtomsAtom.debugLabel = 'cardInstantAtomsAtom'
+
+// TODO
+export const instantDataAtom = atom(get => {
+  const cardInstantAtoms = get(cardInstantAtomsAtom)
+  const cardInstant = R.map((at) => get(at), cardInstantAtoms)
+  return cardInstant
+})
+instantDataAtom.debugLabel = 'instantDataAtom'
+

+ 14 - 0
components/providers/JotaiProvider.tsx

@@ -0,0 +1,14 @@
+'use client'
+
+import { Provider } from 'jotai'
+import { DevTools } from 'jotai-devtools'
+import 'jotai-devtools/styles.css'
+
+export const JotaiProvider = ({ children }) => {
+  return (
+    <Provider>
+      {children}
+      <DevTools />
+    </Provider>
+  )
+}

+ 3 - 1
next.config.mjs

@@ -1,4 +1,6 @@
 /** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+  transpilePackages: ['jotai-devtools'],
+};
 
 export default nextConfig;

File diff suppressed because it is too large
+ 734 - 15
package-lock.json


+ 6 - 0
package.json

@@ -11,10 +11,15 @@
   "dependencies": {
     "@prisma/client": "^5.18.0",
     "@tanstack/react-query": "^5.52.0",
+    "@tiptap/extension-bubble-menu": "^2.6.6",
+    "@tiptap/pm": "^2.6.6",
+    "@tiptap/react": "^2.6.6",
+    "@tiptap/starter-kit": "^2.6.6",
     "@trpc/client": "^11.0.0-rc.482",
     "@trpc/react-query": "^11.0.0-rc.482",
     "@trpc/server": "^11.0.0-rc.482",
     "jotai": "^2.9.3",
+    "jotai-devtools": "^0.10.1",
     "next": "14.2.5",
     "next-auth": "^4.24.7",
     "ramda": "^0.30.1",
@@ -27,6 +32,7 @@
     "zod": "^3.23.8"
   },
   "devDependencies": {
+    "@tailwindcss/typography": "^0.5.14",
     "@tanstack/react-query-devtools": "^5.52.0",
     "@types/node": "^20",
     "@types/ramda": "^0.30.1",

+ 1 - 0
prisma/schema.prisma

@@ -47,6 +47,7 @@ model Ai_Agent_Assistants {
   modelType        String?   @db.VarChar(255)
   createtime       DateTime? @db.DateTime(0)
   updatetime       DateTime? @db.DateTime(0)
+  a String? @db.Text
 }
 
 model Ai_Agent_Threads {

+ 10 - 7
server/router.ts

@@ -1,4 +1,5 @@
 import { t, publicProcedure, router } from "./trpc";
+import prisma from "@/lib/db";
 import { z } from "zod";
 
 export const appRouter = router({
@@ -9,7 +10,7 @@ export const appRouter = router({
     bySlug: publicProcedure
       .input(z.object({ mode: z.string() }))
       .query(async ({ input: { mode } }) => {
-        if (!mode) return {mail: '', name: ''};
+        if (!mode) return { mail: "", name: "" };
         const res = await fetch(
           "https://api.edu.cocorobo.cn/edu/admin/selectorganize",
           {
@@ -26,16 +27,18 @@ export const appRouter = router({
       }),
   },
   flowModel: {
-    list: publicProcedure
+    byId: publicProcedure
       .input(
         z.object({
-          userId: z.string(),
+          multiAgentId: z.string(),
         })
       )
-      .query(async (opts) => {
-        return {
-          greeting: `hello ${opts.input.userId}`,
-        };
+      .query(async ({ input: { multiAgentId } }) => {
+        const record = await prisma.muti_agent_list.findUnique({
+          where: { id: multiAgentId },
+        });
+        if (!record) return null;
+        return { ...record, content: JSON.parse(record.content!) };
       }),
   },
   // ...

+ 3 - 3
tailwind.config.ts

@@ -14,11 +14,11 @@ const config: Config = {
     based: true,
     styled: true,
     utils: true,
-    prefix: '',
+    prefix: "",
     logs: true,
     themes: ["light", "dark", "cupcake"],
-    themeRoot: ":root"
+    themeRoot: ":root",
   },
-  plugins: [require("daisyui")],
+  plugins: [require("daisyui"), require("@tailwindcss/typography")],
 };
 export default config;

Some files were not shown because too many files changed in this diff