Carson 10 місяців тому
батько
коміт
e4e8ffa27a

+ 1 - 1
app/run-agent-flow/components/ASideType/Agent.tsx

@@ -105,7 +105,7 @@ const Agent = ({ node, asideInstantAtom }) => {
     await onSend({ text })
   }
 
-  const cardInstantAtom = cardInstantAtomFamily(node.id)
+  const cardInstantAtom = cardInstantAtomFamily(node)
 
   const dataReducer = (prev, payload) => {
     return { ...prev, ...payload }

+ 64 - 48
app/run-agent-flow/components/Header.tsx

@@ -5,6 +5,11 @@ import { createRef, useEffect, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
 import { trpc } from "@/lib/trpc";
 import Cookies from 'js-cookie';
+import { useAtomValue } from "jotai";
+import { instantDataAtom, stepsNodesAtom } from "../store";
+import { exportFlowToDocx } from "../export";
+import { BsCloudDownload } from "react-icons/bs";
+
 
 export default function Header() {
   const { data: session, status } = useSession()
@@ -79,62 +84,73 @@ export default function Header() {
     }
   }, []);
 
+  const instantData = useAtomValue(instantDataAtom)
+  const stepsNodes = useAtomValue(stepsNodesAtom)
+
+  const onExport = () => {
+    const stepsInstantData = stepsNodes.map(node => instantData[node.id])
+    exportFlowToDocx(stepsInstantData)
+  }
+
   return (
-    <div className="navbar shrink-0 bg-base-100 shadow-xl rounded-box justify-center min-h-4 relative">
-      <div className="dropdown">
+    <div className="navbar shrink-0 bg-base-100 shadow-xl rounded-box justify-center 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-[2] w-52 p-2 shadow">
           <li><a>Item 1</a></li>
           <li><a>Item 2</a></li>
         </ul>
-      </div>
-      {status === 'authenticated'
-        ? (
-          <div className="absolute right-4 flex gap-2">
-            <div>
-              <p>Hi, {session.user?.name}</p>
-            </div>
-            <button className='btn btn-sm' onClick={logOut}>退出登录</button>
-          </div>
-        ) : (
-          <>
-            <dialog className="modal modal-close" onCancel={event => event.preventDefault()}>
-              <div className="modal-box">
-                <iframe src="https://edu.cocorobo.cn/course/login?type=2"
-                  style={{ border: "0px", width: "450px", height: "480px" }}></iframe>
+      </div> */}
+      <div className="absolute right-4 flex items-center">
+        <button className='btn btn-neutral' onClick={onExport}><BsCloudDownload />导出</button>
+        {status === 'authenticated'
+          ? (
+            <div className="absolute right-4 flex gap-2">
+              <div>
+                <p>Hi, {session.user?.name}</p>
               </div>
-              <div className="modal-box" style={{ display: "none" }} >
-                <h3 className="font-bold text-lg" >您需要先登录</h3>
-                <div className="w-full flex flex-col items-center gap-2 py-2">
-                  {isLogInFail && <div role="alert" className="alert alert-error">
-                    <span>账号或密码错误</span>
-                  </div>}
-                  <label className="form-control w-full max-w-xs">
-                    <div className="label">
-                      <span className="label-text">组织(选填)</span>
-                      {orgQuery?.data?.name && <span className="label-text-alt text-indigo-400">{orgQuery.data.name}</span>}
-                    </div>
-                    <input type="text" placeholder="" className="input input-bordered w-full max-w-xs" onChange={e => setOrg(e.target.value)} />
-                  </label>
-                  <label className="form-control w-full max-w-xs">
-                    <div className="label">
-                      <span className="label-text">用户名</span>
-                    </div>
-                    <input type="text" placeholder="" className="input input-bordered w-full max-w-xs" onChange={e => setUsername(e.target.value)} />
-                  </label>
-                  <label className="form-control w-full max-w-xs">
-                    <div className="label">
-                      <span className="label-text">密码</span>
-                    </div>
-                    <input type="password" placeholder="" className="input input-bordered w-full max-w-xs" onChange={e => setPassword(e.target.value)} />
-                  </label>
-                  <button className='btn btn-wide' disabled={!username || !password} onClick={logIn}>登录</button>
+              <button className='btn btn-sm' onClick={logOut}>退出登录</button>
+            </div>
+          ) : (
+            <>
+              <dialog className="modal modal-close" onCancel={event => event.preventDefault()}>
+                <div className="modal-box">
+                  <iframe src="https://edu.cocorobo.cn/course/login?type=2"
+                    style={{ border: "0px", width: "450px", height: "480px" }}></iframe>
                 </div>
-              </div>
-            </dialog>
-          </>
-        )
-      }
+                <div className="modal-box" style={{ display: "none" }} >
+                  <h3 className="font-bold text-lg" >您需要先登录</h3>
+                  <div className="w-full flex flex-col items-center gap-2 py-2">
+                    {isLogInFail && <div role="alert" className="alert alert-error">
+                      <span>账号或密码错误</span>
+                    </div>}
+                    <label className="form-control w-full max-w-xs">
+                      <div className="label">
+                        <span className="label-text">组织(选填)</span>
+                        {orgQuery?.data?.name && <span className="label-text-alt text-indigo-400">{orgQuery.data.name}</span>}
+                      </div>
+                      <input type="text" placeholder="" className="input input-bordered w-full max-w-xs" onChange={e => setOrg(e.target.value)} />
+                    </label>
+                    <label className="form-control w-full max-w-xs">
+                      <div className="label">
+                        <span className="label-text">用户名</span>
+                      </div>
+                      <input type="text" placeholder="" className="input input-bordered w-full max-w-xs" onChange={e => setUsername(e.target.value)} />
+                    </label>
+                    <label className="form-control w-full max-w-xs">
+                      <div className="label">
+                        <span className="label-text">密码</span>
+                      </div>
+                      <input type="password" placeholder="" className="input input-bordered w-full max-w-xs" onChange={e => setPassword(e.target.value)} />
+                    </label>
+                    <button className='btn btn-wide' disabled={!username || !password} onClick={logIn}>登录</button>
+                  </div>
+                </div>
+              </dialog>
+            </>
+          )
+        }
+      </div>
     </div >
   )
 }

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

@@ -29,7 +29,7 @@ const NodeRender = () => {
   }, [node, node?.id])
 
   // 动态注册当前节点的Atom,并将其放进一个atom集合的atom,方便后续格式化所有节点实例
-  const cardInstantAtom = cardInstantAtomFamily(node?.id)
+  const cardInstantAtom = cardInstantAtomFamily(node)
   const [, dispatchCardInstantAtoms] = useAtom(cardInstantAtomsAtom)
   useEffect(() => {
     if (node?.id) {

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

@@ -31,7 +31,7 @@ const Form = ({ node, cardInstantAtom }) => {
           <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) => onChange(e.target.value, field)} />
+          <input type="text" placeholder={field.value} className="input input-bordered w-full max-w-xs" value={cardInstantData?.content?.[field.id]?.value ?? ''} onChange={(e) => onChange(e.target.value, field)} />
         </label>
       ))}
     </div>

+ 152 - 103
app/run-agent-flow/export.ts

@@ -1,148 +1,197 @@
-import _ from 'lodash'
-import { saveAs } from 'file-saver'
-import { Document, Packer, Paragraph, TextRun, Table, TableCell, TableRow, WidthType } from "docx";
-
-
+import _ from "lodash";
+import { saveAs } from "file-saver";
+import {
+  Document,
+  Packer,
+  Paragraph,
+  TextRun,
+  Table,
+  TableCell,
+  TableRow,
+  WidthType,
+} from "docx";
 
 // eslint-disable-next-line
 const buildParagraphOptions = (node, context) => {
   return _.cond([
-    [_.matches({ tagName: 'H1' }), _.constant({ heading: "Heading1" })],
-    [_.matches({ tagName: 'H2' }), _.constant({ heading: "Heading2" })],
-    [_.matches({ tagName: 'H3' }), _.constant({ heading: "Heading3" })],
-    [_.matches({ tagName: 'H4' }), _.constant({ heading: "Heading4" })],
-    [_.matches({ tagName: 'H5' }), _.constant({ heading: "Heading5" })],
-    [_.matches({ tagName: 'H6' }), _.constant({ heading: "Heading6" })],
+    [_.matches({ tagName: "H1" }), _.constant({ heading: "Heading1" })],
+    [_.matches({ tagName: "H2" }), _.constant({ heading: "Heading2" })],
+    [_.matches({ tagName: "H3" }), _.constant({ heading: "Heading3" })],
+    [_.matches({ tagName: "H4" }), _.constant({ heading: "Heading4" })],
+    [_.matches({ tagName: "H5" }), _.constant({ heading: "Heading5" })],
+    [_.matches({ tagName: "H6" }), _.constant({ heading: "Heading6" })],
     [_.stubTrue, _.constant({})],
-  ])(node)
-}
-
-
+  ])(node);
+};
 
-const buildDocxParagraph = (node, context = {}) => new Paragraph({ ...buildParagraphOptions(node, context), children: _.flatMap(node.childNodes, c => buildDocxTextRun(c)) })
+const buildDocxParagraph = (node, context = {}) =>
+  new Paragraph({
+    ...buildParagraphOptions(node, context),
+    children: _.flatMap(node.childNodes, (c) => buildDocxTextRun(c)),
+  });
 
 // eslint-disable-next-line
 const buildTextRunOptions = (node) => {
   // TODO 斜体 加粗
-  return {}
-}
+  return {};
+};
 
 // eslint-disable-next-line
-const buildDocxTextRun = (node, context = {}) => new TextRun({ text: node.textContent.trim(), ...buildTextRunOptions(node) })
-
-
+const buildDocxTextRun = (node, context = {}) =>
+  new TextRun({ text: node.textContent.trim(), ...buildTextRunOptions(node) });
 
 // NOTE this make sure root is a `Paragraph`, Paragraph can not nest Paragraph
-const buildDocxObject = (node, context = {}) => _.cond([
-  [_.matches({ nodeType: Node.ELEMENT_NODE }), _.cond([
-    [_.conforms({ tagName: tag => ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tag) }), c => buildDocxParagraph(c)],
-    [_.matches({ tagName: 'OL' }), (n) => _.flatMap(n.childNodes, c => buildDocxObject(c, context))],
-    [_.matches({ tagName: 'UL' }), (n) => _.flatMap(n.childNodes, c => buildDocxObject(c, context))],
-    [_.matches({ tagName: 'LI' }), (n) => {
-      const childNodes = _.filter(n.childNodes, c => !!c.textContent.trim())
-      let i = _.findIndex(childNodes, c => ['OL', 'UL', 'LI'].includes(c.tagName))
-      if (i === -1) {
-        i = Infinity
-      }
-      const preNodes = _.slice(childNodes, 0, i)
-      const postNodes = _.slice(childNodes, i)
-      return [
-        new Paragraph({ bullet: { level: context.level ?? 0 }, children: _.map(preNodes, c => buildDocxTextRun(c)) }),
-        ..._.flatMap(postNodes, c => buildDocxObject(c, { level: context.level ? context.level + 1 : 1 }))
-      ]
-    }
-    ],
-    [_.stubTrue, buildDocxParagraph],
-  ])],
-  [_.matches({ nodeType: Node.TEXT_NODE }), (n) => new Paragraph(n.textContent.trim())],
-  [_.stubTrue, (n) => new Paragraph(n.textContent.trim())],
-])(node)
-
-
-
-const buildFormCardRows = (node) => {
-  const chunkedFields = _.chunk(_.get(node, ['properties', 'form_card_data'], []), 3)
-  return chunkedFields.map((fields) =>
-    new TableRow({
-      children: _.flatten(
-        _.assign(
-          _.fill(new Array(3), [new TableCell({ children: [], columnSpan: 2 })]),
-          fields?.map(field => {
+const buildDocxObject = (node, context = {}) =>
+  _.cond([
+    [
+      _.matches({ nodeType: Node.ELEMENT_NODE }),
+      _.cond([
+        [
+          _.conforms({
+            tagName: (tag) =>
+              ["P", "H1", "H2", "H3", "H4", "H5", "H6"].includes(tag),
+          }),
+          (c) => buildDocxParagraph(c),
+        ],
+        [
+          _.matches({ tagName: "OL" }),
+          (n) => _.flatMap(n.childNodes, (c) => buildDocxObject(c, context)),
+        ],
+        [
+          _.matches({ tagName: "UL" }),
+          (n) => _.flatMap(n.childNodes, (c) => buildDocxObject(c, context)),
+        ],
+        [
+          _.matches({ tagName: "LI" }),
+          (n) => {
+            const childNodes = _.filter(
+              n.childNodes,
+              (c) => !!c.textContent.trim()
+            );
+            let i = _.findIndex(childNodes, (c) =>
+              ["OL", "UL", "LI"].includes(c.tagName)
+            );
+            if (i === -1) {
+              i = Infinity;
+            }
+            const preNodes = _.slice(childNodes, 0, i);
+            const postNodes = _.slice(childNodes, i);
             return [
-              new TableCell({
-                children: [new Paragraph(field.value)],
-                width: { size: 10, type: WidthType.PERCENTAGE },
+              new Paragraph({
+                bullet: { level: context.level ?? 0 },
+                children: _.map(preNodes, (c) => buildDocxTextRun(c)),
               }),
-              new TableCell({
-                children: [new Paragraph(field.input)],
-                width: { size: 20, type: WidthType.PERCENTAGE },
-              })
-            ]
-          })
-        )
-      )
+              ..._.flatMap(postNodes, (c) =>
+                buildDocxObject(c, {
+                  level: context.level ? context.level + 1 : 1,
+                })
+              ),
+            ];
+          },
+        ],
+        [_.stubTrue, buildDocxParagraph],
+      ]),
+    ],
+    [
+      _.matches({ nodeType: Node.TEXT_NODE }),
+      (n) => new Paragraph(n.textContent.trim()),
+    ],
+    [_.stubTrue, (n) => new Paragraph(n.textContent.trim())],
+  ])(node);
 
-    }),
-  )
-}
+const buildFormCardRows = (node) => {
+  const chunkedFields = _.chunk(_.values(_.get(node, ["content"], {})), 3);
+  return chunkedFields.map(
+    (fields) =>
+      new TableRow({
+        children: _.flatten(
+          _.assign(
+            _.fill(new Array(3), [
+              new TableCell({ children: [], columnSpan: 2 }),
+            ]),
+            fields?.map((field) => {
+              return [
+                new TableCell({
+                  children: [new Paragraph(field.label)],
+                  width: { size: 10, type: WidthType.PERCENTAGE },
+                }),
+                new TableCell({
+                  children: [new Paragraph(field.value)],
+                  width: { size: 20, type: WidthType.PERCENTAGE },
+                }),
+              ];
+            })
+          )
+        ),
+      })
+  );
+};
 
 const convertAgentContent = (content) => {
-
-  const node = document.createElement('div')
-  node.innerHTML = content
-  return _.flatMap(node.childNodes, buildDocxObject)
-}
+  const node = document.createElement("div");
+  node.innerHTML = content;
+  return _.flatMap(node.childNodes, buildDocxObject);
+};
 
 const buildAgentRows = (node) => {
   return [
     new TableRow({
       children: [
         new TableCell({
-          children: [new Paragraph(node.name)],
+          children: [new Paragraph(node.assistantName)],
           width: { size: 10, type: WidthType.PERCENTAGE },
         }),
         new TableCell({
-          children: convertAgentContent(node.properties?.content),
+          children: convertAgentContent(node.content),
           width: { size: 90, type: WidthType.PERCENTAGE },
           columnSpan: 5,
         }),
-      ]
-    })
-  ]
-}
+      ],
+    }),
+  ];
+};
 
 const buildRows = (nodes) => {
   const sectionBuilder = _.cond([
     // Form Node
-    [_.matches({ type: 'form_card' }), buildFormCardRows],
+    [_.matches({ type: "form_card" }), buildFormCardRows],
     // Agent Node
-    [_.matches({ type: 'UserTask' }), buildAgentRows],
-    [_.stubTrue, _.constant([])]
-  ])
-  return _.flatten(nodes.map(sectionBuilder))
-}
+    [_.matches({ type: "UserTask" }), buildAgentRows],
+    [_.stubTrue, _.constant([])],
+  ]);
+  return _.flatten(nodes.map(sectionBuilder));
+};
 
 export const exportFlowToDocx = (nodes) => {
+  const children: unknown[] = [
+    new Paragraph({
+      text: "课程设计",
+      heading: "Heading1",
+      alignment: "center",
+    }),
+  ];
+  const rows = buildRows(nodes);
+  if (rows.length) {
+    children.push(
+      new Table({
+        rows,
+      })
+    );
+  }
 
   const doc = new Document({
-    sections: [{
-      children: [
-        new Paragraph({
-          text: "课程设计",
-          heading: "Heading1",
-          alignment: 'center'
-        }),
-        new Table({
-          rows: buildRows(nodes)
-        }),
-      ],
-    }],
+    sections: [
+      {
+        children,
+      },
+    ],
   });
 
   // 将文档打包成Buffer并保存为文件
   Packer.toBlob(doc).then((blob) => {
-    const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
-    saveAs(blob, 'Generated.docx', { type: mimeType })
+    const mimeType =
+      "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+    saveAs(blob, "课程设计.docx", { type: mimeType });
   });
-  return
-}
+  return;
+};

+ 17 - 1
app/run-agent-flow/store.tsx

@@ -96,7 +96,23 @@ arrowStateAtom.debugLabel = 'arrowStateAtom'
 
 
 // node render下相关的状态
-export const cardInstantAtomFamily = atomFamily((id) => atom({ id }))
+export const cardInstantAtomFamily = atomFamily(
+  (node) => {
+    const defaults = { id: node?.id, type: node?.type }
+    switch (node?.type) {
+      case 'form_card':
+        defaults['fieldsDefinition'] = node?.properties?.fields
+        break;
+      case 'UserTask':
+        defaults['assistantName'] = R.path(['properties', 'item', 'assistantName'], node)
+        break;
+      default:
+        break;
+    }
+    return atom(defaults)
+  },
+  (a, b) => a?.id === b?.id
+)
 
 export const cardInstantAtomsAtom = atomWithReducer({}, (prev, payload: { id: string; } & { [K in any]?: any }) => {
   return { ...prev, ...payload }