Carson 2 місяців тому
батько
коміт
daac7c18d8

+ 9 - 11
app/api/auth/[...nextauth]/route.ts

@@ -15,12 +15,12 @@ const authOptions = {
       // e.g. domain, username, password, 2FA token, etc.
       // You can pass any HTML attribute to the <input> tag through the object.
       credentials: {
-        userid: { label: 'userid', type: 'text' }
+        userId: { label: "theUserId", type: "text", required: true },
         // loginUsername: { label: "用户名", type: "text" },
         // loginPassword: { label: "密码", type: "password" },
       },
       async authorize(credentials, req) {
-        return { userid: credentials.userid };
+        return { id: credentials.userId, name: 'anonymous' };
         // You need to provide your own logic here that takes the credentials
         // submitted and returns either a object representing a user or value
         // that is false/null if the credentials are invalid.
@@ -49,19 +49,17 @@ const authOptions = {
           return { ...user, id: user.userid, name: user.username };
         }
         */
-
-
       },
     }),
   ],
   callbacks: {
-    async session({ session, user }) {
-      console.log(session, user);
+    // we have no db intergrate, `user` is always empty because there is no db record
+    async session({ session, token, user: _user }) {
       // Send properties to the client, like an access_token from a provider.
-      session.user.id = user.userid;
+      session.user.id = token.sub
       try {
         const res = await fetch(
-          `https://pbl.cocorobo.cn/api/pbl/selectUser?userid=${user.userid}`,
+          `https://pbl.cocorobo.cn/api/pbl/selectUser?userid=${token.sub}`,
           {
             method: "GET",
             headers: {
@@ -71,10 +69,10 @@ const authOptions = {
         );
         const username = (await res.json())?.[0]?.[0]?.username;
         session.user.name = username;
+      } catch (e) {
+        session.user.name = token.name
       }
-      catch (e) {
-        return null;
-      }
+      console.log(session, token);
       return session;
     },
   },

+ 0 - 0
components/ChatSender.tsx → app/components/ChatSender.tsx


+ 70 - 0
app/components/providers/AuthProvider.tsx

@@ -0,0 +1,70 @@
+'use client'
+
+import React, { useEffect, useState } from 'react'
+import { signIn, useSession } from "next-auth/react"
+import { ReactNode } from "react"
+import { useQuery } from "@tanstack/react-query";
+
+const queryAuthFn = async () => {
+  const cookie = await fetch("https://beta.api.cocorobo.cn/api/getcookieuserid", {
+    method: "GET",
+    credentials: 'include',
+  });
+  const cookiejson = await cookie.json();
+  const user = cookiejson?.[0]?.[0];
+  if (cookie.ok && user) {
+    return user
+  }
+  return null
+}
+
+const LoadingMask = () => {
+  const { status } = useSession()
+  if (status === 'loading') {
+    return (
+      <div className="absolute top-0 left-0 w-full h-full bg-black/50 flex items-center justify-center">
+        <span className="loading loading-spinner loading-lg m-auto"></span>
+      </div>
+    )
+  }
+  return null
+}
+
+const AuthModal = () => {
+  const { data: user, refetch } = useQuery({ queryKey: ['auth'], queryFn: queryAuthFn })
+
+  useEffect(() => {
+    const intervalId = setInterval(() => {
+      refetch()
+    }, 5000)
+    return () => clearInterval(intervalId)
+  }, [refetch])
+
+  useEffect(() => {
+    if (user) {
+      signIn('credentials', { redirect: false, userId: user.userid })
+    }
+  }, [user])
+
+  return (
+    <dialog className="modal modal-open">
+      <div className="modal-box relative">
+        <iframe src="https://edu.cocorobo.cn/course/login?type=2"
+          style={{ border: "0px", width: "450px", height: "480px" }}></iframe>
+        <LoadingMask />
+      </div>
+    </dialog>
+  )
+}
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+  const { data: session, status } = useSession()
+
+
+  if (status === 'authenticated') {
+    return children
+  }
+  return (
+    <AuthModal />
+  )
+}

+ 0 - 0
components/providers/JotaiProvider.tsx → app/components/providers/JotaiProvider.tsx


+ 14 - 0
app/components/providers/SessionAuthProvider.tsx

@@ -0,0 +1,14 @@
+'use client'
+import { useSession, signIn, signOut, SessionProvider } from "next-auth/react"
+import { type ReactNode } from "react"
+import { AuthProvider } from "./AuthProvider"
+
+export function SessionAuthProvider({ children }: { children: ReactNode }) {
+  return (
+    <SessionProvider>
+      <AuthProvider>
+        {children}
+      </AuthProvider>
+    </SessionProvider>
+  )
+}

+ 0 - 0
components/providers/TrpcContextProvider.tsx → app/components/providers/TrpcContextProvider.tsx


+ 10 - 12
app/layout.tsx

@@ -1,9 +1,9 @@
 import type { Metadata } from "next";
 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'
+import { TrpcContextProvider } from "@/app/components/providers/TrpcContextProvider";
+import { SessionAuthProvider } from "@/app/components/providers/SessionAuthProvider";
+import { JotaiProvider } from '@/app/components/providers/JotaiProvider'
 
 const inter = Inter({ subsets: ["latin"] });
 
@@ -17,18 +17,16 @@ export default function RootLayout({
 }: Readonly<{
   children: React.ReactNode;
 }>) {
-  // const [theme] = useState("cupcake")
-  const theme = "cupcake"
   return (
-    <html lang="en" data-theme={theme}>
+    <html lang="en">
       <body className={inter.className}>
-        <SessionProvider>
-          <JotaiProvider>
-            <TrpcContextProvider>
+        <JotaiProvider>
+          <TrpcContextProvider>
+            <SessionAuthProvider>
               {children}
-            </TrpcContextProvider>
-          </JotaiProvider>
-        </SessionProvider>
+            </SessionAuthProvider>
+          </TrpcContextProvider>
+        </JotaiProvider>
       </body>
     </html>
   );

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

@@ -5,7 +5,7 @@ import * as R from 'ramda'
 import markdownit from 'markdown-it'
 import { v4 as uuid4 } from 'uuid'
 import Chater from './Chater'
-import Sender from '@/components/ChatSender'
+import Sender from '@/app/components/ChatSender'
 import { useReducerAtom } from "jotai/utils"
 import { getChatResponse, template } from "@/lib/utils"
 import { useSession } from "next-auth/react"
@@ -42,22 +42,29 @@ const Agent = ({ node, asideInstantAtom }) => {
     newMessages.push(message)
     setMessages((prev) => [...prev, ...newMessages])
     // messageItem.current = message
-    const [chunks, ctrl] = getChatResponse({
-      text,
-      assistantId,
-      sessionName,
-      // userId: session?.user?.id,
-      // FIXME
-      userId: '1c9dc4b-d95f-11ea-af4c-52540005ab01'
-    })
-    ctrlRef.current = ctrl
-    for await (const chunk of chunks) {
-      message.content += chunk;
+    try {
+      const [chunks, ctrl] = getChatResponse({
+        text,
+        assistantId,
+        sessionName,
+        userId: session?.user?.id,
+        // FIXME
+        // userId: '1c9dc4b-d95f-11ea-af4c-52540005ab01'
+      })
+      ctrlRef.current = ctrl
+      for await (const chunk of chunks) {
+        message.content += chunk;
+        setMessages((prev) => [...prev])
+      }
+    } catch (e) {
+      message.isError = true
+      message.error = e.message
+    } finally {
       setMessages((prev) => [...prev])
+      message.isLoading = false
+      setIsSending(false)
+      return message
     }
-    message.isLoading = false
-    setIsSending(false)
-    return message
   }
 
   const onFirstSend = async () => {

+ 20 - 5
app/run-agent-flow/components/ASideType/Chater.tsx

@@ -4,6 +4,8 @@ import React, { useMemo } from 'react'
 import Markdown from 'react-markdown'
 import { twMerge } from 'tailwind-merge'
 import * as R from 'ramda'
+import { BsXCircle } from "react-icons/bs";
+
 
 
 type IMessage = {
@@ -26,7 +28,7 @@ const Chater = ({ messages, node, onAccept }: { messages: IMessages }) => {
     <div className="flex flex-col-reverse w-full overflow-auto pb-[90px]">
       {
         reversedMessages.map((message, i) => (
-          <div key={i} className={twMerge("chat ", message.role === 'assistant' ? "chat-start" : 'chat-end')}>
+          <div key={i} className={twMerge("chat", message.role === 'assistant' ? "chat-start" : 'chat-end')}>
             <div className="chat-image avatar placeholder">
               {message.role === 'assistant'
                 ? (
@@ -42,7 +44,7 @@ const Chater = ({ messages, node, onAccept }: { messages: IMessages }) => {
                   </div>
                 )}
             </div>
-            <div className={twMerge("chat-bubble prose prose-sm", message.role === 'assistant' ? "chat-bubble-primary" : "chat-bubble-secondary")}>
+            <div className={twMerge("chat-bubble prose prose-sm !prose-invert", message.role === 'assistant' ? "chat-bubble-primary" : "")}>
               <Markdown>
                 {message?.content}
               </Markdown>
@@ -50,13 +52,26 @@ const Chater = ({ messages, node, onAccept }: { messages: IMessages }) => {
                 message.role === 'assistant'
                   ? message?.isLoading
                     ? <span className="loading loading-dots loading-sm"></span>
-                    : node.type === 'UserTask'
+                    : message.isError
                       ? (
                         <>
-                          <button className="btn btn-xs btn-neutral" onClick={() => onAccept?.(message)}>采纳</button>
+                          <div className="divider my-0"></div>
+                          <div role="alert" className="alert alert-error py-1">
+                            <BsXCircle />
+                            <span>
+                              {message.error}
+                            </span>
+                          </div>
                         </>
                       )
-                      : null
+                      : node.type === 'UserTask'
+                        ? (
+                          <>
+                            <div className="divider my-0"></div>
+                            <button className="btn btn-xs btn-neutral" onClick={() => onAccept?.(message)}>采纳</button>
+                          </>
+                        )
+                        : null
                   : null
               }
             </div>

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

@@ -7,7 +7,7 @@ import { useSession } from "next-auth/react";
 import Chater from "./Chater";
 import { getChatResponse } from "@/lib/utils";
 import * as R from 'ramda'
-import Sender from '@/components/ChatSender'
+import Sender from '@/app/components/ChatSender'
 
 
 const Form = ({ node, asideInstantAtom }) => {

+ 10 - 116
app/run-agent-flow/components/Header.tsx

@@ -2,7 +2,6 @@
 import { BsCaretDownFill } from "react-icons/bs";
 import { useSession, signIn, signOut } from "next-auth/react"
 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";
@@ -14,82 +13,12 @@ import { BsCloudDownload } from "react-icons/bs";
 export default function Header() {
   const { data: session, status } = useSession()
 
-  const [org, setOrg] = useState('')
-  const [username, setUsername] = useState('')
-  const [password, setPassword] = useState('')
-  const [isLogInFail, setIsLogInFail] = useState(false)
-
-  const orgQuery = trpc.org.bySlug.useQuery({ mode: org })
-
   const logOut = async () => {
-    const res = await fetch(" beta.api.cocorobo.cn/api/logout", {
-      method: "GET",
-      headers: {
-        Origin: "https://edu.cocorobo.cn"
-      },
+    const res = await fetch("https://beta.api.cocorobo.cn/api/logout", {
+      method: "POST",
     });
     await signOut({ redirect: false })
   }
-  const logIn = async () => {
-    const loginUsername = orgQuery.data?.mail ? `${username}@${orgQuery.data?.mail}` : `${username}@cocorobo.cc`
-    const loginPassword = btoa(password)
-    const res = await signIn('credentials', { redirect: false, loginUsername, loginPassword })
-    console.log(res)
-    if (!res.ok) {
-      setIsLogInFail(true)
-    }
-  }
-
-  // useEffect(() => {
-  //   setIsLogInFail(false)
-  // }, [username, password, org])
-
-
-  /*
-  用户登陆判断
-*/
-  useEffect(() => {
-    const checkLoginStatus = async (intervalId) => {
-      // 检查名为 'authToken' 的 cookie 是否存在
-      const authToken = Cookies.get('cocorobo');
-      console.log(authToken)
-      const tf = authToken ? true : false;
-      if (status !== 'authenticated') {//tf &&
-        const cookie = await fetch("https://beta.api.cocorobo.cn/api/getcookieuserid", {
-          method: "GET",
-          credentials: 'include',
-        });
-        try {
-          const cookiejson = await cookie.json();
-          console.log(cookiejson);
-          const user = cookiejson?.[0]?.[0];
-          if (cookie.ok && user) {
-            const res = await signIn('credentials', { redirect: false, userid: user.userid })
-            setIsLogInFail(true);
-            clearInterval(intervalId);
-          }
-        }
-        catch (e) {
-          setIsLogInFail(false)
-        }
-      }
-      setIsLogInFail(authToken); // 如果存在 authToken,则用户已登录
-    };
-    const intervalId = setInterval(async () => {
-      await checkLoginStatus(intervalId);
-    }, 5000);
-    return () => {
-      clearInterval(intervalId)
-    }
-  }, []);
-
-  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 relative">
@@ -101,53 +30,18 @@ export default function Header() {
         </ul>
       </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 className="flex gap-2 items-center">
+              <p>Hi, </p>
+              <div className="dropdown dropdown-bottom dropdown-end">
+                <label tabIndex={0} className="btn btn-sm m-1">{session.user?.name}</label>
+                <ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
+                  <li><a onClick={logOut}>退出登录</a></li>
+                </ul>
               </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="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>
-            </>
-          )
+          ) : null
         }
       </div>
     </div >

+ 17 - 7
app/run-agent-flow/components/NodeRender.tsx

@@ -5,8 +5,9 @@ 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 { curNodeAtom, curStepAtom, arrowStateAtom, cardInstantAtomFamily, cardInstantAtomsAtom, instantDataAtom, stepsNodesAtom } from '../store';
 import { useAtom, useAtomValue } from 'jotai';
+import { exportFlowToDocx } from '../export';
 
 const NodeRender = () => {
   const [curStep, setCurStep] = useAtom(curStepAtom)
@@ -36,7 +37,6 @@ const NodeRender = () => {
     }
   }, [cardInstantAtom])
 
-
   const onNextStep = () => {
     setCurStep(prev => prev + 1)
   }
@@ -45,6 +45,14 @@ const NodeRender = () => {
     setCurStep(prev => prev - 1)
   }
 
+  const instantData = useAtomValue(instantDataAtom)
+  const stepsNodes = useAtomValue(stepsNodesAtom)
+
+  const onExport = () => {
+    const stepsInstantData = stepsNodes.map(node => instantData[node.id])
+    exportFlowToDocx(stepsInstantData)
+  }
+
   return (
     <div className="card card-compact shadow-xl flex-1 overflow-hidden">
       <div className="card-body overflow-hidden">
@@ -52,15 +60,17 @@ const NodeRender = () => {
           <h2 className="flex-1">
             {curStep + 1}: {nodeName}
           </h2>
-          <button className='btn btn-sm' disabled={!arrowState.prev} onClick={onPrevStep}>上一步</button>
-          <button className='btn btn-sm' disabled={!arrowState.next} onClick={onNextStep}>下一步</button>
+          {arrowState.prev &&
+            <button className='btn btn-sm' onClick={onPrevStep}>上一步</button>
+          }
+          {arrowState.next
+            ? <button className='btn btn-sm' onClick={onNextStep}>下一步</button>
+            : <button className='btn btn-sm btn-neutral' onClick={onExport}>导出</button>
+          }
         </div>
         <div className="flex-1 flex flex-col items-stretch overflow-auto">
           <Comp key={node?.id} node={node} cardInstantAtom={cardInstantAtom}></Comp>
         </div>
-        {/* <div className="card-actions justify-end">
-          <button className="btn btn-primary">确认,下一步</button>
-        </div> */}
       </div>
     </div>
   )

+ 0 - 11
components/providers/SessionProvider.tsx

@@ -1,11 +0,0 @@
-'use client'
-import { useSession, signIn, signOut, SessionProvider as _SessionProvider } from "next-auth/react"
-import { type ReactNode } from "react"
-
-export function SessionProvider({ children }: { children: ReactNode }) {
-  return (
-    <_SessionProvider>
-      {children}
-    </_SessionProvider>
-  )
-}

+ 6 - 7
lib/utils.ts

@@ -63,14 +63,13 @@ export function getChatResponse({
     };
 
     // 处理错误事件
-    eventSource.onerror = () => {
-      done = true;
+    eventSource.onerror = (e) => {
+      error = e
       resolveQueuePromise();
       eventSource.close(); // 关闭连接
     };
 
     ctrl.signal.addEventListener("abort", () => {
-      done = true;
       resolveQueuePromise();
       eventSource.close();
     });
@@ -86,15 +85,15 @@ export function getChatResponse({
     );
 
     while (!done || queue.length > 0) {
+      clearTimeout(timer);
       if (error) {
         throw error;
       }
-      clearTimeout(timer);
+      // every looping stream, we wait for 10s
       timer = setTimeout(() => {
-        ctrl.abort();
         error = new Error("SSE timeout aborted.");
-        resolveQueuePromise();
-      }, 30000);
+        ctrl.abort();
+      }, 10000);
       if (queue.length > 0) {
         yield queue.shift();
       } else {

+ 1 - 1
tailwind.config.ts

@@ -16,7 +16,7 @@ const config: Config = {
     utils: true,
     prefix: "",
     logs: true,
-    themes: ["corporate"],
+    themes: ["light"],
     themeRoot: ":root",
   },
   plugins: [require("daisyui"), require("@tailwindcss/typography")],