From e4bd64486ba4daa2525d6311ff25d5dc04fdce56 Mon Sep 17 00:00:00 2001 From: Yixiang Chen Date: Wed, 16 Apr 2025 16:46:23 -0400 Subject: [PATCH 1/2] autogenerate materials --- app/api/chat/route.ts | 19 +- app/chat/[classroomId]/AutogenerateButton.tsx | 36 ++++ .../GeneratedMaterialsSidebar.tsx | 146 +++++++++++++++ app/chat/[classroomId]/MessageBox.tsx | 35 +--- app/chat/[classroomId]/actions.ts | 79 ++++++-- app/chat/[classroomId]/page.tsx | 50 +++-- app/classroom/actions.ts | 13 ++ components/ui/dialog.tsx | 122 ++++++++++++ package.json | 1 + pnpm-lock.yaml | 173 ++++++++++++++++++ 10 files changed, 579 insertions(+), 95 deletions(-) create mode 100644 app/chat/[classroomId]/AutogenerateButton.tsx create mode 100644 app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx create mode 100644 components/ui/dialog.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 7e81815..676595c 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,6 +1,6 @@ import { type NextRequest } from "next/server"; -const API_URL = process.env.RAGFLOW_API_URL + "/api" || ""; +const API_URL = `${process.env.RAGFLOW_API_URL}/api` || ""; const API_KEY = process.env.RAGFLOW_API_KEY; export async function GET(request: NextRequest) { @@ -17,7 +17,6 @@ export async function GET(request: NextRequest) { "Content-Type": "application/json", }, }); - // const resJson = await res.json(); return Response.json({ success: 200 }); case "delete_sessions": await fetch(`${API_URL}/v1/chats/${assistant}/sessions`, { @@ -49,19 +48,7 @@ export async function GET(request: NextRequest) { } ); return Response.json((await sessions.json()).data); + default: + return Response.json({ error: "Invalid method" }, { status: 400 }); } - - // const res = await fetch( - // `${API_URL}/v1/chats/`, - // { - // method: "DELETE", - // headers: { - // Authorization: `Bearer ${API_KEY}`, - // "Content-Type": "application/json", - // }, - // } - // ); - // const resJson = await res.json(); - - // return Response.json(resJson.data); } diff --git a/app/chat/[classroomId]/AutogenerateButton.tsx b/app/chat/[classroomId]/AutogenerateButton.tsx new file mode 100644 index 0000000..2342e80 --- /dev/null +++ b/app/chat/[classroomId]/AutogenerateButton.tsx @@ -0,0 +1,36 @@ +"use client"; +import { useTransition } from "react"; +import { autogenerateMaterial } from "./actions"; + +type AutogenerateButtonProps = { + classroomId: number; +}; + +export default function AutogenerateButton({ + classroomId, +}: AutogenerateButtonProps) { + const [isPending, startTransition] = useTransition(); + + async function handleClick() { + startTransition(async () => { + try { + const result = await autogenerateMaterial(classroomId); + console.log("Generated material:", result); + // After autogeneration, reload the page to update generated content. + window.location.reload(); + } catch (error) { + console.error("Error during autogeneration:", error); + } + }); + } + + return ( + + ); +} diff --git a/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx b/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx new file mode 100644 index 0000000..b36abf5 --- /dev/null +++ b/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogClose, +} from "@/components/ui/dialog"; // adjust path per your project + +type Session = { + id: string; + name: string; + messages: { role: string; content: string }[]; +}; + +interface GeneratedMaterialsSidebarProps { + assistantId: string; + realUserId: string; +} + +export default function GeneratedMaterialsSidebar({ + assistantId, +}: GeneratedMaterialsSidebarProps) { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [sessions, setSessions] = useState([]); + const [selectedSession, setSelectedSession] = useState(null); + + useEffect(() => { + async function fetchSessions() { + try { + // Use your existing API route to get sessions. + const res = await fetch( + `/api/chat?method=get_sessions&id=${assistantId}` + ); + if (!res.ok) { + const errorText = await res.text(); + throw new Error( + `Failed to fetch sessions: ${res.status} ${errorText}` + ); + } + const data = await res.json(); + setSessions(data); + } catch (error) { + console.error("Error fetching sessions:", error); + } + } + if (isSidebarOpen) { + fetchSessions(); + } + }, [isSidebarOpen, assistantId]); + + return ( + <> + {/* Toggle the sidebar on button click */} + + + {/* Sidebar Panel – always rendered for animation */} +
+
+

Chat Sessions

+ +
+ {sessions.length === 0 ? ( +

No sessions found.

+ ) : ( +
+ {/* Wrap the ul in a div to avoid nesting error */} +
+
    + {sessions.map((session) => ( +
  • + +
  • + ))} +
+
+
+ )} +
+ + {/* Modal for Session Details – slide in from the right */} + setSelectedSession(null)} + > +
+ + + + {selectedSession?.name || "Session Details"} + + + {selectedSession?.messages.length ? ( +
+
    + {selectedSession.messages.map((msg, index) => ( +
  • + {msg.role}: {msg.content} +
  • + ))} +
+
+ ) : ( +

No messages in this session.

+ )} +
+
+
+ + + +
+
+
+
+ + ); +} diff --git a/app/chat/[classroomId]/MessageBox.tsx b/app/chat/[classroomId]/MessageBox.tsx index 60fdabc..666abdd 100644 --- a/app/chat/[classroomId]/MessageBox.tsx +++ b/app/chat/[classroomId]/MessageBox.tsx @@ -8,43 +8,32 @@ function MessageBox(props: { messageHistory: RagFlowMessages | null; }) { const [value, setValue] = useState(""); - const [messages, setMessage] = useState( props.messageHistory ? props.messageHistory : [] ); async function handle() { const ownMessage = { role: "user", content: value }; - setMessage((oldArray) => [...oldArray, ownMessage]); - setValue(""); - const response: string = await sendMessage( value, props.assistantId, props.chatSessionId ); - - // alert(response); - // alert("can we even print anything"); - const messageData = { role: "assistant", content: response }; - setMessage((oldArray) => [...oldArray, messageData]); - // console.log("response thingy2", messages); } return (

Chat:

-
{messages.map((aMessage, idx) => (
))}
- {/*
*/}
- - {/* { - setValue(e.target.value); - }} - /> - - */}
); diff --git a/app/chat/[classroomId]/actions.ts b/app/chat/[classroomId]/actions.ts index 9a399f3..747064f 100644 --- a/app/chat/[classroomId]/actions.ts +++ b/app/chat/[classroomId]/actions.ts @@ -189,36 +189,44 @@ async function createChatAssistant( } export async function getOrCreateSession( - userID: string, + ragflowUserId: string, // “generate_” chatAssistantId: string, - classroomId: ClassroomId + classroomId: number, + localUserId: string // the real Supabase user UUID ) { - const existingSession = await findSessionID(classroomId, userID); + // Check local DB for a session using the REAL userId + const existingSession = await findSessionID(classroomId, localUserId); console.log("Found an existing session:", existingSession); if (existingSession) { return existingSession; } - - return await createSession(chatAssistantId, userID, classroomId); + return await createSession( + chatAssistantId, + ragflowUserId, + classroomId, + localUserId + ); } -async function findSessionID(classroomId: ClassroomId, userID: string) { +// Now findSessionID uses localUserId to match the user’s row. +async function findSessionID(classroomId: number, localUserId: string) { try { - // find it from the supabase const supabase = await createClient(); - const sessionID = await supabase .from("Classroom_Members") .select("ragflow_session_id") .eq("classroom_id", classroomId) - .eq("user_id", userID); + .eq("user_id", localUserId); if (sessionID.error) { console.error("Error fetching session:", sessionID.error); return null; } - return sessionID.data[0].ragflow_session_id; // Return the first session if available + if (!sessionID.data || sessionID.data.length === 0) { + return null; + } + return sessionID.data[0].ragflow_session_id; } catch (error) { console.error("Error fetching session:", error); return null; @@ -227,16 +235,18 @@ async function findSessionID(classroomId: ClassroomId, userID: string) { async function createSession( assistantID: string, - userID: string, - classroomId: ClassroomId + ragflowUserId: string, + classroomId: number, + localUserId: string ) { const newSession = { assistant_id: assistantID, - user_id: userID, - name: `Session_${userID}`, + user_id: ragflowUserId, + name: `Session_${ragflowUserId}`, }; try { + // 1) Create session in Ragflow const res = await fetch(`${API_URL}/v1/chats/${assistantID}/sessions`, { method: "POST", headers: { @@ -246,21 +256,20 @@ async function createSession( body: JSON.stringify(newSession), }); - if (!res.ok) throw new Error("Failed to create session"); - + if (!res.ok) throw new Error("Failed to create session in Ragflow"); const resJson = await res.json(); - // update that in supabase + // 2) Save that session ID in your local DB with the REAL user’s UUID const supabase = createServiceClient(); - const supabaseRes = await supabase .from("Classroom_Members") .update({ ragflow_session_id: resJson.data.id }) .eq("classroom_id", classroomId) - .eq("user_id", userID) - .select(); + .eq("user_id", localUserId); if (supabaseRes.error) { + console.error("Supabase error message:", supabaseRes.error.message); + console.error("Supabase error details:", supabaseRes.error.details); throw new Error(`Failed to update classroom: ${supabaseRes.error}`); } @@ -382,3 +391,33 @@ export async function retrieveMessageHistory( return null; } } + +export async function autogenerateMaterial(classroomId: number) { + // Real user UUID from Supabase + const realUserId = await getCurrentUserId(); + + // “generate_...” string for Ragflow’s user_id + const ragflowUserId = "generate_" + realUserId; + + const datasetId = await getRagflowDatasetId(classroomId); + const assistantResponse = await getOrCreateAssistant(classroomId, datasetId!); + if (!assistantResponse.id) { + throw new Error("Failed to get assistant"); + } + const assistantId = assistantResponse.id; + + const sessionId = await getOrCreateSession( + ragflowUserId, // for Ragflow + assistantId, + classroomId, + realUserId // for local DB + ); + + if (!sessionId) { + throw new Error("Failed to create session"); + } + const prompt = + "Generate sample multiple choice questions for me with answers please"; + const generatedMaterial = await sendMessage(prompt, assistantId, sessionId); + return generatedMaterial; +} diff --git a/app/chat/[classroomId]/page.tsx b/app/chat/[classroomId]/page.tsx index 6668e94..0acd01e 100644 --- a/app/chat/[classroomId]/page.tsx +++ b/app/chat/[classroomId]/page.tsx @@ -7,16 +7,17 @@ import { getDisplayInfo, retrieveMessageHistory, } from "./actions"; - import MessageBox from "./MessageBox"; +import AutogenerateButton from "./AutogenerateButton"; +import GeneratedMaterialsSidebar from "./GeneratedMaterialsSidebar"; export default async function ChatPage({ params, }: { - params: Promise<{ classroomId: string }>; + params: { classroomId: string }; }) { const userId = await getCurrentUserId(); - const { classroomId } = await params; + const { classroomId } = params; const classroomIdNum = Number(classroomId); const datasetId = await getRagflowDatasetId(classroomIdNum); @@ -24,26 +25,26 @@ export default async function ChatPage({ return

No dataset found!

; } - const chatAssistantId = await getOrCreateAssistant( - Number(classroomId), - datasetId - ); - if (chatAssistantId.status == "empty") { + const chatAssistantId = await getOrCreateAssistant(classroomIdNum, datasetId); + if (chatAssistantId.status === "empty") { return ( <>

Classroom dataset empty!

- ); } + + // For normal chat sessions, use the real userId for both Ragflow and local DB. const chatSessionId = await getOrCreateSession( userId, chatAssistantId.id, - classroomIdNum + classroomIdNum, + userId ); const messageHistory = await retrieveMessageHistory( @@ -51,36 +52,33 @@ export default async function ChatPage({ userId, chatSessionId ); - const displayInfo = await getDisplayInfo(classroomIdNum, userId); - // console.log("chatAssistant", chatAssistant); return (
- {/*

- Classroom ID: - {classroomId} -

- User ID: - {userId}

- Ragflow Dataset ID: {datasetId}

- Chat Assistant ID: {chatAssistantId}

- Chat Session ID: {chatSessionId} -

*/}

- Welcome to: - {displayInfo[0]}, {displayInfo[1]}
+ Welcome to: {displayInfo[0]},{" "} + {displayInfo[1]} +
Ragflow Dataset ID: {datasetId}
Chat Assistant ID: {chatAssistantId.id}
Chat Session ID: {chatSessionId} -

{" "} +

{chatAssistantId && chatSessionId && messageHistory && ( + /> )} +
+ +
+ {/* Include the sidebar to list all sessions for the current chat assistant */} +
); } diff --git a/app/classroom/actions.ts b/app/classroom/actions.ts index 04cfb52..d098eec 100644 --- a/app/classroom/actions.ts +++ b/app/classroom/actions.ts @@ -283,3 +283,16 @@ export async function unarchiveClassroom(classroom_id: number) { return { success: true, data }; } + +export async function insertRandom() { + // Notice how we use a createServiceClient instead of createClient from server + // this BYPASSES ALL RLS in the case that you have to do some more complex things and we don't + // want to write RLS rules for all of it. See our project doc Resources section for more info + const supabase = createServiceClient(); + + const { error } = await supabase.from("Classroom_Members").insert({ + classroom_id: 17, + user_id: "05929f55-42bb-42d4-86bd-ddc0c7d12685", + }); + console.log(error); +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..399eb42 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/package.json b/package.json index d881e2b..4826f48 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "format:check": "prettier --check ." }, "dependencies": { + "@headlessui/react": "^2.2.0", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 783a6b1..1a43b6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@headlessui/react': + specifier: ^2.2.0 + version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-avatar': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -443,6 +446,34 @@ packages: resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@headlessui/react@2.2.0': + resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -914,6 +945,43 @@ packages: '@types/react-dom': optional: true + '@react-aria/focus@3.20.1': + resolution: {integrity: sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.24.1': + resolution: {integrity: sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.7': + resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.28.1': + resolution: {integrity: sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/flags@3.1.0': + resolution: {integrity: sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==} + + '@react-stately/utils@3.10.5': + resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.28.0': + resolution: {integrity: sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@rollup/rollup-android-arm-eabi@4.34.8': resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} cpu: [arm] @@ -1055,10 +1123,19 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.6': + resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.2': resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.6': + resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -2984,6 +3061,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwind-merge@3.0.2: resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} @@ -3628,6 +3708,40 @@ snapshots: '@eslint/core': 0.10.0 levn: 0.4.1 + '@floating-ui/core@1.6.9': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.6.13': + dependencies: + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/dom': 1.6.13 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@floating-ui/react@0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@floating-ui/utils': 0.2.9 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.9': {} + + '@headlessui/react@2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-aria/focus': 3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-virtual': 3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4010,6 +4124,55 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@react-aria/focus@3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-types/shared': 3.28.0(react@19.0.0) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@react-aria/interactions@3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-aria/ssr': 3.9.7(react@19.0.0) + '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-stately/flags': 3.1.0 + '@react-types/shared': 3.28.0(react@19.0.0) + '@swc/helpers': 0.5.15 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@react-aria/ssr@3.9.7(react@19.0.0)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0 + + '@react-aria/utils@3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-aria/ssr': 3.9.7(react@19.0.0) + '@react-stately/flags': 3.1.0 + '@react-stately/utils': 3.10.5(react@19.0.0) + '@react-types/shared': 3.28.0(react@19.0.0) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@react-stately/flags@3.1.0': + dependencies: + '@swc/helpers': 0.5.15 + + '@react-stately/utils@3.10.5(react@19.0.0)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0 + + '@react-types/shared@3.28.0(react@19.0.0)': + dependencies: + react: 19.0.0 + '@rollup/rollup-android-arm-eabi@4.34.8': optional: true @@ -4131,8 +4294,16 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@tanstack/react-virtual@3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/virtual-core': 3.13.6 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@tanstack/table-core@8.21.2': {} + '@tanstack/virtual-core@3.13.6': {} + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 @@ -6325,6 +6496,8 @@ snapshots: symbol-tree@3.2.4: {} + tabbable@6.2.0: {} + tailwind-merge@3.0.2: {} tailwindcss-animate@1.0.7(tailwindcss@3.4.17): From 24656b7a42c269e371bb7f998e93d547495b1d2a Mon Sep 17 00:00:00 2001 From: Yixiang Chen Date: Wed, 16 Apr 2025 17:08:59 -0400 Subject: [PATCH 2/2] updates --- .env.example | 2 +- .prettierrc.json | 3 +- .tekton/events/trigger_binding.yaml | 2 +- .vscode/settings.json | 2 + app/{ => api}/auth/callback/route.ts | 8 +- .../[datasetId]/[documentId]/route.ts | 40 - .../GeneratedMaterialsSidebar.tsx | 2 +- app/chat/[classroomId]/actions.ts | 4 +- .../components/delete-chatroom-button.tsx | 47 + .../components/invite-chatroom-dialog.tsx | 239 +++ .../components/leave-chatroom-button.tsx | 18 +- .../[chatroomId]/components/message-area.tsx | 363 ++++ .../[chatroomId]/components/new-messages.tsx | 149 -- app/chatrooms/[chatroomId]/page.tsx | 99 +- app/chatrooms/actions.ts | 362 ++-- app/chatrooms/components/invite-form.tsx | 106 - app/chatrooms/config.ts | 7 + app/chatrooms/helpers.ts | 338 ---- app/chatrooms/invite/page.tsx | 73 - app/chatrooms/page.tsx | 153 -- .../[classroomId]/upload/preview/page.tsx | 17 - .../[classroomId]/upload/uploadComponent.tsx | 187 -- app/classroom/actions.ts | 298 --- app/classroom/classroomList.tsx | 312 --- app/classroom/memberList.tsx | 49 - app/classroom/new/actions.ts | 79 - app/classroom/new/page.tsx | 73 - app/classroom/newClassroomButton.tsx | 15 - app/classroom/page.tsx | 25 - .../[classroomId]/augment/AugmentNotes.tsx | 83 + app/classrooms/[classroomId]/augment/page.tsx | 31 + .../[classroomId]/chat/MessageBox.tsx | 135 ++ app/classrooms/[classroomId]/chat/page.tsx | 125 ++ .../_components/create-chatroom-dialog.tsx | 75 + .../[classroomId]/chatrooms/page.tsx | 144 ++ .../manage/_components}/inviteMember.tsx | 18 +- .../[classroomId]/manage/buttons.tsx | 185 ++ app/classrooms/[classroomId]/manage/page.tsx | 83 + .../[classroomId]/upload/actions.ts | 6 +- .../[classroomId]/upload/page.tsx | 12 +- .../[classroomId]/upload/preview/page.tsx | 42 + .../[classroomId]/upload/uploadComponent.tsx | 222 ++ .../_components}/columns.tsx | 8 +- app/classrooms/_components/invite-dialog.tsx | 110 + app/classrooms/_components/join-dialog.tsx | 95 + app/classrooms/_components/memberList.tsx | 138 ++ .../_components/saveClassroomDialog.tsx | 97 + app/classrooms/actions.ts | 260 +++ app/classrooms/clientUtils.tsx | 82 + .../join/[code]/route.ts | 32 +- app/classrooms/page.tsx | 496 +++++ app/{auth => error}/unauthorized/page.tsx | 12 +- app/favicon.ico | Bin 25931 -> 9854 bytes app/globals.css | 117 +- app/layout.tsx | 76 +- app/lib/utils.ts | 6 - app/{auth => login}/actions.ts | 4 +- app/login/page.tsx | 38 +- app/page.tsx | 61 +- components.json | 10 +- components/ui/toast.tsx | 129 -- components/ui/toaster.tsx | 35 - eslint.config.mjs | 3 + hooks/use-toast.ts | 192 -- middleware.ts | 2 +- next.config.ts | 5 + package.json | 17 +- pnpm-lock.yaml | 1802 ++++++++++++++--- public/logo.svg | 12 + .../components}/GoogleSignInButton.tsx | 0 shared/components/Logo.tsx | 18 + shared/components/ui/accordion.tsx | 65 + shared/components/ui/alert-dialog.tsx | 141 ++ .../components}/ui/avatar.tsx | 2 +- shared/components/ui/badge.tsx | 36 + shared/components/ui/button.tsx | 134 ++ shared/components/ui/card.tsx | 122 ++ shared/components/ui/chat/chat-bubble.tsx | 223 ++ shared/components/ui/chat/chat-input.tsx | 43 + .../components/ui/chat/chat-message-list.tsx | 59 + shared/components/ui/chat/expandable-chat.tsx | 153 ++ .../ui/chat/hooks/useAutoScroll.tsx | 135 ++ shared/components/ui/chat/message-loading.tsx | 45 + shared/components/ui/collapsible.tsx | 11 + shared/components/ui/command.tsx | 153 ++ .../components}/ui/data-table.tsx | 3 +- .../components}/ui/dialog.tsx | 2 +- shared/components/ui/dropdown-menu.tsx | 200 ++ shared/components/ui/input-otp.tsx | 81 + .../components}/ui/input.tsx | 2 +- shared/components/ui/label.tsx | 26 + .../components}/ui/scroll-area.tsx | 8 +- shared/components/ui/separator.tsx | 31 + .../components}/ui/sheet.tsx | 2 +- shared/components/ui/sidebar.tsx | 774 +++++++ shared/components/ui/sidebar/app-sidebar.tsx | 339 ++++ shared/components/ui/sidebar/back-button.tsx | 22 + shared/components/ui/sidebar/mode-toggle.tsx | 136 ++ shared/components/ui/sidebar/nav-utils.ts | 55 + .../components}/ui/skeleton.tsx | 2 +- shared/components/ui/sonner.tsx | 31 + .../components}/ui/table.tsx | 2 +- shared/components/ui/tabs.tsx | 55 + shared/components/ui/textarea.tsx | 22 + shared/components/ui/theme-provider.tsx | 17 + shared/components/ui/tooltip.tsx | 30 + shared/hooks/use-mobile.tsx | 21 + shared/lib/ragflow/chat/chat-client.ts | 908 +++++++++ shared/lib/ragflow/chat/chat-configs.ts | 102 + {app => shared}/lib/ragflow/dataset-client.ts | 108 +- shared/lib/supabase/shared.ts | 13 + shared/lib/userContext/contextFetcher.ts | 59 + shared/lib/userContext/userContext.tsx | 58 + {lib => shared/lib}/utils.ts | 0 {utils => shared/utils}/supabase/client.ts | 0 .../utils}/supabase/database.types.ts | 98 +- .../utils}/supabase/middleware.ts | 2 +- {utils => shared/utils}/supabase/server.ts | 0 .../utils}/supabase/service-server.ts | 0 tailwind.config.ts | 139 +- tsconfig.json | 3 +- 121 files changed, 9810 insertions(+), 2953 deletions(-) rename app/{ => api}/auth/callback/route.ts (81%) delete mode 100644 app/api/document/[datasetId]/[documentId]/route.ts create mode 100644 app/chatrooms/[chatroomId]/components/delete-chatroom-button.tsx create mode 100644 app/chatrooms/[chatroomId]/components/invite-chatroom-dialog.tsx create mode 100644 app/chatrooms/[chatroomId]/components/message-area.tsx delete mode 100644 app/chatrooms/[chatroomId]/components/new-messages.tsx delete mode 100644 app/chatrooms/components/invite-form.tsx create mode 100644 app/chatrooms/config.ts delete mode 100644 app/chatrooms/helpers.ts delete mode 100644 app/chatrooms/invite/page.tsx delete mode 100644 app/chatrooms/page.tsx delete mode 100644 app/classroom/[classroomId]/upload/preview/page.tsx delete mode 100644 app/classroom/[classroomId]/upload/uploadComponent.tsx delete mode 100644 app/classroom/actions.ts delete mode 100644 app/classroom/classroomList.tsx delete mode 100644 app/classroom/memberList.tsx delete mode 100644 app/classroom/new/actions.ts delete mode 100644 app/classroom/new/page.tsx delete mode 100644 app/classroom/newClassroomButton.tsx delete mode 100644 app/classroom/page.tsx create mode 100644 app/classrooms/[classroomId]/augment/AugmentNotes.tsx create mode 100644 app/classrooms/[classroomId]/augment/page.tsx create mode 100644 app/classrooms/[classroomId]/chat/MessageBox.tsx create mode 100644 app/classrooms/[classroomId]/chat/page.tsx create mode 100644 app/classrooms/[classroomId]/chatrooms/_components/create-chatroom-dialog.tsx create mode 100644 app/classrooms/[classroomId]/chatrooms/page.tsx rename app/{classroom => classrooms/[classroomId]/manage/_components}/inviteMember.tsx (75%) create mode 100644 app/classrooms/[classroomId]/manage/buttons.tsx create mode 100644 app/classrooms/[classroomId]/manage/page.tsx rename app/{classroom => classrooms}/[classroomId]/upload/actions.ts (97%) rename app/{classroom => classrooms}/[classroomId]/upload/page.tsx (80%) create mode 100644 app/classrooms/[classroomId]/upload/preview/page.tsx create mode 100644 app/classrooms/[classroomId]/upload/uploadComponent.tsx rename app/{classroom => classrooms/_components}/columns.tsx (83%) create mode 100644 app/classrooms/_components/invite-dialog.tsx create mode 100644 app/classrooms/_components/join-dialog.tsx create mode 100644 app/classrooms/_components/memberList.tsx create mode 100644 app/classrooms/_components/saveClassroomDialog.tsx create mode 100644 app/classrooms/actions.ts create mode 100644 app/classrooms/clientUtils.tsx rename app/{classroom => classrooms}/join/[code]/route.ts (62%) create mode 100644 app/classrooms/page.tsx rename app/{auth => error}/unauthorized/page.tsx (61%) delete mode 100644 app/lib/utils.ts rename app/{auth => login}/actions.ts (86%) delete mode 100644 components/ui/toast.tsx delete mode 100644 components/ui/toaster.tsx delete mode 100644 hooks/use-toast.ts create mode 100644 public/logo.svg rename {components => shared/components}/GoogleSignInButton.tsx (100%) create mode 100644 shared/components/Logo.tsx create mode 100644 shared/components/ui/accordion.tsx create mode 100644 shared/components/ui/alert-dialog.tsx rename {components => shared/components}/ui/avatar.tsx (97%) create mode 100644 shared/components/ui/badge.tsx create mode 100644 shared/components/ui/button.tsx create mode 100644 shared/components/ui/card.tsx create mode 100644 shared/components/ui/chat/chat-bubble.tsx create mode 100644 shared/components/ui/chat/chat-input.tsx create mode 100644 shared/components/ui/chat/chat-message-list.tsx create mode 100644 shared/components/ui/chat/expandable-chat.tsx create mode 100644 shared/components/ui/chat/hooks/useAutoScroll.tsx create mode 100644 shared/components/ui/chat/message-loading.tsx create mode 100644 shared/components/ui/collapsible.tsx create mode 100644 shared/components/ui/command.tsx rename {components => shared/components}/ui/data-table.tsx (96%) rename {components => shared/components}/ui/dialog.tsx (98%) create mode 100644 shared/components/ui/dropdown-menu.tsx create mode 100644 shared/components/ui/input-otp.tsx rename {components => shared/components}/ui/input.tsx (95%) create mode 100644 shared/components/ui/label.tsx rename {components => shared/components}/ui/scroll-area.tsx (91%) create mode 100644 shared/components/ui/separator.tsx rename {components => shared/components}/ui/sheet.tsx (99%) create mode 100644 shared/components/ui/sidebar.tsx create mode 100644 shared/components/ui/sidebar/app-sidebar.tsx create mode 100644 shared/components/ui/sidebar/back-button.tsx create mode 100644 shared/components/ui/sidebar/mode-toggle.tsx create mode 100644 shared/components/ui/sidebar/nav-utils.ts rename {components => shared/components}/ui/skeleton.tsx (85%) create mode 100644 shared/components/ui/sonner.tsx rename {components => shared/components}/ui/table.tsx (98%) create mode 100644 shared/components/ui/tabs.tsx create mode 100644 shared/components/ui/textarea.tsx create mode 100644 shared/components/ui/theme-provider.tsx create mode 100644 shared/components/ui/tooltip.tsx create mode 100644 shared/hooks/use-mobile.tsx create mode 100644 shared/lib/ragflow/chat/chat-client.ts create mode 100644 shared/lib/ragflow/chat/chat-configs.ts rename {app => shared}/lib/ragflow/dataset-client.ts (82%) create mode 100644 shared/lib/supabase/shared.ts create mode 100644 shared/lib/userContext/contextFetcher.ts create mode 100644 shared/lib/userContext/userContext.tsx rename {lib => shared/lib}/utils.ts (100%) rename {utils => shared/utils}/supabase/client.ts (100%) rename {utils => shared/utils}/supabase/database.types.ts (75%) rename {utils => shared/utils}/supabase/middleware.ts (97%) rename {utils => shared/utils}/supabase/server.ts (100%) rename {utils => shared/utils}/supabase/service-server.ts (100%) diff --git a/.env.example b/.env.example index f077752..935732f 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,4 @@ NEXT_PUBLIC_HOSTED_DOMAIN="nyu.edu" SUPABASE_SERVICE_ROLE_KEY= RAGFLOW_API_KEY= -RAGFLOW_API_URL= +RAGFLOW_API_URL= \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index b5c6918..909c770 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,6 @@ "tabWidth": 2, "semi": true, "singleQuote": false, - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-tailwindcss"], + "endOfLine": "auto" } diff --git a/.tekton/events/trigger_binding.yaml b/.tekton/events/trigger_binding.yaml index bea878d..ebee9da 100644 --- a/.tekton/events/trigger_binding.yaml +++ b/.tekton/events/trigger_binding.yaml @@ -5,7 +5,7 @@ metadata: spec: params: - name: git-repo-url - value: $(body.repository.url) + value: $(body.repository.clone_url) - name: git-repo-name value: $(body.repository.name) - name: git-revision diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e7a819..ac31ce1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,10 @@ { "cSpell.words": [ "bitnami", + "Chatrooms", "pipelinerun", "Ragflow", + "Sonner", "supabase", "taskrun", "tekton", diff --git a/app/auth/callback/route.ts b/app/api/auth/callback/route.ts similarity index 81% rename from app/auth/callback/route.ts rename to app/api/auth/callback/route.ts index 48ee6ff..d1eba2e 100644 --- a/app/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { createClient } from "@/utils/supabase/server"; +import { createClient } from "@shared/utils/supabase/server"; export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); @@ -8,10 +8,8 @@ export async function GET(request: Request) { const next = searchParams.get("next") ?? "/"; const error = searchParams.get("error_description"); - if (error && error === "Database error saving new user") { - return NextResponse.redirect( - `${origin}/auth/unauthorized?message=ORGANIZATION_EMAIL_REQUIRED` - ); + if (error) { + return NextResponse.redirect(`${origin}/error/unauthorized`); } if (code) { diff --git a/app/api/document/[datasetId]/[documentId]/route.ts b/app/api/document/[datasetId]/[documentId]/route.ts deleted file mode 100644 index 5a72436..0000000 --- a/app/api/document/[datasetId]/[documentId]/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const RAGFLOW_API_KEY: string = process.env.RAGFLOW_API_KEY || ""; -const RAGFLOW_SERVER_URL: string = process.env.RAGFLOW_API_URL || ""; - -export async function GET( - _: NextRequest, - { params }: { params: Promise<{ datasetId: string; documentId: string }> } -) { - const { datasetId, documentId } = await params; - - const response = await fetch( - `${RAGFLOW_SERVER_URL}/api/v1/datasets/${datasetId}/documents/${documentId}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${RAGFLOW_API_KEY}`, - }, - } - ); - // return response; - if (!response.ok) { - return NextResponse.json( - { error: "RAGFlow server error" }, - { status: response.status } - ); - } - - const data = await response.arrayBuffer(); - // TODO: Right now we are hard-coding a pdf file type. Unfortunately the RAGFlow API only returns - // Content-Type: 'application/octet-stream'. We will probably have to regex find the file name/extension - // and map it to its MIME type (like 'application/pdf') - // List supported by RAGFlow: PDF, DOC, DOCX, TXT, MD, CSV, XLSX, XLS, JPEG, JPG, PNG, TIF, GIF, PPT, PPTX - return new NextResponse(data, { - headers: { - "Content-Type": "application/pdf", - "Content-Disposition": "inline", - }, - }); -} diff --git a/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx b/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx index b36abf5..53b4884 100644 --- a/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx +++ b/app/chat/[classroomId]/GeneratedMaterialsSidebar.tsx @@ -8,7 +8,7 @@ import { DialogTitle, DialogDescription, DialogClose, -} from "@/components/ui/dialog"; // adjust path per your project +} from "@/shared/components/ui/dialog"; // adjust path per your project type Session = { id: string; diff --git a/app/chat/[classroomId]/actions.ts b/app/chat/[classroomId]/actions.ts index 747064f..5e706c7 100644 --- a/app/chat/[classroomId]/actions.ts +++ b/app/chat/[classroomId]/actions.ts @@ -1,7 +1,7 @@ "use server"; -import { createClient } from "@/utils/supabase/server"; -import { createServiceClient } from "@/utils/supabase/service-server"; +import { createClient } from "@/shared/utils/supabase/server"; +import { createServiceClient } from "@/shared/utils/supabase/service-server"; export type RagFlowMessage = { content: string; diff --git a/app/chatrooms/[chatroomId]/components/delete-chatroom-button.tsx b/app/chatrooms/[chatroomId]/components/delete-chatroom-button.tsx new file mode 100644 index 0000000..585e5a5 --- /dev/null +++ b/app/chatrooms/[chatroomId]/components/delete-chatroom-button.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { deleteChatroom } from "@/app/chatrooms/actions"; +import { Button } from "@/shared/components/ui/button"; +import { Trash2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export default function DeleteChatroomButton({ + classroomId, + chatroomId, + assistantId, +}: { + classroomId: number; + chatroomId: string; + assistantId: string | null; +}) { + const [isDeleting, setIsDeleting] = useState(false); + const router = useRouter(); + + const handleDelete = async () => { + if (confirm("Are you sure you want to delete this chatroom?")) { + setIsDeleting(true); + try { + await deleteChatroom(chatroomId, assistantId); + router.push(`/classrooms/${classroomId}/chatrooms`); + } catch (error) { + console.error("Error deleting chatroom:", error); + alert("Failed to delete chatroom. Please try again."); + } finally { + setIsDeleting(false); + } + } + }; + + return ( + + ); +} diff --git a/app/chatrooms/[chatroomId]/components/invite-chatroom-dialog.tsx b/app/chatrooms/[chatroomId]/components/invite-chatroom-dialog.tsx new file mode 100644 index 0000000..ce80b1f --- /dev/null +++ b/app/chatrooms/[chatroomId]/components/invite-chatroom-dialog.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { Check, Plus } from "lucide-react"; + +import { Button } from "@shared/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@shared/components/ui/command"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@shared/components/ui/dialog"; +import { useEffect, useState } from "react"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/shared/components/ui/avatar"; +import { createClient } from "@/shared/utils/supabase/client"; +import { toast } from "sonner"; +import { inviteUserToChatroom } from "../../actions"; + +interface ChatroomMembers { + chatroom_id: string; + created_at: string; + id: number; + is_active: boolean; + member_id: number; + Classroom_Members: { + id: number; + user_id: string; + classroom_id: number; + Users: { + id: string; + full_name: string | null; + avatar_url: string | null; + }; + }; +} + +interface ClassroomMember { + id: number; + user_id: string; + classroom_id: number; + Users: { + email: string | null; + full_name: string | null; + avatar_url: string | null; + }; +} + +export function InviteChatroomButton({ + chatroomId, + classroomId, + chatroomMembers, +}: { + chatroomId: string; + classroomId: number; + chatroomMembers: ChatroomMembers[]; +}) { + const [open, setOpen] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + const [classroomInvitees, setClassroomInvitees] = useState( + [] + ); + + // get a list of Classroom_Members id + const currentMemberIds = chatroomMembers.map( + (member) => member.Classroom_Members.id + ); + + useEffect(() => { + async function fetchClassroomMembers() { + const supabase = createClient(); + + const { data, error } = await supabase + .from("Classroom_Members") + .select( + ` + id, + user_id, + classroom_id, + Users ( + email, + full_name, + avatar_url + ) + ` + ) + .eq("classroom_id", classroomId); + + if (error) { + console.error("Error fetching classroom members:", error); + } else { + setClassroomInvitees(data as ClassroomMember[]); + } + } + + if (open) { + fetchClassroomMembers(); + } + }, [classroomId, currentMemberIds, open]); + + const handleInviteUsers = async () => { + if (selectedUsers.length === 0) return; + + try { + const invitePromises = selectedUsers.map((invitee) => + inviteUserToChatroom(chatroomId, invitee.Users.email || "") + ); + + await Promise.all(invitePromises); + + setOpen(false); + setSelectedUsers([]); + toast.success("Successfully invited user(s) to chatroom"); + } catch (error) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + return ( + <> + + { + setOpen(isOpen); + if (!isOpen) { + setSelectedUsers([]); + } + }} + > + + + New chatroom Member + + Invite a user to this chatroom. You can only invite members of + current classroom. + + + + + + No members found. + + {classroomInvitees.map((user) => ( + { + if ( + selectedUsers.some( + (selected) => selected.id === user.id + ) + ) { + setSelectedUsers( + selectedUsers.filter( + (selectedUser) => selectedUser.id !== user.id + ) + ); + } else { + setSelectedUsers([...selectedUsers, user]); + } + }} + > + + + + {user.Users.full_name?.[0] || "U"} + + +
+

+ {user.Users.full_name || "Unknown"} +

+

+ {user.Users.email || "No email"} +

+
+ {selectedUsers.some( + (selected) => selected.id === user.id + ) ? ( + + ) : null} +
+ ))} +
+
+
+ + {selectedUsers.length > 0 ? ( +
+ {selectedUsers.map((user) => ( + + + {user.Users.full_name![0]} + + ))} +
+ ) : ( +

+ Select users to add to this thread. +

+ )} + +
+
+
+ + ); +} + +export default InviteChatroomButton; diff --git a/app/chatrooms/[chatroomId]/components/leave-chatroom-button.tsx b/app/chatrooms/[chatroomId]/components/leave-chatroom-button.tsx index 5f697fb..2268ab3 100644 --- a/app/chatrooms/[chatroomId]/components/leave-chatroom-button.tsx +++ b/app/chatrooms/[chatroomId]/components/leave-chatroom-button.tsx @@ -2,11 +2,15 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { leaveChatroom } from "../../actions"; +import { leaveChatroom } from "@/app/chatrooms/actions"; +import { Button } from "@/shared/components/ui/button"; +import { DoorOpen } from "lucide-react"; export default function LeaveChatroomButton({ + classroomId, chatroomId, }: { + classroomId: number; chatroomId: string; }) { const [isLeaving, setIsLeaving] = useState(false); @@ -17,7 +21,7 @@ export default function LeaveChatroomButton({ setIsLeaving(true); try { await leaveChatroom(chatroomId); - router.push("/chatrooms"); + router.push(`/classrooms/${classroomId}/chatrooms`); } catch (error) { console.error("Error leaving chatroom:", error); alert("Failed to leave chatroom. Please try again."); @@ -28,12 +32,14 @@ export default function LeaveChatroomButton({ }; return ( - + + {isLeaving ? "Leaving..." : "Leave"} + ); } diff --git a/app/chatrooms/[chatroomId]/components/message-area.tsx b/app/chatrooms/[chatroomId]/components/message-area.tsx new file mode 100644 index 0000000..7c17d4f --- /dev/null +++ b/app/chatrooms/[chatroomId]/components/message-area.tsx @@ -0,0 +1,363 @@ +"use client"; + +import { ChatClientWithSession } from "@shared/lib/ragflow/chat/chat-client"; +import { UserContext } from "@shared/lib/userContext/userContext"; +import { Skeleton } from "@shared/components/ui/skeleton"; +import { createClient } from "@shared/utils/supabase/client"; +import { Database, Tables } from "@shared/utils/supabase/database.types"; +import { useContext, useEffect, useState } from "react"; +import { askLLM } from "../../actions"; +import { createBrowserClient } from "@supabase/ssr"; +import config from "../../config"; +import { ChatMessageList } from "@/shared/components/ui/chat/chat-message-list"; +import { ChatInput } from "@/shared/components/ui/chat/chat-input"; +import { Button } from "@/shared/components/ui/button"; +import { SendIcon } from "lucide-react"; +import { + AIAvatar, + ChatBubble, + ChatBubbleAvatar, + ChatBubbleMessage, +} from "@/shared/components/ui/chat/chat-bubble"; +import ReactMarkdown from "react-markdown"; +import { toast } from "sonner"; +import Logo from "@/shared/components/Logo"; +import { Separator } from "@/shared/components/ui/separator"; + +interface Message extends Tables<"Messages"> { + user_id: string; + full_name: string | null; + avatar_url: string | null; +} + +type ChatroomMemberRecord = { + chatroom_id: string; + created_at: string; + id: number; + is_active: boolean; + member_id: number; + Classroom_Members: { + id: number; + user_id: string; + classroom_id: number; + Users: { + id: string; + full_name: string | null; + avatar_url: string | null; + }; + }; +}; + +const MessageArea = ({ + chatHistory, + chatroomId, + chatroomMemberRecord, + supabaseClientUrl, + supabaseClientKey, +}: { + chatHistory: Message[]; + chatroomId: string; + chatroomMemberRecord: ChatroomMemberRecord; + supabaseClientUrl: string; + supabaseClientKey: string; +}) => { + const [messages, setMessages] = useState(chatHistory); + const [chatClient, setChatClient] = useState( + null + ); + const [messageBoxValue, setMessageBoxValue] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // add database changes to messages state + useEffect(() => { + const supabase = createBrowserClient( + supabaseClientUrl, + supabaseClientKey + ); + const room = supabase.channel(`chatroom-${chatroomId}`); + + room.on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "Messages", + filter: `chatroom_id=eq.${chatroomId}`, + }, + async (payload) => { + const messageRaw = payload.new as Tables<"Messages">; + + // handle LLM message + if (messageRaw.member_id === null) { + const llmMessage: Message = { + ...messageRaw, + user_id: config.llmId, + full_name: config.llmName, + // TODO: We might need an avatar for assitant + avatar_url: config.llmAvatar, + }; + setMessages((prevMessages) => [...prevMessages, llmMessage]); + return; + } + + // For user messages, fetch the user details + try { + const { data: memberData } = await supabase + .from("Chatroom_Members") + .select( + ` + *, + Classroom_Members( + Users( + id, + full_name, + avatar_url + ) + ) + ` + ) + .eq("id", messageRaw.member_id) + .single(); + + if (memberData) { + const message: Message = { + ...messageRaw, + user_id: memberData.Classroom_Members.Users.id, + full_name: memberData.Classroom_Members.Users.full_name, + avatar_url: memberData.Classroom_Members.Users.avatar_url, + }; + + setMessages((prevMessages) => [...prevMessages, message]); + } + } catch (error) { + console.error("Error fetching message user details:", error); + } + } + ); + + // Subscribe to the channel + room.subscribe(); + + // Cleanup function + return () => { + supabase.removeChannel(room); + }; + }, [chatroomId, supabaseClientKey, supabaseClientUrl]); + + // TODO: get all avatars/user info from here at the beginning using the context instead of every message + const userContext = useContext(UserContext); + // // If the userContext is undefined still, give loading visual + if (!userContext) { + return ( +
+ +
+ + +
+
+ ); + } + + // // get the data and setter from the context (these are just a regular useState, so treat them like that) + const { userAndClassData } = userContext; + // const userId = userAndClassData.userData.id; + + const classroomInfo = userAndClassData.classroomsData.find( + (x) => x.id === chatroomMemberRecord.Classroom_Members.classroom_id + ); + if (!classroomInfo) { + console.log( + "Error rendering chatroom page, member of chatroom but didn't find information about the underlying classroom" + ); + return ( + // TODO: make 404 page since this is a classroom not found +

404

+ ); + } + + // Send the message directly from the browser, and also tell the server action if an LLM is involved + const sendMessageToChatroom = async () => { + const supabase = createClient(); + let content = messageBoxValue; + const isAskCommand = content.startsWith("/ask "); + if (isAskCommand) { + content = content.substring(5).trim(); + } + + if (!content) { + console.log("Message should not be empty"); + setMessageBoxValue(""); + return null; + } + + // Insert the message + const { error: messageError } = await supabase.from("Messages").insert([ + { + content, + member_id: chatroomMemberRecord.id, + chatroom_id: chatroomId, + is_ask: isAskCommand, + }, + ]); + + if (messageError) { + toast.error("Error sending message to chatroom", { + description: "Please refresh and try again", + }); + setMessageBoxValue(""); + return; + } + + setMessageBoxValue(""); + setIsLoading(true); + // Handle user "/ask" command + if (isAskCommand) { + const askResult = await askLLM(classroomInfo, chatroomId, chatClient); + if (!askResult.clientCreationSuccess) { + if (!askResult.failedBecauseEmptyDataset) { + // TODO: ask result has more detailed error differentiations if we want to tell the user + toast.error("Error sending communicating with LLM", { + description: "Please refresh and try again", + }); + setChatClient(null); // In case client is bad, clear it out + setMessageBoxValue(""); + return; + } + } + setChatClient(askResult.client); + } + setIsLoading(false); + }; + + function cleanMessage(content: string): string { + // Remove any reference patterns like ##number$$ + return content.replace(/\s##\d+\$\$/g, "").trim(); + } + let previousMessageTime: Date | undefined = undefined; + return ( +
+ + {/*
*/} +
+ + {messages.flatMap((message) => { + const variant = + message.user_id === userAndClassData.userData.id + ? "sent" + : "received"; + + // Format the timestamp + const messageTime = new Date(message.created_at); + const formattedTime = messageTime.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", + }); + const formattedDay = new Date( + message.created_at + ).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + const elements = []; + if ( + !previousMessageTime || + isDifferentDay(messageTime, previousMessageTime) + ) { + elements.push( +
+ + {formattedDay} + +
+ ); + } + previousMessageTime = messageTime; + elements.push( + + {!message?.member_id ? ( + + ) : ( + + )} +
+
+ {message?.member_id + ? (message.full_name ?? "Unknown") + : AIFullNameFormatted("AI Assistant")}{" "} + • {formattedTime} + {message.is_ask && ( + + Ask LLM + + )} +
+ + + {cleanMessage(message.content)} + + +
+
+ ); + return elements; + })} + {isLoading && ( + + + + + )} +
+
+
+ setMessageBoxValue(e.target.value)} + placeholder="Type your message or use /ask [message] to pull the LLM into the conversation..." + onEnter={sendMessageToChatroom} + className="focus-visible:ringof min-h-10 resize-none border-0 bg-background shadow-none focus-visible:ring-0" + /> + +
+
+ ); +}; + +function isDifferentDay(date1: Date, date2: Date) { + return !( + date1.getDate() === date2.getDate() && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + ); +} + +function AIFullNameFormatted(name: string) { + return ( + + {name} + + ); +} +export default MessageArea; diff --git a/app/chatrooms/[chatroomId]/components/new-messages.tsx b/app/chatrooms/[chatroomId]/components/new-messages.tsx deleted file mode 100644 index 3b9746b..0000000 --- a/app/chatrooms/[chatroomId]/components/new-messages.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client"; - -import { createClient } from "@/utils/supabase/client"; -import { Tables } from "@/utils/supabase/database.types"; -import Image from "next/image"; -import { useEffect, useState } from "react"; - -interface Message extends Tables<"Messages"> { - user_id: string; - full_name: string | null; - avatar_url: string | null; -} - -const NewMessages = ({ - chatHistory, - chatroomId, -}: { - chatHistory: Message[]; - chatroomId: string; -}) => { - const [messages, setMessages] = useState(chatHistory); - - useEffect(() => { - const supabase = createClient(); - - const room = supabase.channel(`chatroom-${chatroomId}`); - - room.on( - "postgres_changes", - { - event: "INSERT", - schema: "public", - table: "Messages", - filter: `chatroom_id=eq.${chatroomId}`, - }, - async (payload) => { - const messageRaw = payload.new as Tables<"Messages">; - - // handle LLM message - if (messageRaw.member_id === null) { - const llmMessage: Message = { - ...messageRaw, - user_id: "llm", - full_name: "AI Assistant", - // TODO: We might need an avatar for assitant - avatar_url: "", - }; - setMessages((prevMessages) => [...prevMessages, llmMessage]); - return; - } - - // For user messages, fetch the user details - try { - const { data: memberData } = await supabase - .from("Chatroom_Members") - .select( - ` - *, - Classroom_Members( - Users( - id, - full_name, - avatar_url - ) - ) - ` - ) - .eq("id", messageRaw.member_id) - .single(); - - if (memberData) { - const message: Message = { - ...messageRaw, - user_id: memberData.Classroom_Members.Users.id, - full_name: memberData.Classroom_Members.Users.full_name, - avatar_url: memberData.Classroom_Members.Users.avatar_url, - }; - - setMessages((prevMessages) => [...prevMessages, message]); - } - } catch (error) { - console.error("Error fetching message user details:", error); - } - } - ); - - // Subscribe to the channel - room.subscribe(); - - // Cleanup function - return () => { - supabase.removeChannel(room); - }; - }, [chatroomId]); - - return ( -
- {messages.length === 0 ? ( -

- No messages yet. Start the conversation! -

- ) : ( - messages.map((message, index) => { - const isLLM = message.user_id === "llm"; - - return ( -
- {isLLM ? ( -
- AI -
- ) : message.avatar_url ? ( - {message.full_name - ) : ( -
- {message.full_name?.charAt(0) || "?"} -
- )} -
-
- - {isLLM - ? "AI Assistant" - : message.full_name || "Unknown User"} - - - {new Date(message.created_at).toLocaleString()} - -
-
{message.content}
-
-
- ); - }) - )} -
- ); -}; - -export default NewMessages; diff --git a/app/chatrooms/[chatroomId]/page.tsx b/app/chatrooms/[chatroomId]/page.tsx index 2a33c6a..df4c54c 100644 --- a/app/chatrooms/[chatroomId]/page.tsx +++ b/app/chatrooms/[chatroomId]/page.tsx @@ -1,9 +1,11 @@ -import { createClient } from "@/utils/supabase/server"; -import Link from "next/link"; +import { createClient } from "@shared/utils/supabase/server"; import { redirect } from "next/navigation"; +import config from "../config"; +import DeleteChatroomButton from "./components/delete-chatroom-button"; +import InviteChatroomButton from "./components/invite-chatroom-dialog"; import LeaveChatroomButton from "./components/leave-chatroom-button"; -import NewMessages from "./components/new-messages"; -import { sendMessageToChatroom } from "../actions"; +import MessageArea from "./components/message-area"; +import { Users } from "lucide-react"; const ChatroomPage = async ({ params, @@ -33,11 +35,15 @@ const ChatroomPage = async ({ Classroom_Members ( id, user_id, + classroom_id, Users( id, full_name, avatar_url ) + ), + Chatrooms ( + ragflow_session_id ) ` ) @@ -49,6 +55,9 @@ const ChatroomPage = async ({ throw new Error("Error fetching chatroom members"); } + const classroomId = chatroomMembers[0].Classroom_Members.classroom_id; + const assistantId = chatroomMembers[0].Chatrooms.ragflow_session_id; + // Get current Chatroom Member const { data: { user }, @@ -63,9 +72,10 @@ const ChatroomPage = async ({ (member) => member.Classroom_Members.user_id === currentUser ); - // If user is not in this chatroom redirect to /chatrooms + // If user is not in this chatroom redirect to /error + // TODO: We might need to create an chatroom unauthroized page if (!currentMember) { - redirect("/chatrooms"); + redirect("/error"); } // Get messages @@ -95,64 +105,61 @@ const ChatroomPage = async ({ throw new Error("Error fetching messages"); } + // serialize llm messages const messages = messageRaw ? messageRaw.map((message) => { const { Chatroom_Members, ...newMessage } = message; return { - // HACK: llm response has member_id set to null and there is special user_id and full_name reserved for LLM - user_id: Chatroom_Members?.Classroom_Members.Users.id ?? "llm", + // HACK: llm response has member_id set to null and special user_id and full_name reserved for LLM + user_id: Chatroom_Members?.Classroom_Members.Users.id ?? config.llmId, full_name: Chatroom_Members?.Classroom_Members.Users.full_name ?? - "AI Assistant", + config.llmName, avatar_url: - Chatroom_Members?.Classroom_Members.Users.avatar_url ?? "", + Chatroom_Members?.Classroom_Members.Users.avatar_url ?? + config.llmAvatar, ...newMessage, }; }) : []; return ( -
+
-

{chatroom.name}

+
+

{chatroom.name}

+

+ Collaborative Chatroom +

+
- {currentUser !== chatroom.creater_user_id && ( - + {currentUser !== chatroom.creater_user_id ? ( + + ) : ( + )} - - Back to Chatrooms - -
-
-
- -
-
-
- - - - -
+
+ +
); }; diff --git a/app/chatrooms/actions.ts b/app/chatrooms/actions.ts index b756788..7ffde65 100644 --- a/app/chatrooms/actions.ts +++ b/app/chatrooms/actions.ts @@ -1,18 +1,16 @@ "use server"; -import { createClient } from "@/utils/supabase/server"; +import { createClient } from "@shared/utils/supabase/server"; import { revalidatePath } from "next/cache"; import { - getRagflowDatasetId, - sendMessage, -} from "../chat/[classroomId]/actions"; -import { + ChatClientWithSession, + createChatClient, deleteSession, - findChatAssistant, - getOrCreateAssistant, - getOrCreateSession, - llmToChatroom, -} from "./helpers"; + sendMessage, +} from "@shared/lib/ragflow/chat/chat-client"; +import { ClassroomWithMembers } from "@shared/lib/userContext/contextFetcher"; +import { createDatasetClient } from "@shared/lib/ragflow/dataset-client"; +import { chatroomConfigTemplate } from "@shared/lib/ragflow/chat/chat-configs"; export const createChatroom = async (formData: FormData) => { const supabase = await createClient(); @@ -21,10 +19,11 @@ export const createChatroom = async (formData: FormData) => { } = await supabase.auth.getUser(); if (!user) { - throw new Error("No authenticated user found"); + console.error("Unauthenticated user on chatroom page"); + return; // {supabaseCallSuccess: false} } - const name = (formData.get("name") as string) || "New Chatroom"; + const name = (formData.get("chatroom-name") as string) || "New Chatroom"; const classroom_id = parseInt(formData.get("classroom_id") as string); // Create a new chatroom @@ -41,7 +40,8 @@ export const createChatroom = async (formData: FormData) => { .single(); if (chatroomError) { - throw new Error(`Failed to create chatroom: ${chatroomError.message}`); + console.error(`Failed to create chatroom: ${chatroomError.message}`); + return; // {supabaseCallSuccess: false} } // Get the user's classroom member ID @@ -53,7 +53,8 @@ export const createChatroom = async (formData: FormData) => { .single(); if (memberError) { - throw new Error(`Failed to get member ID: ${memberError.message}`); + console.error(`Failed to get member ID: ${memberError.message}`); + return; //{supabaseCallSuccess: false} } // Add the user to the chatroom @@ -67,25 +68,27 @@ export const createChatroom = async (formData: FormData) => { ]); if (chatMemberError) { - throw new Error( - `Failed to add user to chatroom: ${chatMemberError.message}` - ); + console.error(`Failed to add user to chatroom: ${chatMemberError.message}`); + return; //{supabaseCallSuccess: false} } - revalidatePath("/chatrooms"); + return; //{supabaseCallSuccess: true} }; export const deleteChatroom = async ( chatroomId: string, - classroomId: number + chatroomAssistantId: string | null ) => { const supabase = await createClient(); - const assistantId = await findChatAssistant(classroomId, chatroomId); + // const assistantId = await findChatAssistant(classroomId, chatroomId); - if (assistantId) { - console.log("found session. delete session for chatroom"); - await deleteSession(chatroomId, assistantId); + if (chatroomAssistantId) { + // console.log("found session. delete session for chatroom"); + await deleteSession(chatroomAssistantId, { + primaryKeyValuesSessions: [{ key: "id", value: chatroomId }], + sessionIdStorage: chatroomConfigTemplate.sessionIdStorage, + }); } const { error: chatroomError } = await supabase @@ -94,64 +97,67 @@ export const deleteChatroom = async ( .eq("id", chatroomId); if (chatroomError) { - throw new Error(`Failed to delete chatroom: ${chatroomError.message}`); + console.error(`Failed to delete chatroom: ${chatroomError.message}`); } revalidatePath("/chatrooms"); }; -export const sendMessageToChatroom = async (formData: FormData) => { - const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); - - if (!user) { - throw new Error("No authenticated user found"); - } - - const chatroomId = formData.get("chatroomId") as string; - let content = (formData.get("message") as string).trim(); - const chatroomMemberId = parseInt(formData.get("chatroomMemberId") as string); - - if (!content || !chatroomMemberId || !chatroomId) { - throw new Error( - "Chatroom ID, chatroom member ID, and message content are required" - ); - } - - // Check if the message starts with "/ask" and trim it - const isAskCommand = content.startsWith("/ask "); - if (isAskCommand) { - content = content.substring(5).trim(); - if (!content) { - throw new Error("Message content is required after the /ask command"); - } - } - - // Insert the message - const { error: messageError } = await supabase.from("Messages").insert([ - { - content, - member_id: chatroomMemberId, - chatroom_id: chatroomId, - is_ask: isAskCommand, - }, - ]); - - if (messageError) { - throw new Error(`Failed to send message: ${messageError.message}`); - } - - // Handle user "/ask" command - if (isAskCommand) { - askLLM(chatroomId); - } - - revalidatePath(`/chatrooms/${chatroomId}`); -}; - -export const inviteUserToChatroom = async (formData: FormData) => { +// export const sendMessageToChatroom = async (formData: FormData) => { +// const supabase = await createClient(); +// const { +// data: { user }, +// } = await supabase.auth.getUser(); + +// if (!user) { +// throw new Error("No authenticated user found"); +// } + +// const chatroomId = formData.get("chatroomId") as string; +// let content = (formData.get("message") as string).trim(); +// const chatroomMemberId = parseInt(formData.get("chatroomMemberId") as string); + +// if (!content || !chatroomMemberId || !chatroomId) { +// throw new Error( +// "Chatroom ID, chatroom member ID, and message content are required" +// ); +// } + +// // Check if the message starts with "/ask" and trim it +// const isAskCommand = content.startsWith("/ask "); +// if (isAskCommand) { +// content = content.substring(5).trim(); +// if (!content) { +// throw new Error("Message content is required after the /ask command"); +// } +// } + +// // Insert the message +// const { error: messageError } = await supabase.from("Messages").insert([ +// { +// content, +// member_id: chatroomMemberId, +// chatroom_id: chatroomId, +// is_ask: isAskCommand, +// }, +// ]); + +// if (messageError) { +// throw new Error(`Failed to send message: ${messageError.message}`); +// } + +// // Handle user "/ask" command +// if (isAskCommand) { +// askLLM(chatroomId); +// } + +// revalidatePath(`/chatrooms/${chatroomId}`); +// }; + +export const inviteUserToChatroom = async ( + chatroomId: string, + inviteeEmail: string +) => { const supabase = await createClient(); const { data: { user }, @@ -161,9 +167,6 @@ export const inviteUserToChatroom = async (formData: FormData) => { throw new Error("No authenticated user found"); } - const chatroomId = formData.get("chatroom_id") as string; - const inviteeEmail = formData.get("email") as string; - if (!chatroomId || !inviteeEmail) { throw new Error("Chatroom ID and invitee email are required"); } @@ -348,18 +351,29 @@ export const leaveChatroom = async (chatroomId: string) => { revalidatePath("/chatrooms"); }; -export const askLLM = async (chatroomId: string) => { +export const askLLM = async ( + classroomInfo: ClassroomWithMembers, + chatroomId: string, + client: ChatClientWithSession | null +): Promise<{ + client: ChatClientWithSession | null; + supabaseMessageFetch: boolean; + datasetClientCreationSuccess: boolean; + failedBecauseEmptyDataset: boolean; + clientCreationSuccess: boolean; + llmMessageSend: boolean; +}> => { const supabase = await createClient(); - const { data: chatroom, error: chatroomError } = await supabase - .from("Chatrooms") - .select("classroom_id") - .eq("id", chatroomId) - .single(); + // const { data: chatroom, error: chatroomError } = await supabase + // .from("Chatrooms") + // .select("classroom_id") + // .eq("id", chatroomId) + // .single(); - if (chatroomError) { - throw new Error(`Failed to find chatroom: ${chatroomError.message}`); - } + // if (chatroomError) { + // throw new Error(`Failed to find chatroom: ${chatroomError.message}`); + // } // get all messages that is new const { data: messageRaw, error: messagesError } = await supabase @@ -383,7 +397,14 @@ export const askLLM = async (chatroomId: string) => { if (messagesError || !messageRaw) { console.error("Error fetching messages:", messagesError); - throw new Error("Error fetching messages or messages is null"); + return { + client: null, + supabaseMessageFetch: false, + datasetClientCreationSuccess: false, + failedBecauseEmptyDataset: false, + clientCreationSuccess: false, + llmMessageSend: false, + }; } const messages = messageRaw.map((message) => { @@ -400,42 +421,119 @@ export const askLLM = async (chatroomId: string) => { // HACK: We might need better prompt engineering at some point to optomize performance const prompt = ` -Below is the chat history before your last response (if any) in JSON: + Below is the chat history before your last response (if any) in JSON: -${JSON.stringify(messages)} - `; + ${JSON.stringify(messages)} + `; - const datasetId = await getRagflowDatasetId(chatroom.classroom_id); + // If no client already provided, make a new one + if (!client) { + // First we create the dataset client + const datasetClient = await createDatasetClient( + { + classroomId: classroomInfo.id.toString(), + classroomName: classroomInfo.name ?? "Classroom", //TODO: make class name non-nullable in supabase + }, + classroomInfo.ragflow_dataset_id + ); - if (!datasetId) { - llmToChatroom(chatroomId, "No dataset found!"); - return; - } + if (!datasetClient) { + console.log( + "Error rendering chat page, error creating or fetching dataset for classroom." + ); + return { + client: null, + supabaseMessageFetch: true, + datasetClientCreationSuccess: false, + failedBecauseEmptyDataset: false, + clientCreationSuccess: false, + llmMessageSend: false, + }; + } + // Then create the chat client using the chatroom specific template + const createClientResponse = await createChatClient( + { + ...chatroomConfigTemplate, + associatedClassroomName: classroomInfo.name ?? "Classroom", + primaryKeyValuesAssistant: [{ key: "id", value: classroomInfo.id }], + primaryKeyValuesSession: [{ key: "id", value: chatroomId }], + datasets: [datasetClient.client.datasetId], + } + // classroomInfo.chat_assistant_id + ); - const assistant = await getOrCreateAssistant( - chatroomId, - datasetId, - chatroom.classroom_id - ); + if (!createClientResponse.client) { + if (createClientResponse.failBecauseDatasetEmpty) { + llmToChatroom( + chatroomId, + "The dataset is empty right now, please ask your instructor to add materials to this classroom's dataset!" + ); + return { + client: null, + supabaseMessageFetch: true, + datasetClientCreationSuccess: true, + failedBecauseEmptyDataset: true, + clientCreationSuccess: false, + llmMessageSend: false, + }; + } + return { + client: null, + supabaseMessageFetch: true, + datasetClientCreationSuccess: true, + failedBecauseEmptyDataset: false, + clientCreationSuccess: false, + llmMessageSend: false, + }; + } - if (!assistant.id) { - llmToChatroom(chatroomId, "Dataset is empty"); - return; + client = createClientResponse.client as ChatClientWithSession; } - const chatSessionId = await getOrCreateSession( - chatroomId, - assistant.id, - chatroom.classroom_id - ); + // const datasetId = await getRagflowDatasetId(chatroom.classroom_id); - const response: string = await sendMessage( - prompt, - assistant.id, - chatSessionId - ); + // if (!datasetId) { + // llmToChatroom(chatroomId, "No dataset found!"); + // return; + // } + + // const assistant = await getOrCreateAssistant( + // chatroomId, + // datasetId, + // chatroom.classroom_id + // ); + + // if (!assistant.id) { + // llmToChatroom(chatroomId, "Dataset is empty"); + // return; + // } + + // const chatSessionId = await getOrCreateSession( + // chatroomId, + // assistant.id, + // chatroom.classroom_id + // ); - llmToChatroom(chatroomId, response); + const messageResponse = await sendMessage(client, prompt); + + if (!messageResponse.ragflowCallSuccess) { + return { + client: null, + supabaseMessageFetch: true, + datasetClientCreationSuccess: true, + failedBecauseEmptyDataset: false, + clientCreationSuccess: false, + llmMessageSend: false, + }; + } + + // const response: string = await sendMessage( + // prompt, + // assistant.id, + // chatSessionId + // ); + + llmToChatroom(chatroomId, messageResponse.response); // mark all messages as not new message const messageIds = messages.map((message) => message.id); @@ -447,8 +545,36 @@ ${JSON.stringify(messages)} .in("id", messageIds); if (messageMarkError) { - throw new Error( - `Failed to mark messages as not new: ${messageMarkError.message}` + console.error( + `Error setting message as not new and sent to the LLM for chatroom ID ${chatroomId}:`, + messageMarkError + ); + } + return { + client: client, + supabaseMessageFetch: true, + datasetClientCreationSuccess: true, + failedBecauseEmptyDataset: false, + clientCreationSuccess: true, + llmMessageSend: true, + }; +}; + +const llmToChatroom = async (chatroomId: string, message: string) => { + const supabase = await createClient(); + + const { error } = await supabase.from("Messages").insert([ + { + chatroom_id: chatroomId, + content: message, + is_new: false, + }, + ]); + + if (error) { + console.error( + `Error while sending message from LLM to chatroom ${chatroomId}:`, + error ); } }; diff --git a/app/chatrooms/components/invite-form.tsx b/app/chatrooms/components/invite-form.tsx deleted file mode 100644 index 4a272a7..0000000 --- a/app/chatrooms/components/invite-form.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { inviteUserToChatroom } from "../actions"; -import { Tables } from "@/utils/supabase/database.types"; - -export default function InviteForm({ - chatrooms, -}: { - chatrooms: Tables<"Chatrooms">[]; -}) { - console.log(chatrooms); - const [isInviting, setIsInviting] = useState(false); - const [message, setMessage] = useState<{ - type: "success" | "error"; - text: string; - } | null>(null); - - const handleSubmit = async (formData: FormData) => { - setIsInviting(true); - setMessage(null); - - try { - const result = await inviteUserToChatroom(formData); - setMessage({ type: "success", text: result.message }); - const form = document.getElementById("invite-form") as HTMLFormElement; - form.reset(); - } catch (error) { - setMessage({ - type: "error", - text: error instanceof Error ? error.message : "Failed to invite user", - }); - } finally { - setIsInviting(false); - } - }; - - return ( -
-

- Invite User to Chatroom -

- - {message && ( -
- {message.text} -
- )} - -
-
- - -
- -
- - -
- - -
-
- ); -} diff --git a/app/chatrooms/config.ts b/app/chatrooms/config.ts new file mode 100644 index 0000000..b75046c --- /dev/null +++ b/app/chatrooms/config.ts @@ -0,0 +1,7 @@ +const config = { + llmId: "llm", + llmName: "AI Assistant", + llmAvatar: "", +}; + +export default config; diff --git a/app/chatrooms/helpers.ts b/app/chatrooms/helpers.ts deleted file mode 100644 index 9a230ef..0000000 --- a/app/chatrooms/helpers.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { createClient } from "@/utils/supabase/server"; -import { createServiceClient } from "@/utils/supabase/service-server"; - -// TODO: these helpers should probably be combined with the actions in /chat - -const API_URL = process.env.RAGFLOW_API_URL + "/api" || ""; -const API_KEY = process.env.RAGFLOW_API_KEY; - -export const findChatAssitantAndUpdate = async ( - datasetId: string | null, - chatroomId: string, - classroomId: number -) => { - if (!datasetId) { - return null; - } - const name = `${datasetId}-${chatroomId}`; - - console.log("Finding existing assitant on ragflow"); - - try { - const res = await fetch(`${API_URL}/v1/chats?name=${name}`, { - method: "GET", - headers: { - Authorization: `Bearer ${API_KEY}`, - "Content-type": "application/json; charset=UTF-8", - }, - }); - - if (!res.ok) throw new Error("Failed to find chat assistant"); - - const resJson = await res.json(); - - if (resJson.code !== 0) { - return null; - } - - const data = resJson.data[0].id; - - // update assistant on supabase - const supabase = createServiceClient(); - - const { error } = await supabase - .from("Classrooms") - .update({ chatroom_assistant_id: data }) - .eq("id", classroomId); - - if (error) { - throw new Error(`Failed to update classroom: ${error}`); - } - - return data; - } catch (error) { - console.error("Error finding chat assistant:", error); - return null; - } -}; - -export const findChatAssistant = async ( - classroomId: number, - chatroomId: string -) => { - try { - const supabase = await createClient(); - - const res = await supabase - .from("Classrooms") - .select("chatroom_assistant_id, ragflow_dataset_id") - .eq("id", classroomId) - .single(); - - if (res.error) throw new Error(`Failed to fetch chats: ${res.error}`); - - const data = - res.data.chatroom_assistant_id || - (await findChatAssitantAndUpdate( - res.data.ragflow_dataset_id, - chatroomId, - classroomId - )); - - // console.log(data); - - return data; - } catch (error) { - console.error("Error fetching chat assistant:", error); - return null; - } -}; - -export const createChatAssistant = async ( - chatroomId: string, - classroomId: number, - datasetId: string -) => { - const newAssistant = { - dataset_ids: [datasetId], - name: `${datasetId}-${chatroomId}`, - llm: { - frequency_penalty: 0.7, - presence_penalty: 0.4, - temperature: 0.1, - top_p: 0.3, - }, - prompt: { - prompt: `You are an advanced language model named 'Classroom LM' participating in a collaborative chat with a group of users. Your primary goal is to assist students with factual, well-structured answers based on the knowledge base provided. If the knowledge base has relevant content, use it to generate responses. If not, provide the best possible answer based on your general understanding. - -In addition to answering questions, you can **generate exam materials** when requested. This includes: - -- **Multiple-choice questions** (4 options each, one correct) -- **Short answer questions** -- **Essay prompts for critical thinking** -- **Problem-solving exercises (for STEM)** -- **True/False questions with explanations** - -You will be given the chat history before your last response (if any), including messages in JSON format from the user(s). Use this history to understand the context and generate a helpful response to the users. - -**Instructions**: -- Carefully review the chat history to understand the context of the conversation. -- Focus on the latest message marked with \`"is_ask": true\` and generate a response that aligns with the ongoing discussion. -- Ensure your response is clear, concise, and helpful to the group. -- If the question is ambiguous or lacks sufficient context, politely ask for clarification. -- If your response needs to reference a specific message in the chat history, address the user by their \`full_name\`. -- Correct any factual errors or misunderstandings in the conversation about the topic, using the knowledge base provided. Reference the specific message where the error occurred, if applicable. -- Clearly indicate whether your response is based on retrieval from the knowledge base or your general understanding. - -**Knowledge Base:** -{knowledge}`, - empty_response: "", - variables: [{ key: "knowledge", optional: true }], - keywords_similarity_weight: 0.75, - similarity_threshold: 0.2, - top_n: 6, - show_quote: true, - }, - }; - - try { - const res = await fetch(`${API_URL}/v1/chats`, { - method: "POST", - body: JSON.stringify(newAssistant), - headers: { - Authorization: `Bearer ${API_KEY}`, - "Content-type": "application/json; charset=UTF-8", - }, - }); - - if (!res.ok) throw new Error("Failed to create chat assistant"); - - const resJson = await res.json(); - - if (!resJson?.data) { - return null; - } - - // update that in supabase - const supabase = await createClient(); - - const supabaseRes = await supabase - .from("Classrooms") - .update({ chatroom_assistant_id: resJson.data.id }) - .eq("id", classroomId) - .select(); - - if (supabaseRes.error) { - throw new Error(`Failed to update classroom: ${supabaseRes.error}`); - } - - return resJson; - } catch (error) { - console.error("Error creating chat assistant:", error); - return null; - } -}; - -export const getOrCreateAssistant = async ( - chatroomId: string, - datasetId: string, - classroomId: number -) => { - const existingChat = await findChatAssistant(classroomId, chatroomId); - if (existingChat) { - return { id: existingChat }; - } - - console.log("Get or create: didn't find an assistant, creating a new one"); - - const newAssistant = await createChatAssistant( - chatroomId, - classroomId, - datasetId - ); - - return { id: newAssistant?.data.id || null }; -}; - -export const findSessionID = async ( - classroomId: number, - chatroomId: string -) => { - try { - const supabase = await createClient(); - - const sessionID = await supabase - .from("Chatrooms") - .select("ragflow_session_id") - .eq("classroom_id", classroomId) - .eq("id", chatroomId) - .single(); - - if (sessionID.error) { - console.error("Error fetching session:", sessionID.error); - return null; - } - - return sessionID.data.ragflow_session_id; - } catch (error) { - console.error("Error fetching session:", error); - return null; - } -}; - -async function createSession( - assistantID: string, - chatroomId: string, - classroomId: number -) { - const newSession = { - assistant_id: assistantID, - user_id: chatroomId, - name: `Session_Chatroom_${chatroomId}`, - }; - - try { - const res = await fetch(`${API_URL}/v1/chats/${assistantID}/sessions`, { - method: "POST", - headers: { - Authorization: `Bearer ${API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(newSession), - }); - - if (!res.ok) throw new Error("Failed to create session"); - - const resJson = await res.json(); - - console.log(resJson); - - // update that in supabase - const supabase = createServiceClient(); - - const supabaseRes = await supabase - .from("Chatrooms") - .update({ ragflow_session_id: resJson.data.id }) - .eq("classroom_id", classroomId) - .eq("id", chatroomId) - .select(); - - if (supabaseRes.error) { - throw new Error(`Failed to update classroom: ${supabaseRes.error}`); - } - - return resJson.data.id; - } catch (error) { - console.error("Error creating session:", error); - return null; - } -} - -export const getOrCreateSession = async ( - chatroomId: string, - chatAssistantId: string, - classroomId: number -) => { - const existingSession = await findSessionID(classroomId, chatroomId); - console.log("Found an existing session:", existingSession); - if (existingSession) { - return existingSession; - } - - return await createSession(chatAssistantId, chatroomId, classroomId); -}; - -export const deleteSession = async ( - chatroomId: string, - assistantId: string -) => { - const supabase = await createClient(); - const { data: session, error: sessionError } = await supabase - .from("Chatrooms") - .select("ragflow_session_id") - .eq("id", chatroomId) - .single(); - - if (sessionError) { - throw new Error("Error fetching session id"); - } - - const body = { - ids: [session.ragflow_session_id], - }; - - try { - const res = await fetch(`${API_URL}/v1/chats/${assistantId}/sessions`, { - method: "DELETE", - headers: { - Authorization: `Bearer ${API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - - if (!res.ok) throw new Error("Failed to delete session"); - - const resJson = await res.json(); - console.log(resJson); - } catch (error) { - console.error("Error creating session:", error); - return null; - } -}; - -export const llmToChatroom = async (chatroomId: string, message: string) => { - const supabase = await createClient(); - - const { error } = await supabase.from("Messages").insert([ - { - chatroom_id: chatroomId, - content: message, - is_new: false, - }, - ]); - - if (error) { - throw new Error("Error when sending message from LLM"); - } -}; diff --git a/app/chatrooms/invite/page.tsx b/app/chatrooms/invite/page.tsx deleted file mode 100644 index 3ef7f23..0000000 --- a/app/chatrooms/invite/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { createClient } from "@/utils/supabase/server"; -import InviteForm from "../components/invite-form"; -import Link from "next/link"; - -export default async function InvitePage() { - const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); - if (!user) { - throw new Error("No authenticated user found"); - } - const currentUser = user.id; - - // get all the classrooms that current user joined - const { data: classroomMembers, error: memberError } = await supabase - .from("Classroom_Members") - .select("id, classroom_id") - .eq("user_id", currentUser); - - if (memberError) { - throw new Error(`Failed to get classroom members: ${memberError.message}`); - } - - const memberIds = classroomMembers?.map((element) => element.id) || []; - - // get all the chatroomsIds that current user joined - const chatroomIds = []; - - for (const memberId of memberIds) { - const { data, error } = await supabase - .from("Chatroom_Members") - .select("chatroom_id") - .eq("member_id", memberId) - .eq("is_active", true); - - if (data && !error) { - for (const element of data) { - chatroomIds.push(element.chatroom_id); - } - } - } - - // get all the chatrooms that current user joined - const chatrooms = []; - - for (const chatroomId of chatroomIds) { - const { data, error } = await supabase - .from("Chatrooms") - .select("*") - .eq("id", chatroomId) - .single(); - - if (data && !error) { - chatrooms.push(data); - } - } - - return ( -
-
- - ← Back to Chatrooms - -
- - -
- ); -} diff --git a/app/chatrooms/page.tsx b/app/chatrooms/page.tsx deleted file mode 100644 index 0842936..0000000 --- a/app/chatrooms/page.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { createClient } from "@/utils/supabase/server"; -import Link from "next/link"; -import { createChatroom, deleteChatroom } from "./actions"; - -const ChatroomsPage = async () => { - const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); - if (!user) { - throw new Error("No authenticated user found"); - } - - const currentUser = user.id; - - // get all classrooms that user joined - const { data: classroomMembers, error: classroomMembersError } = - await supabase - .from("Classroom_Members") - .select( - ` - *, - Classrooms ( - id, - name - ) - ` - ) - .eq("user_id", currentUser); - - if (classroomMembersError) { - console.error("Error fetching classrooms:", classroomMembersError); - throw new Error("Error fetching classrooms"); - } - - // take out all the classrooms - const classrooms = classroomMembers.map((member) => member.Classrooms) || []; - - // Get all classroom member ids for the current user - const userClassroomMemberIds = classroomMembers.map((member) => member.id); - - // Get all chatrooms that user joined - const { data: chatroomMembers, error: chatroomMembersError } = await supabase - .from("Chatroom_Members") - .select( - ` - *, - Chatrooms( - id, - name, - classroom_id, - creater_user_id, - Classrooms( - name - ) - ) - ` - ) - .in("member_id", userClassroomMemberIds) - .eq("is_active", true); - - if (chatroomMembersError) { - console.error("Error fetching chatrooms:", chatroomMembersError); - throw new Error("Error fetching chatrooms"); - } - - const chatrooms = chatroomMembers.map((member) => member.Chatrooms); - - return ( -
-
-

Your Chatrooms

-
- - - - - Invite Users - -
-
- -
- {chatrooms.length > 0 ? ( - chatrooms.map((chatroom) => ( -
-

{chatroom.name}

-

{`Classroom: ${chatroom.Classrooms.name}`}

-
- - Enter Chatroom - - - {currentUser === chatroom.creater_user_id && ( -
- -
- )} -
-
- )) - ) : ( -
-

- You don't have any chatrooms yet. -

-
- )} -
-
- ); -}; - -export default ChatroomsPage; diff --git a/app/classroom/[classroomId]/upload/preview/page.tsx b/app/classroom/[classroomId]/upload/preview/page.tsx deleted file mode 100644 index 8c256ad..0000000 --- a/app/classroom/[classroomId]/upload/preview/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default async function PreviewPage({ - searchParams, -}: { - searchParams: Promise<{ documentId: string; datasetId: string }>; -}) { - const { documentId, datasetId } = await searchParams; - - return ( -
- -
- ); -} diff --git a/app/classroom/[classroomId]/upload/uploadComponent.tsx b/app/classroom/[classroomId]/upload/uploadComponent.tsx deleted file mode 100644 index 302a175..0000000 --- a/app/classroom/[classroomId]/upload/uploadComponent.tsx +++ /dev/null @@ -1,187 +0,0 @@ -"use client"; - -import { useState, useEffect, ChangeEvent, FormEvent, useRef } from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { - createDatasetClient, - DatasetClient, - retrieveDocuments, - uploadFile, -} from "@/app/lib/ragflow/dataset-client"; -import { Input } from "@/components/ui/input"; -import { toast } from "@/hooks/use-toast"; -import { Skeleton } from "@/components/ui/skeleton"; -import { ScrollArea } from "@/components/ui/scroll-area"; - -type UploadedFile = { - id: string; - datasetId: string; - name: string; - size: number; - type: string; - status: string; -}; - -export default function UploadComponent({ - classroomId, - classroomName, -}: { - classroomId: string; - classroomName: string; -}) { - const [file, setFile] = useState(null); - const [uploadedFiles, setUploadedFiles] = useState( - null - ); - const [datasetClient, setDatasetClient] = useState(); - const [loading, setLoading] = useState(false); - const inputFile = useRef(null); - - useEffect(() => { - async function fetchFiles() { - let clientToUse = datasetClient; - if (!clientToUse) { - const result = await createDatasetClient({ - classroomId, - classroomName, - }); - if (result) { - clientToUse = result.client; - setDatasetClient(clientToUse); - } else { - return; - } - } - const retrieveResult = await retrieveDocuments(clientToUse); - console.log("retrieve: ", retrieveResult); - if (!retrieveResult.ragflowCallSuccess) { - return; - } - setUploadedFiles(retrieveResult.files); - } - - fetchFiles(); - const interval = setInterval(fetchFiles, 5000); - return () => clearInterval(interval); - }, [classroomId, classroomName, datasetClient]); - - const handleFileChange = (e: ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - setFile(e.target.files[0]); - } - }; - - async function handleSubmit(e: FormEvent) { - e.preventDefault(); - if (!file) return; - - setLoading(true); - - const formData = new FormData(); - formData.append("file", file); - - if (!datasetClient) { - return; - } - const response = await uploadFile(datasetClient, formData); - - toast({ - title: "Upload started!", - description: `Document ${file.name} has uploaded and began parsing`, - duration: 10000, - }); - setLoading(false); - - // if (!response || typeof response !== "object") { - // setErrorMessage("Invalid response from server."); - // return; - // } - if ( - response.isAdmin && - response.parseCallSuccess && - response.uploadCallSuccess - ) { - setUploadedFiles(response.files); - setFile(null); - if (inputFile.current) { - inputFile.current.value = ""; - } - } - // } else { - // setErrorMessage(response.message || "Failed to upload file."); - // } - } - - return ( -
-
-

File Upload

- {datasetClient == undefined || uploadedFiles == null ? ( -
- -
- - -
-
- ) : ( - <> -
-
- -
- - -
- - {/* Move to component with files passed in as props so that newly fetch data doesn't trigger a rerender (and thus another fetch) infinitely so*/} - - - )} -
-
- ); -} - -function FileList({ uploadedFiles }: { uploadedFiles: UploadedFile[] }) { - const pathname = usePathname(); - return ( - uploadedFiles.length > 0 && ( - -
-

Uploaded Files

-
    - {uploadedFiles.map((file) => ( -
  • - - {file.name} - -

    - {(file.size / 1024).toFixed(2)} KB - {file.type} -{" "} - {file.status} -

    -
  • - ))} -
-
-
- ) - ); -} diff --git a/app/classroom/actions.ts b/app/classroom/actions.ts deleted file mode 100644 index d098eec..0000000 --- a/app/classroom/actions.ts +++ /dev/null @@ -1,298 +0,0 @@ -"use server"; -import { createServiceClient } from "@/utils/supabase/service-server"; -import { createClient } from "@/utils/supabase/server"; -import { Tables } from "@/utils/supabase/database.types"; -import { deleteDataset } from "../lib/ragflow/dataset-client"; - -export interface ClassroomWithMembers extends Tables<"Classrooms"> { - Classroom_Members?: Array<{ - id: number; - classroom_id: number; - Users: { - id: string; - email: string; - full_name: string; - avatar_url: string; - }; - }>; -} -const RAGFLOW_SERVER_URL = process.env.RAGFLOW_API_URL || ""; -const RAGFLOW_API_KEY = process.env.RAGFLOW_API_KEY; - -// export async function getCurrentUserID2() { -// const supabase = createServiceClient(); - -// const { data: { user } } = await supabase.auth.getUser() - -// // // Get the current user using the updated method -// // const { data: user, error } = await supabase.auth.getUser(); - -// // if (error) { -// // throw new Error(error.message); -// // } - -// // if (!user) { -// // throw new Error("No user is logged in"); -// // } - -// // return user.id; // Return the current user's ID -// } -export async function getCurrentUserId() { - const supabase = await createClient(); - - const { - data: { user }, - } = await supabase.auth.getUser(); - if (!user) { - throw Error("No authenticated user found"); - } - return user.id; -} - -export async function deleteClassroom(classroom_id: number) { - // Deleting Associated Supabase - const supabase = await createClient(); - const { data, error } = await supabase - .from("Classrooms") - .delete() - .eq("id", classroom_id) - .select() - .single(); - - if (error) { - throw new Error(error.message); - } - - // Deleting Associated Chat Assistant - const chat_assistant_id = data.chat_assistant_id; - - if (chat_assistant_id) { - const requestChatBody = { - ids: [chat_assistant_id], - }; - - const chatResponse = await fetch(`${RAGFLOW_SERVER_URL}/api/v1/chats`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${RAGFLOW_API_KEY}`, - }, - body: JSON.stringify(requestChatBody), - }); - - if (!chatResponse.ok) { - throw new Error( - `Failed while deleting assistant from Ragflow: ${chatResponse.statusText}` - ); - } - } else { - // If no chat assistant, we don't want to error out - console.log("No chat assistant found for classroom when deleting"); - } - - // Deleting associated RAGFlow dataset if exists - if (data.ragflow_dataset_id) { - deleteDataset(data.id.toString(), data.ragflow_dataset_id); - } - // const ragflow_dataset_id = data.ragflow_dataset_id; - - // if (!ragflow_dataset_id) { - // throw new Error("No related RAGFlow dataset found for this classroom."); - // } - - // //gets ids of ragflow_dataset_id - // const requestBody = { - // ids: [ragflow_dataset_id], - // }; - - // //deletes the respective dataset - // const ragflowResponse = await fetch(`${RAGFLOW_SERVER_URL}/api/v1/datasets`, { - // method: "DELETE", - // headers: { - // "Content-Type": "application/json", - // Authorization: `Bearer ${RAGFLOW_API_KEY}`, - // }, - // body: JSON.stringify(requestBody), - // }); - - // if (!ragflowResponse.ok) { - // throw new Error( - // `Failed to delete dataset from Ragflow: ${ragflowResponse.statusText}` - // ); - // } - - return data || []; -} - -export async function leaveClassroom(classroom_id: number, user_id: string) { - const supabase = await createServiceClient(); - const { data, error } = await supabase - .from("Classroom_Members") - .delete() - .eq("classroom_id", classroom_id) - .eq("user_id", user_id); - if (error) { - throw new Error(error.message); - } - return data || []; -} - -// export async function getClassroomAdminID(classroom_id: number) { -// const supabase = await createClient(); -// const { data, error } = await supabase -// .from("Classrooms") -// .select("admin_user_id") -// .eq("id", classroom_id); - -// if (error) { -// throw new Error(error.message); -// } - -// return data[0]?.admin_user_id || null; -// } - -export async function getUserClassrooms() { - const supabase = await createClient(); - const { data, error } = await supabase.from("Classrooms").select(` - *, - Classroom_Members ( - id, - classroom_id, - Users ( - id, - email, - full_name, - avatar_url - ) - ) - `); - if (error) { - throw new Error(error.message); - } - return data || []; -} - -export async function retrieveClassroomData(userId: string) { - const classrooms = await getUserClassrooms(); - - // if (!classrooms || classrooms.length === 0) { - // return; - // } - - const validAdminClasses = classrooms.filter( - (classroom) => classroom.admin_user_id == userId - ); - - const validNonAdminClasses = classrooms.filter( - (classroom) => classroom.admin_user_id != userId - ); - - return { validAdminClasses, validNonAdminClasses }; -} - -export async function inviteMemberToClassroom( - email: string, - classroom_id: number -) { - const supabase = createServiceClient(); - - const { data: users, error: userError } = await supabase - .from("Users") - .select("id") - .eq("email", email) - .single(); - - if (userError || !users) { - throw new Error("User does not exist"); - } - - //checks for duplicate - const { data: member } = await supabase - .from("Classroom_Members") - .select("id") - .eq("classroom_id", classroom_id) - .eq("user_id", users.id); - - if (member && member.length > 0) { - throw new Error("User already in the classroom"); - } - - //insert if no errors - const { error: insertError } = await supabase - .from("Classroom_Members") - .insert({ - classroom_id: classroom_id, - user_id: users.id, - }); - - if (insertError) { - throw new Error("Error inserting classroom member"); - } - return true; -} - -export async function changeClassroomName( - classroom_id: number, - newName: string -) { - const supabase = await createServiceClient(); - const { data, error } = await supabase - .from("Classrooms") - .update({ name: newName }) - .eq("id", classroom_id) - .select(); - - if (error) { - console.log("Error changing name"); - } - - //console.log(data[0].name); - return data; -} - -export async function archiveClassroom(classroom_id: number) { - const supabase = await createServiceClient(); - - const { data, error } = await supabase - .from("Classrooms") - .update({ archived: true }) - .eq("id", classroom_id) - .select(); - - if (error) { - console.error("Error archiving classroom:", error); - return { success: false, error: error.message }; - } - - return { success: true, data }; -} - -export async function unarchiveClassroom(classroom_id: number) { - const supabase = await createServiceClient(); - - const { data, error } = await supabase - .from("Classrooms") - .update({ archived: false }) - .eq("id", classroom_id) - .select(); - - if (error) { - console.error("Error unarchiving classroom:", error); - return { success: false, error: error.message }; - } - - return { success: true, data }; -} - -export async function insertRandom() { - // Notice how we use a createServiceClient instead of createClient from server - // this BYPASSES ALL RLS in the case that you have to do some more complex things and we don't - // want to write RLS rules for all of it. See our project doc Resources section for more info - const supabase = createServiceClient(); - - const { error } = await supabase.from("Classroom_Members").insert({ - classroom_id: 17, - user_id: "05929f55-42bb-42d4-86bd-ddc0c7d12685", - }); - console.log(error); -} diff --git a/app/classroom/classroomList.tsx b/app/classroom/classroomList.tsx deleted file mode 100644 index 99c3871..0000000 --- a/app/classroom/classroomList.tsx +++ /dev/null @@ -1,312 +0,0 @@ -"use client"; -import { useState } from "react"; -import { - deleteClassroom, - leaveClassroom, - retrieveClassroomData, - changeClassroomName, - ClassroomWithMembers, - archiveClassroom, - unarchiveClassroom, -} from "./actions"; -import { Tables } from "@/utils/supabase/database.types"; -import InviteMember from "./inviteMember"; -import Link from "next/link"; -import MemberList from "./memberList"; - -export default function ClassroomList({ - userId, - initialAdminData, - initialMemberData, -}: { - userId: string; - initialAdminData: Tables<"Classrooms">[]; - initialMemberData: Tables<"Classrooms">[]; -}) { - const [adminClasses, setAdminClassrooms] = useState(initialAdminData); - const [memberClasses, setMemberClassrooms] = useState(initialMemberData); - - const deleteClassroomAndFetch = async (classroomId: number) => { - try { - await deleteClassroom(classroomId); - refreshClassrooms(); - } catch (error: unknown) { - //type unknown for typescript lint - if (error instanceof Error) { - console.error(error.message); - } else { - console.error("Error Occured"); - } - } - }; - - const archiveClassroomAndFetch = async (classroomId: number) => { - try { - setAdminClassrooms((prevClasses) => - prevClasses.map((classroom) => - classroom.id === classroomId - ? { ...classroom, archived: true } - : classroom - ) - ); - await archiveClassroom(classroomId); - refreshClassrooms(); - } catch { - console.error("Error occurred while archiving the classroom"); - } - }; - - const unarchiveClassroomAndFetch = async (classroomId: number) => { - try { - setAdminClassrooms((prevClasses) => - prevClasses.map((classroom) => - classroom.id === classroomId - ? { ...classroom, archived: false } - : classroom - ) - ); - await unarchiveClassroom(classroomId); - refreshClassrooms(); - } catch { - console.error("Error occurred while archiving the classroom"); - } - }; - - const handleChangeClassroomName = async (classroomId: number) => { - const newName = window.prompt("Enter new class name:"); - if (newName !== null && newName !== "") { - setAdminClassrooms((prevClasses) => - prevClasses.map((classroom) => - classroom.id === classroomId - ? { ...classroom, name: newName } - : classroom - ) - ); - - try { - await changeClassroomName(classroomId, newName); - } catch (error) { - console.error("Error changing classroom name:", error); - setAdminClassrooms((prevClasses) => - prevClasses.map((classroom) => - classroom.id === classroomId - ? { ...classroom, name: classroom.name } - : classroom - ) - ); - } - } - }; - - const leaveClassroomAndFetch = async (classroomId: number) => { - try { - await leaveClassroom(classroomId, userId); - refreshClassrooms(); - } catch (error: unknown) { - //type unknown for typescript lint - if (error instanceof Error) { - console.error(error.message); - } else { - console.error("Error Occured"); - } - } - }; - - const refreshClassrooms = async () => { - const refreshedData = await retrieveClassroomData(userId); - if (refreshedData) { - setAdminClassrooms(refreshedData.validAdminClasses); - setMemberClassrooms(refreshedData.validNonAdminClasses); - } - }; - - function mapToListItem( - classroomList: ClassroomWithMembers[], - isAdmin: boolean - ) { - return classroomList.map((classroom) => { - return ( -
- {!classroom.archived && ( - <> -

{classroom.name}

-

Classroom ID: {classroom.id}

-

- Ragflow Dataset ID: {classroom.ragflow_dataset_id || "null"} -

-

Join Code: {classroom.join_code || "N/A"}

- - {classroom.Classroom_Members && - classroom.Classroom_Members.length > 0 && ( - - )} - - - - - - - - {isAdmin && ( - - )} - - {isAdmin && ( - - - - )} - - {isAdmin && ( - - )} - -
- - )} -
- ); - }); - } - - function mapToListItemArchived( - classroomList: ClassroomWithMembers[], - isAdmin: boolean - ) { - return classroomList.map((classroom) => { - return ( -
- {classroom.archived && ( - <> -

{classroom.name}

-

Classroom ID: {classroom.id}

-

- Ragflow Dataset ID: {classroom.ragflow_dataset_id || "null"} -

- - {classroom.Classroom_Members && - classroom.Classroom_Members.length > 0 && ( -
-

Members:

-
    - {classroom.Classroom_Members.map((member) => ( -
  • User ID: {member.id}
  • - ))} -
-
- )} - - {/* */} - {/* - - */} - - - {isAdmin && ( - - )} - -
- - )} -
- ); - }); - } - - return ( - <> -

My Classrooms

-

Admin Classrooms

- {/* ADMIN CLASSES */} - {mapToListItem(adminClasses, true)} -
-

Member Classrooms

- {/* NON-ADMIN CLASSES */} - {mapToListItem(memberClasses, false)} -
-

- Archived Classrooms -

-

Admin Classrooms

- {mapToListItemArchived(adminClasses, true)} - -
-

Member Classrooms

- {/* NON-ADMIN CLASSES */} - {mapToListItemArchived(memberClasses, false)} - {/* - - */} - {/* */} - - ); -} diff --git a/app/classroom/memberList.tsx b/app/classroom/memberList.tsx deleted file mode 100644 index 85b4a4f..0000000 --- a/app/classroom/memberList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import { ClassroomWithMembers } from "./actions"; -import { columns } from "./columns"; -import { DataTable } from "@/components/ui/data-table"; - -export default function MemberList({ - classroom, -}: { - classroom: ClassroomWithMembers; -}) { - if (!classroom.Classroom_Members) { - return

No members found!

; - } - // other table implementation: https://data-table.openstatus.dev/ - return ( - - - - - {/* not used: https://github.com/shadcn-ui/ui/issues/16#issuecomment-1602565563 */} - - {/* for some reason chanding the width doesn't actually do anything */} - - {classroom.name} Members - {/* - Make changes to your profile here. Click save when - you're done. - */} - - {/* todo future, for smaller screens, make the width even smaller */} -
- x.Users)} - /> -
-
-
-
- ); -} diff --git a/app/classroom/new/actions.ts b/app/classroom/new/actions.ts deleted file mode 100644 index d8e3a42..0000000 --- a/app/classroom/new/actions.ts +++ /dev/null @@ -1,79 +0,0 @@ -"use server"; -import { createClient } from "@/utils/supabase/server"; -import { createServiceClient } from "@/utils/supabase/service-server"; - -// const RAGFLOW_SERVER_URL = process.env.RAGFLOW_API_URL || ""; -// const RAGFLOW_API_KEY = process.env.RAGFLOW_API_KEY; - -export async function newClassroom(name: string, id: string) { - //Create a new RAGFlow dataset - // const timestamp = Date.now(); - // const ragflowName = name + " " + timestamp + " " + id.substring(0, 6); - // const ragflowResponse = await fetch(`${RAGFLOW_SERVER_URL}/api/v1/datasets`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // Authorization: `Bearer ${RAGFLOW_API_KEY}`, - // }, - // body: JSON.stringify({ - // name: ragflowName, - // }), - // }); - - // if (!ragflowResponse.ok) { - // const error = await ragflowResponse.json(); - // console.error("Error creating dataset in RAGFlow:", error); - // return null; - // } - - // const ragflowData = await ragflowResponse.json(); - // console.log(ragflowData); - // const ragflowDatasetId = ragflowData.data.id; - - const supabase = await createServiceClient(); - const { data, error } = await supabase - .from("Classrooms") - .insert([ - { - // ragflow_dataset_id: ragflowDatasetId, #??? TODO:: see if its okay that we moved this to uploading - name: name, - admin_user_id: id, - archived: false, - }, - ]) - .select("id"); - - if (error) { - console.error("Error inserting classroom:", error); - return null; - } - - // add yourself to member list - if (data && data.length > 0) { - const classroomId = data[0].id; - console.log("Classroom ID:", classroomId); - const { error } = await supabase - .from("Classroom_Members") - .insert([{ classroom_id: classroomId, user_id: id }]) - .select(); - - if (error) { - console.error("Error inserting admin to classroom member list:", error); - return null; - } - } - - return data; -} - -export async function getCurrentUserId() { - const supabase = await createClient(); - - const { - data: { user }, - } = await supabase.auth.getUser(); - if (!user) { - throw Error("No authenticated user found"); - } - return user.id; -} diff --git a/app/classroom/new/page.tsx b/app/classroom/new/page.tsx deleted file mode 100644 index 8b527df..0000000 --- a/app/classroom/new/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; -import { useState } from "react"; -import { newClassroom, getCurrentUserId } from "./actions"; -import Link from "next/link"; - -export default function NewClassroomPage() { - const [className, setClassName] = useState(""); - const [resultText, setResultText] = useState(""); - const addClassroom = async () => { - try { - const userId = await getCurrentUserId(); - const result = await newClassroom(className, userId); - if (!result) { - setResultText(`Error while making classroom!`); - return; - } - setResultText(`Created classroom ${className} successfully!`); - } catch (error: unknown) { - //type unknown for typescript lint - if (error instanceof Error) { - console.error(error.message); - } else { - console.error("Error Occured"); - } - } - setClassName(""); - }; - - return ( - <> -
-

Create a new classroom!

-
-

Name:

- setClassName(e.target.value)} - style={{ - color: "black", - outline: "2px solid black", - outlineColor: "black", - }} - /> -
- {resultText &&
{resultText}
} -
- - - - -
-
- - ); -} diff --git a/app/classroom/newClassroomButton.tsx b/app/classroom/newClassroomButton.tsx deleted file mode 100644 index de522ba..0000000 --- a/app/classroom/newClassroomButton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client"; -import Link from "next/link"; - -export default function NewClassroomButton() { - return ( - - - - ); -} diff --git a/app/classroom/page.tsx b/app/classroom/page.tsx deleted file mode 100644 index f217790..0000000 --- a/app/classroom/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// import { createClient } from "@/utils/supabase/server"; // notice how it uses the server one since we don't have "useclient" so the default is server side component -// "use client"; - -import { getCurrentUserId, retrieveClassroomData } from "./actions"; -import ClassroomList from "./classroomList"; -import NewClassroomButton from "./newClassroomButton"; -export default async function ClassroomPage() { - const userId = await getCurrentUserId(); - - const classData = await retrieveClassroomData(userId); - - return ( - <> -
-

User ID: {userId}

- - -
- - ); -} diff --git a/app/classrooms/[classroomId]/augment/AugmentNotes.tsx b/app/classrooms/[classroomId]/augment/AugmentNotes.tsx new file mode 100644 index 0000000..1b8bf19 --- /dev/null +++ b/app/classrooms/[classroomId]/augment/AugmentNotes.tsx @@ -0,0 +1,83 @@ +"use client"; +import { useState, useRef } from "react"; + +import { Button } from "@shared/components/ui/button"; +import { Input } from "@shared/components/ui/input"; + +import { FileText, Upload } from "lucide-react"; +import { toast } from "sonner"; + +export default function AugmentComponent({ + // TODO: remove for classroomId and setIsProcessing once we actually implement augments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + classroomId, +}: { + classroomId: string; +}) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [isProcessing, setIsProcessing] = useState(false); + const [file, setFile] = useState(null); + const inputFile = useRef(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (!selectedFile) return; + + // Double-check that file passed by user is markdown or pdf + const fileExtension = selectedFile.name.split(".").at(-1)?.toLowerCase(); + if (fileExtension !== "pdf" && fileExtension !== "md") { + toast.error("Invalid file format", { + description: "Please upload a Markdown (.md) or PDF (.pdf) file", + }); + return; + } + + setFile(selectedFile); + }; + + const handleUpload = async () => { + // TODO + }; + + return ( +
+

Augment Notes

+

+ Upload your notes to get AI-powered enhancements and improvements +

+
+ + + {file && ( + + )} +
+
+ ); +} diff --git a/app/classrooms/[classroomId]/augment/page.tsx b/app/classrooms/[classroomId]/augment/page.tsx new file mode 100644 index 0000000..6be88dc --- /dev/null +++ b/app/classrooms/[classroomId]/augment/page.tsx @@ -0,0 +1,31 @@ +import { createClient } from "@shared/utils/supabase/server"; +import { notFound } from "next/navigation"; + +import AugmentComponent from "./AugmentNotes"; + +export default async function UploadPage({ + params, +}: { + params: Promise<{ classroomId: string }>; +}) { + const { classroomId } = await params; + + const supabase = await createClient(); + const { data, error } = await supabase + .from("Classrooms") + .select("name") + .eq("id", Number(classroomId)) + .single(); + + if (error || !data || !data.name) { + console.error("Error fetching classroom or its name:", error); + notFound(); + } + + return ( + <> +

Classroom: {data.name}

+ + + ); +} diff --git a/app/classrooms/[classroomId]/chat/MessageBox.tsx b/app/classrooms/[classroomId]/chat/MessageBox.tsx new file mode 100644 index 0000000..5486f49 --- /dev/null +++ b/app/classrooms/[classroomId]/chat/MessageBox.tsx @@ -0,0 +1,135 @@ +"use client"; +import { useState } from "react"; +import { + AIAvatar, + ChatBubble, + ChatBubbleAvatar, + ChatBubbleMessage, +} from "@shared/components/ui/chat/chat-bubble"; +import { ChatMessageList } from "@shared/components/ui/chat/chat-message-list"; +import { ChatInput } from "@shared/components/ui/chat/chat-input"; +import { Button } from "@/shared/components/ui/button"; +import { + ChatClientWithSession, + RagFlowMessage, + RagFlowMessages, + sendMessage, +} from "@shared/lib/ragflow/chat/chat-client"; +import { toast } from "sonner"; +import Logo from "@/shared/components/Logo"; +import { SendIcon } from "lucide-react"; +import ReactMarkdown from "react-markdown"; + +interface MessageBoxProps { + chatClient: ChatClientWithSession; + messageHistory: RagFlowMessages | null; +} + +export default function MessageBox({ + chatClient, + messageHistory, +}: MessageBoxProps) { + const [value, setValue] = useState(""); + const [messages, setMessages] = useState( + messageHistory || [] + ); + const [isLoading, setIsLoading] = useState(false); + + function cleanMessage(content: string): string { + return content.replace(/\s##\d+\$\$/g, "").trim(); + } + + async function handleSend() { + if (!value.trim()) return; + + const userMessage: RagFlowMessage = { role: "user", content: value }; + setMessages((prev) => [...prev, userMessage]); + setValue(""); + setIsLoading(true); + const response = await sendMessage(chatClient, value); + setIsLoading(false); + + if (!response.ragflowCallSuccess) { + toast.error("Error sending message", { + description: `Please try refreshing the page`, + duration: 10000, + }); + return; + } + + const assistantMessage: RagFlowMessage = { + role: "assistant", + content: response.response, + created_at: response.responseTimeSeconds, + }; + setMessages((prev) => [...prev, assistantMessage]); + } + console.log(messages); + return ( +
+ +
+ + {messages.map((msg, index) => ( + + {msg.role === "assistant" ? ( + + ) : ( + + )} +
+ + {msg?.created_at && + getTimeDate(msg.created_at) && + getTimeDate(msg.created_at)} + + + + {cleanMessage(msg.content)} + +
+
+ ))} + {isLoading && ( + + + + + )} +
+
+
+ setValue(e.target.value)} + placeholder="Type your message..." + onEnter={handleSend} + className="focus-visible:ringof min-h-10 resize-none border-0 bg-background shadow-none focus-visible:ring-0" + /> + +
+
+ ); +} + +function getTimeDate(created_at_seconds: number) { + return new Date(created_at_seconds * 1000).toLocaleTimeString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); +} diff --git a/app/classrooms/[classroomId]/chat/page.tsx b/app/classrooms/[classroomId]/chat/page.tsx new file mode 100644 index 0000000..1d33314 --- /dev/null +++ b/app/classrooms/[classroomId]/chat/page.tsx @@ -0,0 +1,125 @@ +import Link from "next/link"; + +import MessageBox from "./MessageBox"; +import { createDatasetClient } from "@shared/lib/ragflow/dataset-client"; +import { getUserAndClassroomData } from "@shared/lib/userContext/contextFetcher"; +import { + ChatClientWithSession, + createChatClient, +} from "@shared/lib/ragflow/chat/chat-client"; +import { personalChatConfigTemplate } from "@shared/lib/ragflow/chat/chat-configs"; +import { Button } from "@shared/components/ui/button"; +import { SpeechIcon, Upload } from "lucide-react"; + +export default async function ChatPage({ + params, +}: { + params: Promise<{ classroomId: string }>; +}) { + // Get user and class data in order to get the classroom name, at different points error out if the calls arent successful + const userAndClassData = await getUserAndClassroomData(); + if (!userAndClassData) { + console.log( + "Error rendering chat page, error fetching user and classroom info." + ); + return ( + // TODO: make server error page +

server error

+ ); + } + const { classroomId } = await params; + const classroomIdNum = Number(classroomId); + const user = userAndClassData.userData; + // const username = user.user_metadata?.full_name ?? "User Name"; + + const classroomInfo = userAndClassData.classroomsData.find( + (x) => x.id === classroomIdNum + ); + if (!classroomInfo) { + console.log( + "Error rendering chat page, supabase call successful but classroom not found" + ); + return ( + // TODO: make 404 page since this is a classroom not found +

404

+ ); + } + + // Now that we know we have a valid classroom, we create the dataset client. Creating the client + // ensures that we have a valid RagFlow dataset (it will make it if one doesn't already exist + // or the currently stored ID refers to an invalid dataset) and that this is properly reflected in Supabase + const datasetClient = await createDatasetClient( + { + classroomId, + classroomName: classroomInfo.name ?? "Classroom", //TODO: make class name non-nullable in supabase + }, + classroomInfo.ragflow_dataset_id + ); + + if (!datasetClient) { + console.log( + "Error rendering chat page, error creating or fetching dataset for classroom." + ); + return ( + // TODO: make server error page +

server error

+ ); + } + + const chatClient = await createChatClient( + { + ...personalChatConfigTemplate, + associatedClassroomName: classroomInfo.name ?? "Classroom", + primaryKeyValuesAssistant: [{ key: "id", value: classroomInfo.id }], + primaryKeyValuesSession: [ + { key: "classroom_id", value: classroomInfo.id }, + { key: "user_id", value: user.id }, + ], + datasets: [datasetClient.client.datasetId], + } + // classroomInfo.chat_assistant_id + ); + + if (!chatClient.client) { + if (chatClient.failBecauseDatasetEmpty) { + return user.id === classroomInfo.admin_user_id ? ( +
+

Empty dataset, go upload files for classroom:

+ +
+ ) : ( +

+ Classroom dataset empty, please ask your instructor to add some + resources! +

+ ); + } + return

Server error!

; //TODO fixthis + } + + return ( + //
+
+

+ {classroomInfo.name} +

+

+ Personal Assistant +

+ + +
+ ); +} diff --git a/app/classrooms/[classroomId]/chatrooms/_components/create-chatroom-dialog.tsx b/app/classrooms/[classroomId]/chatrooms/_components/create-chatroom-dialog.tsx new file mode 100644 index 0000000..70916e0 --- /dev/null +++ b/app/classrooms/[classroomId]/chatrooms/_components/create-chatroom-dialog.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { createChatroom } from "@/app/chatrooms/actions"; +import { Button } from "@shared/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@shared/components/ui/dialog"; +import { Input } from "@shared/components/ui/input"; +import { Label } from "@shared/components/ui/label"; +import { Plus } from "lucide-react"; +import { useState } from "react"; +import { useFormStatus } from "react-dom"; + +function CreateButton() { + const { pending } = useFormStatus(); + + return ( + + ); +} + +export function CreateChatroomDialog({ classroomId }: { classroomId: string }) { + const [open, setOpen] = useState(false); + + const handleCreateChatroom = async (formData: FormData) => { + await createChatroom(formData); + setOpen(false); // Close the dialog after submission + }; + + return ( + + + + + + + New Chatroom + + Create a new Chatroom for current classroom. + + +
+
+
+ + +
+ + + + +
+
+
+
+ ); +} diff --git a/app/classrooms/[classroomId]/chatrooms/page.tsx b/app/classrooms/[classroomId]/chatrooms/page.tsx new file mode 100644 index 0000000..6150dc9 --- /dev/null +++ b/app/classrooms/[classroomId]/chatrooms/page.tsx @@ -0,0 +1,144 @@ +import { Button } from "@/shared/components/ui/button"; +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card"; +import { Separator } from "@/shared/components/ui/separator"; +import { createClient } from "@shared/utils/supabase/server"; +import { ArrowRightIcon, DoorOpen } from "lucide-react"; +import Link from "next/link"; +import { CreateChatroomDialog } from "./_components/create-chatroom-dialog"; + +const ChatroomsPage = async ({ + params, +}: { + params: Promise<{ classroomId: string }>; +}) => { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) { + throw new Error("No authenticated user found"); + } + + const { classroomId } = await params; + + const currentUser = user.id; + + // Get all chatrooms that user joined for this classroom + const { data: chatroomMembers, error: chatroomMembersError } = await supabase + .from("Chatroom_Members") + .select( + ` + *, + Chatrooms( + id, + name, + classroom_id, + creater_user_id, + Classrooms( + name, + chatroom_assistant_id + ), + Users ( + full_name + ) + ), + Classroom_Members!inner( + user_id, + classroom_id + ) + ` + ) + .eq("Classroom_Members.user_id", currentUser) + .eq("Classroom_Members.classroom_id", parseInt(classroomId, 10)) + .eq("is_active", true); + + if (chatroomMembersError) { + console.error("Error fetching chatrooms:", chatroomMembersError); + throw new Error("Error fetching chatrooms"); + } + + const chatrooms = chatroomMembers.map((member) => member.Chatrooms); + + return ( +
+
+

Chatrooms

+ + +
+ + + +
+ {chatrooms.length > 0 ? ( + chatrooms.map((chatroom) => ( + + + + {chatroom.name} + + +
+ Owner: {chatroom.Users.full_name} +
+
+
+ + + {/* + + + + + } + content={"Enter"} + /> */} + + {/* {chatroomMembers && chatroomMembers.length > 0 && ( */} + {/**/} + {/* )} */} + +
+ )) + ) : ( +
+

+ You don't have any chatrooms yet. +

+
+ )} +
+
+ ); +}; + +export default ChatroomsPage; diff --git a/app/classroom/inviteMember.tsx b/app/classrooms/[classroomId]/manage/_components/inviteMember.tsx similarity index 75% rename from app/classroom/inviteMember.tsx rename to app/classrooms/[classroomId]/manage/_components/inviteMember.tsx index 69af839..c647525 100644 --- a/app/classroom/inviteMember.tsx +++ b/app/classrooms/[classroomId]/manage/_components/inviteMember.tsx @@ -1,24 +1,22 @@ "use client"; import { useState } from "react"; -import { inviteMemberToClassroom } from "./actions"; +import { inviteMemberToClassroom } from "../../../actions"; +import { toast } from "sonner"; -export default function InviteMember({ - classroomId, - onInviteSuccess, -}: { - classroomId: number; - onInviteSuccess: () => void; -}) { +export default function InviteMember({ classroomId }: { classroomId: number }) { const [email, setEmail] = useState(""); const handleInvite = async () => { try { await inviteMemberToClassroom(email, classroomId); setEmail(""); - onInviteSuccess(); + toast.success("Added Member Successfully", { + description: `${email} was added to the class.`, + }); } catch (error: unknown) { //type unknown for typescript lint if (error instanceof Error) { - console.error(error.message); + toast.error("The user is already part of the classroom."); + // console.error(error.message); } else { console.error("Error Occured"); } diff --git a/app/classrooms/[classroomId]/manage/buttons.tsx b/app/classrooms/[classroomId]/manage/buttons.tsx new file mode 100644 index 0000000..43dae6d --- /dev/null +++ b/app/classrooms/[classroomId]/manage/buttons.tsx @@ -0,0 +1,185 @@ +// import { createClient } from "@/utils/supabase/server"; // notice how it uses the server one since we don't have "useclient" so the default is server side component +// "use client"; + +// import { getCurrentUserId, retrieveClassroomData } from "../../classroom/actions"; +"use client"; + +import Link from "next/link"; +// import MemberList from "../../classroom/memberList"; +import { + changeClassroomName, + deleteClassroom, + setArchiveStatusClassroom, +} from "../../actions"; +import { optimisticUpdateAndFetchClassroomData } from "../../clientUtils"; +import { + ClassroomWithMembers, + getUserAndClassroomData, + UserWithClassroomsData, +} from "@shared/lib/userContext/contextFetcher"; +import { useRouter } from "next/navigation"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@shared/components/ui/alert-dialog"; +import { Dispatch, SetStateAction, useTransition } from "react"; +import { User } from "@supabase/supabase-js"; +import SaveClassroomDialog from "../../_components/saveClassroomDialog"; +import { Skeleton } from "@shared/components/ui/skeleton"; +import MemberList from "../../_components/memberList"; +import InviteMember from "./_components/inviteMember"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +export default function ClassroomManagementButtons({ + userData, + classroomData, + setUserAndClassCallback, +}: { + userData: User; + classroomData: ClassroomWithMembers; + setUserAndClassCallback: Dispatch>; +}) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + // optimisticUpdateAndFetchClassroomData( + // classroomId, + // async () => changeClassroomName(classroomId, newName), + // { name: newName }, + // setUserAndClassData, + // refreshClassrooms + // ); + // }; + const refreshClassrooms = async () => { + const refreshedData = await getUserAndClassroomData(); + if (refreshedData) { + setUserAndClassCallback(refreshedData); + } + }; + + const handleChangeClassroomName = async (newName: string) => { + return await optimisticUpdateAndFetchClassroomData( + async () => changeClassroomName(classroomData.id, newName), + { name: newName }, + setUserAndClassCallback, + classroomData.id, + refreshClassrooms + ); + }; + + const deleteClassroomFunction = async () => { + router.push(`/classrooms`); + startTransition(async () => { + await deleteClassroom(classroomData.id); + }); + toast.success("Successfully deleted classroom"); + // const confirmation = window.confirm( + // "Are you sure? This action can't be undone." + // ); + // if (confirmation) { + + // const delete_success = new URL("/classroom"); + // delete_success.searchParams.append( + // "delete_success", + // classroomId.toString() + // ); + // redirect(delete_success) + + refreshClassrooms(); + }; + + const archiveClassroomCallback = async () => { + // optimisticUpdateAndFetchClassroomData( + // classroomId, + // async () => setArchiveStatusClassroom(classroomId, true), + // { archived: true }, + // setUserAndClassData, + // refreshClassrooms + // ); + setArchiveStatusClassroom(classroomData.id, true); + toast.success("Successfully archived classroom"); + router.push(`/classrooms`); + // router.push(`/classroom?archive_success=${classroomData.id.toString()}`); + refreshClassrooms(); + }; + + return ( +
+ {"Look at the class info: " + classroomData.name} + + + + {/* ARCHIVE BUTTON */} + + + {/* */} + + + Delete + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete this + classroom. + + + + Cancel + deleteClassroomFunction()} + > + {isPending && } Continue + + + + + + + + {classroomData.Classroom_Members && + classroomData.Classroom_Members.length > 0 ? ( + + ) : ( + + )} +

Invite Member:

+ +
+ ); +} diff --git a/app/classrooms/[classroomId]/manage/page.tsx b/app/classrooms/[classroomId]/manage/page.tsx new file mode 100644 index 0000000..820ce5a --- /dev/null +++ b/app/classrooms/[classroomId]/manage/page.tsx @@ -0,0 +1,83 @@ +// import { createClient } from "@/utils/supabase/server"; // notice how it uses the server one since we don't have "useclient" so the default is server side component +"use client"; + +// import { getCurrentUserId, retrieveClassroomData } from "../../classroom/actions"; + +import { useContext, useEffect, useState } from "react"; +import ClassroomManagementButtons from "./buttons"; +import Link from "next/link"; +import { UserContext } from "@shared/lib/userContext/userContext"; +import { useParams } from "next/navigation"; +import { Skeleton } from "@shared/components/ui/skeleton"; +import { ClassroomWithMembers } from "@shared/lib/userContext/contextFetcher"; + +export default function ClassroomManagementPage() { + const { classroomId } = useParams<{ classroomId: string }>(); + const [staleDataIfDeleted, setStaleDataIfDeleted] = useState< + ClassroomWithMembers | undefined + >(); + + const userContext = useContext(UserContext); + if (!userContext) { + return ( +
+ +
+ + +
+
+ ); + } + const classroomIdNumber = Number(classroomId); + const { setUserAndClassData, userAndClassData } = userContext; + const classroomInfo = userAndClassData.classroomsData.find( + (x) => x.id === classroomIdNumber + ); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setStaleDataIfDeleted(structuredClone(classroomInfo)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // If the userContext is undefined still, give loading visual + let classToRender = undefined; + + if (!classroomInfo) { + if (staleDataIfDeleted) { + classToRender = staleDataIfDeleted; + } else { + return ( +
+ +
+ + +
+
+ ); + } + } else { + classToRender = classroomInfo; + } + + return ( +
+

Hello this is classroom {classroomId}

+ + + + +
+ ); +} diff --git a/app/classroom/[classroomId]/upload/actions.ts b/app/classrooms/[classroomId]/upload/actions.ts similarity index 97% rename from app/classroom/[classroomId]/upload/actions.ts rename to app/classrooms/[classroomId]/upload/actions.ts index 500656e..4ae7cde 100644 --- a/app/classroom/[classroomId]/upload/actions.ts +++ b/app/classrooms/[classroomId]/upload/actions.ts @@ -1,8 +1,8 @@ "use server"; -import { getCurrentUserId } from "@/app/chat/[classroomId]/actions"; +import { getCurrentUserIdServer } from "@shared/lib/supabase/shared"; // import { createDatasetClient } from "@/app/lib/ragflow/dataset-client"; -import { createClient } from "@/utils/supabase/server"; +import { createClient } from "@shared/utils/supabase/server"; // const RAGFLOW_API_KEY: string = process.env.RAGFLOW_API_KEY || ""; // const RAGFLOW_SERVER_URL: string = process.env.RAGFLOW_API_URL || ""; @@ -169,7 +169,7 @@ import { createClient } from "@/utils/supabase/server"; export async function isUserAdminForClassroom(classroomId: number) { const supabase = createClient(); - const userId = await getCurrentUserId(); + const userId = await getCurrentUserIdServer(); const { data, error } = await (await supabase) .from("Classrooms") diff --git a/app/classroom/[classroomId]/upload/page.tsx b/app/classrooms/[classroomId]/upload/page.tsx similarity index 80% rename from app/classroom/[classroomId]/upload/page.tsx rename to app/classrooms/[classroomId]/upload/page.tsx index 83c3b31..54638ca 100644 --- a/app/classroom/[classroomId]/upload/page.tsx +++ b/app/classrooms/[classroomId]/upload/page.tsx @@ -1,6 +1,8 @@ -import { isUserAdminForClassroom } from "./actions"; +import { notFound } from "next/navigation"; +import { createClient } from "@shared/utils/supabase/server"; + import UploadComponent from "./uploadComponent"; -import { createClient } from "@/utils/supabase/server"; +import { isUserAdminForClassroom } from "./actions"; export default async function UploadPage({ params, @@ -10,9 +12,7 @@ export default async function UploadPage({ const { classroomId } = await params; const isAdmin = await isUserAdminForClassroom(Number(classroomId)); if (!isAdmin) { - return ( -

Not the admin for this classroom! But change this to 404 page

- ); + notFound(); } // #TODO: move this out to general supabase place @@ -25,7 +25,7 @@ export default async function UploadPage({ if (error || !data || !data.name) { console.error("Error fetching classroom or its name:", error); - return

Insert 404 page here

; + notFound(); } return ( diff --git a/app/classrooms/[classroomId]/upload/preview/page.tsx b/app/classrooms/[classroomId]/upload/preview/page.tsx new file mode 100644 index 0000000..3a91320 --- /dev/null +++ b/app/classrooms/[classroomId]/upload/preview/page.tsx @@ -0,0 +1,42 @@ +import { downloadDocument } from "@/shared/lib/ragflow/dataset-client"; +import { notFound } from "next/navigation"; + +export default async function PreviewPage({ + searchParams, +}: { + searchParams: Promise<{ documentId: string; datasetId: string }>; +}) { + const { documentId, datasetId } = await searchParams; + + const { ragflowCallSuccess, content, mimeType, fileName } = + await downloadDocument(datasetId, documentId); + + if (!ragflowCallSuccess) { + notFound(); + } + + console.log(`Rendering file: ${fileName}, with MIME type: ${mimeType}`); + + if (mimeType === "application/octet-stream") { + // We fallback to rendering as text + const text = new TextDecoder().decode(new Uint8Array(content)); + return ( +
+
{text}
+
+ ); + } else { + // Convert binary content to base64 for embedding non-text file types + // Allows us to render PDFs, images and other binary formats directly in the browser + const base64Content = Buffer.from(content).toString("base64"); + return ( +
+ +
+ ); + } +} diff --git a/app/classrooms/[classroomId]/upload/uploadComponent.tsx b/app/classrooms/[classroomId]/upload/uploadComponent.tsx new file mode 100644 index 0000000..3759d60 --- /dev/null +++ b/app/classrooms/[classroomId]/upload/uploadComponent.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useState, useEffect, ChangeEvent, FormEvent, useRef } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + createDatasetClient, + DatasetClient, + retrieveDocuments, + uploadFile, +} from "@shared/lib/ragflow/dataset-client"; +import { Input } from "@shared/components/ui/input"; +import { Button } from "@shared/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@shared/components/ui/card"; +import { Skeleton } from "@shared/components/ui/skeleton"; +import { ScrollArea } from "@shared/components/ui/scroll-area"; +import { toast } from "sonner"; +import { SquareArrowOutUpRight } from "lucide-react"; + +type UploadedFile = { + id: string; + datasetId: string; + name: string; + size: number; + type: string; + status: string; +}; + +const ACTION_MAX_SIZE_BYTES = 10 * 1_000_000; // this is set in next.config.ts + +export default function UploadComponent({ + classroomId, + classroomName, +}: { + classroomId: string; + classroomName: string; +}) { + const [file, setFile] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState( + null + ); + const [datasetClient, setDatasetClient] = useState(); + const [loading, setLoading] = useState(false); + const inputFile = useRef(null); + + useEffect(() => { + const fetchFiles = async () => { + let clientToUse = datasetClient; + if (!clientToUse) { + const result = await createDatasetClient({ + classroomId, + classroomName, + }); + if (result) { + clientToUse = result.client; + setDatasetClient(clientToUse); + } else { + return; + } + } + const retrieveResult = await retrieveDocuments(clientToUse); + console.log("retrieve: ", retrieveResult); + if (!retrieveResult.ragflowCallSuccess) { + return; + } + setUploadedFiles(retrieveResult.files); + }; + + fetchFiles(); + const interval = setInterval(fetchFiles, 5000); + return () => clearInterval(interval); + }, [classroomId, classroomName, datasetClient]); + + const handleFileChange = (e: ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + if (selectedFile.size > ACTION_MAX_SIZE_BYTES) { + toast.error("File size too big!"); + e.preventDefault(); + if (inputFile.current) { + inputFile.current.value = ""; + } + return; + } else { + setFile(selectedFile); + } + } + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!file) return; + + setLoading(true); + + let toastResolve = undefined; + let toastError = undefined; + const toastPromise = new Promise((res, rej) => { + toastResolve = res; + toastError = rej; + }); + + const formData = new FormData(); + formData.append("file", file); + + if (!datasetClient) { + return; + } + const response = await uploadFile(datasetClient, formData); + + toast.promise(toastPromise, { + loading: `Document ${file.name} has uploaded and began parsing`, + success: () => ({ + message: `${file.name} successfully uploaded!`, + }), + error: () => ({ + message: `Error while uploading ${file.name}`, + }), + }); + + setLoading(false); + + if ( + response.isAdmin && + response.parseCallSuccess && + response.uploadCallSuccess + ) { + setUploadedFiles(response.files); + setFile(null); + if (inputFile.current) { + inputFile.current.value = ""; + } + if (toastResolve) { + (toastResolve as unknown as (value: unknown) => void)(null); + } + } else if (toastError) { + (toastError as unknown as (value: unknown) => void)(null); + } + }; + + return ( +
+ + + File Upload + + + {datasetClient == undefined || uploadedFiles == null ? ( +
+ +
+ + +
+
+ ) : ( + <> +
+
+ +
+ + +
+ + {/* Move to component with files passed in as props so that newly fetch data doesn't trigger a rerender (and thus another fetch) infinitely so*/} + + + )} +
+
+
+ ); +} + +function FileList({ uploadedFiles }: { uploadedFiles: UploadedFile[] }) { + const pathname = usePathname(); + return ( + uploadedFiles.length > 0 && ( + +
+

Uploaded Files

+
    + {uploadedFiles.map((file) => ( +
  • + + {file.name} + + +

    + {(file.size / 1024).toFixed(2)} KB - {file.type} -{" "} + {file.status} +

    +
  • + ))} +
+
+
+ ) + ); +} diff --git a/app/classroom/columns.tsx b/app/classrooms/_components/columns.tsx similarity index 83% rename from app/classroom/columns.tsx rename to app/classrooms/_components/columns.tsx index 4a54aff..f21be4f 100644 --- a/app/classroom/columns.tsx +++ b/app/classrooms/_components/columns.tsx @@ -1,8 +1,12 @@ "use client"; -import { Tables } from "@/utils/supabase/database.types"; +import { Tables } from "@shared/utils/supabase/database.types"; import { ColumnDef } from "@tanstack/react-table"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@shared/components/ui/avatar"; export const columns: ColumnDef>[] = [ { diff --git a/app/classrooms/_components/invite-dialog.tsx b/app/classrooms/_components/invite-dialog.tsx new file mode 100644 index 0000000..210ec90 --- /dev/null +++ b/app/classrooms/_components/invite-dialog.tsx @@ -0,0 +1,110 @@ +// "use server" + +import { Button } from "@shared/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@shared/components/ui/dialog"; +import { Input } from "@shared/components/ui/input"; +import { DialogTrigger } from "@radix-ui/react-dialog"; +import { ReactNode, useState } from "react"; +import { Check, Copy, Share2 } from "lucide-react"; + +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/shared/components/ui/input-otp"; + +export default function InviteInfoDialog({ + code, + classroomName, + dialogTrigger, +}: { + code: string; + classroomName: string; + dialogTrigger?: ReactNode; +}) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isCopied, setIsCopied] = useState(false); + + const TIMEOUT = 4000; + + let joinUrl = ""; + if (typeof window !== "undefined") { + joinUrl = `${window.location.origin}/classrooms/join/${code}`; + } + + const copyToClipboard = (value: string) => { + if (typeof window === "undefined" || !navigator.clipboard.writeText) { + return; + } + + if (!value) return; + + navigator.clipboard.writeText(value).then(() => { + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, TIMEOUT); + }, console.error); + }; + + return ( + + {dialogTrigger ?? ( + + + + )} + + + Invite to {classroomName} + + Have members join using this link or from the Join button their + classroom page. + + +
+ + +
+ + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/app/classrooms/_components/join-dialog.tsx b/app/classrooms/_components/join-dialog.tsx new file mode 100644 index 0000000..b9df76b --- /dev/null +++ b/app/classrooms/_components/join-dialog.tsx @@ -0,0 +1,95 @@ +// "use server" + +import { Button } from "@shared/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@shared/components/ui/dialog"; +import { DialogTrigger } from "@radix-ui/react-dialog"; +import { ReactNode, useState } from "react"; +import { UserPlus } from "lucide-react"; + +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/shared/components/ui/input-otp"; +import { useRouter } from "next/navigation"; +import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; + +export default function JoinDialog({ + dialogTrigger, +}: { + dialogTrigger?: ReactNode; +}) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [joinValue, setJoinValue] = useState(""); + + const router = useRouter(); + + const joinButtonCallback = () => { + if (joinValue.length == 8) { + router.push(`/classrooms/join/${joinValue}`); + setIsDialogOpen(false); + } + }; + + return ( + { + setIsDialogOpen(val); + if (val == true) { + setJoinValue(""); + } + }} + > + {dialogTrigger ?? ( + + + + )} + + + Join a classroom + Enter join code: + + + setJoinValue(value)} + > + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/classrooms/_components/memberList.tsx b/app/classrooms/_components/memberList.tsx new file mode 100644 index 0000000..6e4f88f --- /dev/null +++ b/app/classrooms/_components/memberList.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@shared/components/ui/sheet"; +import { columns } from "./columns"; +import { DataTable } from "@shared/components/ui/data-table"; +import { ClassroomWithMembers } from "@shared/lib/userContext/contextFetcher"; +import { Trash2, Users } from "lucide-react"; +import { ReactNode, useEffect, useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@shared/components/ui/tooltip"; +import { Button } from "@shared/components/ui/button"; +import { Tables } from "@shared/utils/supabase/database.types"; +import { Row } from "@tanstack/react-table"; +import { removeMember } from "../actions"; + +/** + * + * @param triggerButton make sure you wrap with a SheetTrigger with an asChild in order for the sheet to work + * @returns + */ +export default function MemberList({ + classroom, + userId, + enableDeletion, + triggerButton, +}: { + classroom: ClassroomWithMembers; + userId: string; + enableDeletion: boolean; + triggerButton?: ReactNode; +}) { + const [members, setMembers] = useState[]>([]); + + useEffect(() => { + if (classroom.Classroom_Members) { + setMembers(classroom.Classroom_Members.map((x) => x.Users)); + } + }, [classroom.Classroom_Members]); + + if (!classroom.Classroom_Members) { + return

No members found!

; + } + + // const handleDelete = (memberId: string) => { + // console.log("ID:" + memberId); + // }; + + const removeMemberFunction = async (memberId: string) => { + await removeMember(classroom.id, memberId); + setMembers((prev) => prev.filter((member) => member.id !== memberId)); + }; + + // other table implementation: https://data-table.openstatus.dev/ + return ( + + {triggerButton ? ( + triggerButton + ) : ( + + + + )} + + {/* not used: https://github.com/shadcn-ui/ui/issues/16#issuecomment-1602565563 */} + + + {classroom.name} Members + {/* + Make changes to your profile here. Click save when + you're done. + */} + + {/* todo future, for smaller screens, make the width even smaller */} +
+ > }) => { + // const adminId = getCurrentUserId(); + const isAdmin = row.original.id === userId; // Check if the current row is the admin + + // Only render the delete button if the current row is NOT the admin + if (isAdmin) { + return null; // Return null, which removes the button from the row + } + + return ( + + + + + + Remove user + + + ); + }, + }, + ] + : []), + ]} + data={members} + /> +
+
+
+
+ ); +} diff --git a/app/classrooms/_components/saveClassroomDialog.tsx b/app/classrooms/_components/saveClassroomDialog.tsx new file mode 100644 index 0000000..d3eaff6 --- /dev/null +++ b/app/classrooms/_components/saveClassroomDialog.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { Button } from "@shared/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@shared/components/ui/dialog"; +import { Input } from "@shared/components/ui/input"; +import { Label } from "@shared/components/ui/label"; +import { DialogTrigger } from "@radix-ui/react-dialog"; +import { ReactNode, useState } from "react"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +export default function SaveClassroomDialog({ + optimisticUpdateCallback, + actionText, + dialogTrigger, +}: { + optimisticUpdateCallback: (name: string) => Promise; + actionText: string; + dialogTrigger?: ReactNode; +}) { + const [newClassName, setNewClassName] = useState(""); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isPending, setIsPending] = useState(false); + + const saveClassroomCallback = async () => { + setIsDialogOpen(false); + setIsPending(true); + // startTransition(async () => { + const result = await optimisticUpdateCallback(newClassName); + if (!result) { + toast.error( + `Uh oh! Something went wrong when attempting to ${actionText.toLocaleLowerCase()} the classroom.`, + { + description: `Please refresh and try again`, + } + ); + return; + } + toast.success(`${capitalize(actionText)}d classroom successfully!`); + setIsDialogOpen(false); + setIsPending(false); + setNewClassName(""); + // }); + return; + }; + + return ( + + {dialogTrigger ?? ( + + + + )} + + + {capitalize(actionText)} a Classroom + + Make changes to the name of your classroom here. + + +
+
+ + setNewClassName(e.target.value)} + /> +
+
+ + + +
+
+ ); +} + +const capitalize = (s: string) => + s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); diff --git a/app/classrooms/actions.ts b/app/classrooms/actions.ts new file mode 100644 index 0000000..9341938 --- /dev/null +++ b/app/classrooms/actions.ts @@ -0,0 +1,260 @@ +"use server"; +import { createServiceClient } from "@shared/utils/supabase/service-server"; +import { createClient } from "@shared/utils/supabase/server"; +import { deleteDataset } from "@shared/lib/ragflow/dataset-client"; +import { deleteAssistant } from "@shared/lib/ragflow/chat/chat-client"; + +// const RAGFLOW_SERVER_URL = process.env.RAGFLOW_API_URL || ""; +// const RAGFLOW_API_KEY = process.env.RAGFLOW_API_KEY; + +export async function deleteClassroom(classroom_id: number) { + // Deleting Associated Supabase + const supabase = await createClient(); + const { data, error } = await supabase + .from("Classrooms") + .delete() + .eq("id", classroom_id) + .select() + .single(); + + if (error) { + throw new Error(error.message); + } + + // Deleting Associated Chat Assistant + const chat_assistant_id = data.chat_assistant_id; + const chatroom_assistant_id = data.chatroom_assistant_id; + + if (chat_assistant_id) { + deleteAssistant(chat_assistant_id); + // const requestChatBody = { + // ids: [chat_assistant_id], + // }; + + // const chatResponse = await fetch(`${RAGFLOW_SERVER_URL}/api/v1/chats`, { + // method: "DELETE", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${RAGFLOW_API_KEY}`, + // }, + // body: JSON.stringify(requestChatBody), + // }); + + // if (!chatResponse.ok) { + // throw new Error( + // `Failed while deleting assistant from Ragflow: ${chatResponse.statusText}` + // ); + // } + } + if (chatroom_assistant_id) { + deleteAssistant(chatroom_assistant_id); + } + // else { + // // If no chat assistant, we don't want to error out + // console.log("No chat assistant found for classroom when deleting"); + // } + + // Deleting associated RAGFlow dataset if exists + if (data.ragflow_dataset_id) { + deleteDataset(data.id.toString(), data.ragflow_dataset_id); + } + // const ragflow_dataset_id = data.ragflow_dataset_id; + + // if (!ragflow_dataset_id) { + // throw new Error("No related RAGFlow dataset found for this classroom."); + // } + + // //gets ids of ragflow_dataset_id + // const requestBody = { + // ids: [ragflow_dataset_id], + // }; + + // //deletes the respective dataset + // const ragflowResponse = await fetch(`${RAGFLOW_SERVER_URL}/api/v1/datasets`, { + // method: "DELETE", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${RAGFLOW_API_KEY}`, + // }, + // body: JSON.stringify(requestBody), + // }); + + // if (!ragflowResponse.ok) { + // throw new Error( + // `Failed to delete dataset from Ragflow: ${ragflowResponse.statusText}` + // ); + // } + + return data || []; +} + +export async function removeMember(classroom_id: number, user_id: string) { + const supabase = await createServiceClient(); + const { data, error } = await supabase + .from("Classroom_Members") + .delete() + .eq("classroom_id", classroom_id) + .eq("user_id", user_id); + + if (error) { + throw new Error(error.message); + } + + return data || []; +} + +export async function leaveClassroom(classroom_id: number, user_id: string) { + const supabase = await createServiceClient(); + const { data, error } = await supabase + .from("Classroom_Members") + .delete() + .eq("classroom_id", classroom_id) + .eq("user_id", user_id); + if (error) { + throw new Error(error.message); + } + return data || []; +} + +// export async function getUserClassrooms() { +// const supabase = await createClient(); +// const { data, error } = await supabase.from("Classrooms").select(` +// *, +// Classroom_Members ( +// id, +// classroom_id, +// Users ( +// id, +// email, +// full_name, +// avatar_url +// ) +// ) +// `); +// if (error) { +// throw new Error(error.message); +// } +// return data || []; +// } + +export async function inviteMemberToClassroom( + email: string, + classroom_id: number +) { + const supabase = createServiceClient(); + + const { data: users, error: userError } = await supabase + .from("Users") + .select("id") + .eq("email", email) + .single(); + + if (userError || !users) { + throw new Error("User does not exist"); + } + + //checks for duplicate + const { data: member } = await supabase + .from("Classroom_Members") + .select("id") + .eq("classroom_id", classroom_id) + .eq("user_id", users.id); + + if (member && member.length > 0) { + throw new Error("User already in the classroom"); + } + + //insert if no errors + const { error: insertError } = await supabase + .from("Classroom_Members") + .insert({ + classroom_id: classroom_id, + user_id: users.id, + }); + + if (insertError) { + throw new Error("Error inserting classroom member"); + } + return true; +} + +export async function changeClassroomName( + classroom_id: number, + newName: string +) { + const supabase = await createServiceClient(); + const { data, error } = await supabase + .from("Classrooms") + .update({ name: newName }) + .eq("id", classroom_id) + .select(); + + if (error) { + console.log("Error changing name"); + } + + //console.log(data[0].name); + return data; +} + +export async function setArchiveStatusClassroom( + classroom_id: number, + status: boolean +) { + const supabase = await createServiceClient(); + + const { data, error } = await supabase + .from("Classrooms") + .update({ archived: status }) + .eq("id", classroom_id) + .select(); + + if (error) { + console.error("Error setting archive status classroom:", error); + return { success: false, error: error.message }; + } + + return { success: true, data }; +} + +export async function newClassroom(name: string, id: string) { + const supabase = await createServiceClient(); + const { data, error } = await supabase + .from("Classrooms") + .insert([{ name: name, admin_user_id: id, archived: false }]) + .select("id"); + + if (error) { + console.error("Error inserting classroom:", error); + return null; + } + + // add yourself to member list + if (data && data.length > 0) { + const classroomId = data[0].id; + console.log("Classroom ID:", classroomId); + const { error } = await supabase + .from("Classroom_Members") + .insert([{ classroom_id: classroomId, user_id: id }]) + .select(); + + if (error) { + console.error("Error inserting admin to classroom member list:", error); + return null; + } + } + + return data; +} + +// export async function getCurrentUserId() { +// const supabase = await createClient(); + +// const { +// data: { user }, +// } = await supabase.auth.getUser(); +// if (!user) { +// throw Error("No authenticated user found"); +// } +// return user.id; +// } diff --git a/app/classrooms/clientUtils.tsx b/app/classrooms/clientUtils.tsx new file mode 100644 index 0000000..dea7458 --- /dev/null +++ b/app/classrooms/clientUtils.tsx @@ -0,0 +1,82 @@ +"use client"; + +import React, { ReactNode } from "react"; +import { + ClassroomWithMembers, + UserWithClassroomsData, +} from "@shared/lib/userContext/contextFetcher"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@shared/components/ui/tooltip"; +import { SheetTrigger } from "@shared/components/ui/sheet"; + +/** + * Called "optimistic" because it changes the data in the UI (eg. the name or deletes + * the classroom) without waiting for it to see if the actual database was successful. + * So the flow: update the UI, call the action, refresh with the actual database data + * (which 99% of the time) will match what you optimistically update with anyway + * Check uses of this below + * @param classroomId classId to change + * @param action action callback to call, just provide an async + * @param newValue the value to optimistically update the classroom with + */ +export const optimisticUpdateAndFetchClassroomData = async < + K extends keyof ClassroomWithMembers, +>( + action: () => Promise, + newValue: + | { [k in K]: ClassroomWithMembers[k] } + | ClassroomWithMembers + | "remove", + setUserAndClassDataFunction: React.Dispatch< + React.SetStateAction + >, + classroomId?: number, + refreshFunction?: () => Promise +) => { + setUserAndClassDataFunction((prevData) => ({ + userData: prevData.userData, + classroomsData: prevData.classroomsData + .flatMap((classroom) => { + if (classroom.id === classroomId) { + return newValue === "remove" ? [] : { ...classroom, ...newValue }; + } + return classroom; + }) + .concat(typeof newValue === "object" && "id" in newValue ? newValue : []), + })); + const returnVal = await action(); + if (refreshFunction) { + refreshFunction(); + } + return returnVal; +}; + +export function TooltipUtil({ + content, + trigger, + delayDuration = 0, + useSheetTrigger = false, +}: { + content: ReactNode; + trigger: ReactNode; + delayDuration?: number; + useSheetTrigger?: boolean; +}) { + const tooltipTrigger = {trigger}; + return ( + + + {useSheetTrigger ? ( + {tooltipTrigger} + ) : ( + tooltipTrigger + )} + {content} + + + ); +} diff --git a/app/classroom/join/[code]/route.ts b/app/classrooms/join/[code]/route.ts similarity index 62% rename from app/classroom/join/[code]/route.ts rename to app/classrooms/join/[code]/route.ts index 6e30441..d600963 100644 --- a/app/classroom/join/[code]/route.ts +++ b/app/classrooms/join/[code]/route.ts @@ -1,6 +1,7 @@ -import { NextResponse, NextRequest } from "next/server"; -import { createServiceClient } from "@/utils/supabase/service-server"; -import { getCurrentUserId } from "../../actions"; +import { NextRequest } from "next/server"; +import { createServiceClient } from "@shared/utils/supabase/service-server"; +import { getCurrentUserIdServer } from "@shared/lib/supabase/shared"; +import { redirect } from "next/navigation"; export async function GET( request: NextRequest, @@ -21,17 +22,16 @@ export async function GET( if (classroomError || !classroom) { console.error("Classroom not found:", classroomError); - return NextResponse.redirect(new URL("/classroom", request.url)); + return redirect("/classrooms"); } //ensures that the user is authenticated - let userId: string; - try { - userId = await getCurrentUserId(); - } catch (err) { - console.error("User is not authenticated:", err); + const userId = await getCurrentUserIdServer(); + + if (!userId) { + console.error("User is not authenticated"); //login page - return NextResponse.redirect(new URL("/login", request.url)); + return redirect("/classrooms/login"); } //if the person is already in there, should be redirect to just classroom @@ -44,11 +44,11 @@ export async function GET( if (memberError) { console.error("Error checking membership:", memberError); - return NextResponse.redirect(new URL("/classroom", request.url)); + return redirect("/classrooms"); } if (existingMember) { - return NextResponse.redirect(new URL("/classroom", request.url)); + return redirect("/classrooms"); } const { error: insertError } = await supabase @@ -60,8 +60,12 @@ export async function GET( if (insertError) { console.error("Error adding member to classroom:", insertError); - return NextResponse.redirect(new URL("/classroom", request.url)); + return redirect("/classrooms"); } //redirect to classroom - return NextResponse.redirect(new URL("/classroom", request.url)); + + const successParams = new URLSearchParams({ + joinSuccess: classroom.id.toString(), + }); + return redirect(`/classrooms?${successParams.toString()}`); } diff --git a/app/classrooms/page.tsx b/app/classrooms/page.tsx new file mode 100644 index 0000000..f3aa33a --- /dev/null +++ b/app/classrooms/page.tsx @@ -0,0 +1,496 @@ +"use client"; +import { useContext, useEffect, useState } from "react"; +import { leaveClassroom, newClassroom } from "./actions"; +import Link from "next/link"; +import MemberList from "./_components/memberList"; +import { + ClassroomWithMembers, + getUserAndClassroomData, +} from "@shared/lib/userContext/contextFetcher"; +import { + UserContext, + UserContextType, +} from "@shared/lib/userContext/userContext"; +import { Skeleton } from "@shared/components/ui/skeleton"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@shared/components/ui/card"; +import { + optimisticUpdateAndFetchClassroomData, + TooltipUtil, +} from "./clientUtils"; + +import { useSearchParams } from "next/navigation"; +import { + Edit, + LogOut, + MessageSquareMore, + MessagesSquareIcon, + UserPlus, + Users, + FileText, +} from "lucide-react"; +import { Button } from "@shared/components/ui/button"; +import SaveClassroomDialog from "./_components/saveClassroomDialog"; +import { toast } from "sonner"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@/shared/components/ui/tabs"; +import { Separator } from "@/shared/components/ui/separator"; +import InviteInfoDialog from "./_components/invite-dialog"; +import JoinDialog from "./_components/join-dialog"; +import { DialogTrigger } from "@radix-ui/react-dialog"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/shared/components/ui/accordion"; +import { cn } from "@/shared/lib/utils"; + +export default function ClassroomPage() { + const userContext = useContext(UserContext); + // If the userContext is undefined still, give loading visual + if (!userContext) { + return ( +
+ +
+ + +
+
+ ); + } + + return ; +} + +function ClassroomList({ userContext }: { userContext: UserContextType }) { + const searchParams = useSearchParams(); + const [currentTab, setCurrentTab] = useState("enrolled"); + + // get the data and setter from the context (these are just a regular useState, so treat them like that) + const { setUserAndClassData, userAndClassData } = userContext; + + const userId = userAndClassData.userData.id; + + const refreshClassrooms = async () => { + const refreshedData = await getUserAndClassroomData(); + if (refreshedData) { + setUserAndClassData(refreshedData); + } + }; + + useEffect(() => { + const tab = searchParams.get("tab"); + if (tab && (tab == "enrolled" || tab == "admin")) { + setCurrentTab(tab); + } else { + setCurrentTab("enrolled"); + } + }, [searchParams]); + + useEffect(() => { + const joinedClassSuccess = searchParams.get("joinSuccess"); + console.log(joinedClassSuccess); + if (joinedClassSuccess && !isNaN(Number(joinedClassSuccess))) { + const joinClassInfo = userAndClassData.classroomsData.find( + (x) => x.id === Number(joinedClassSuccess) + ); + if (!joinClassInfo) { + refreshClassrooms(); + return; + } + if (joinClassInfo) { + // Join class doesn't need to refresh classroom data since we know its fresh + // since it's coming from a redirect from a reoute + // router.replace("/classroom", { scroll: false }); + // https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate + + if (typeof window !== "undefined") { + console.log("replacing and ", window.history.state, [ + ...searchParams.entries(), + ]); + window.history.replaceState(null, "", "/classrooms"); + + toast.success( +
+ Successfully joined classroom + {joinClassInfo.name}! +
, + { duration: 10000 } + ); + } + } + } + // const deleteSuccess = searchParams.get("delete_success"); + // if (deleteSuccess && !isNaN(Number(deleteSuccess))) { + // if (typeof window !== "undefined") { + // window.history.replaceState(null, "", "/classroom"); + // } + // optimisticUpdateAndFetchClassroomData( + // async () => undefined, + // "remove", + // setUserAndClassData, + // Number(deleteSuccess), + // refreshClassrooms + // ); + // } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams, userAndClassData.classroomsData]); + + // const archiveClassSuccess = searchParams.get("archiveClassSuccess"); + + // if (archiveClassSuccess && !isNaN(Number(archiveClassSuccess))) { + // const archiveClassInfo = userAndClassData.classroomsData.find( + // (x) => x.id === Number(archiveClassSuccess) + // ); + // if (archiveClassInfo) { + // toast({ + // description: ( + //
+ // Successfully archived classroom + // {archiveClassInfo.name}! + //
+ // ), + // duration: 10000, + // }); + // refreshClassrooms(); + // } + // } + + // }, [searchParams]); + + const adminClasses = userAndClassData?.classroomsData + .filter((classroom) => classroom.admin_user_id == userId) + .sort((x, y) => + (x?.name ?? "").toLowerCase().localeCompare((y?.name ?? "").toLowerCase()) + ); + + const memberClasses = userAndClassData?.classroomsData + .filter((classroom) => classroom.admin_user_id != userId) + .sort((x, y) => + (x?.name ?? "").toLowerCase().localeCompare((y?.name ?? "").toLowerCase()) + ); + + const addOptimistic = async (newName: string) => { + return await optimisticUpdateAndFetchClassroomData( + async () => newClassroom(newName, userId), + { name: newName, archived: false, admin_user_id: userId, id: -1 }, + setUserAndClassData, + undefined, + refreshClassrooms + ); + }; + + const leaveOptimistic = async (classroomId: number) => { + return await optimisticUpdateAndFetchClassroomData( + async () => leaveClassroom(classroomId, userId), + "remove", + setUserAndClassData, + classroomId, + refreshClassrooms + ); + }; + + function ClassroomCard({ + classroom, + isAdmin, + }: { + classroom: ClassroomWithMembers; + isAdmin: boolean; + }) { + return ( + + + + {classroom.name} + + + +
+ Created:{" "} + {new Date(classroom.created_at).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }) || } +
+
+
+ +
+ + +
+ + + + + + } + content={"Chat!"} + /> + + + + + + } + content={"Chatrooms"} + /> + + {classroom.Classroom_Members && + classroom.Classroom_Members.length > 0 && ( + + + + } + content={"View Members"} + useSheetTrigger + /> + } + userId={userId} + /> + )} + + + + + + + } + content={"Augment Notes"} + /> +
+ {isAdmin ? ( + + + + + + } + content={"Manage Classroom"} + /> + ) : ( + leaveOptimistic(classroom.id)} + className="ml-auto" + > + + + } + content={"Leave Classroom"} + /> + )} +
+ + ); + } + + function ArchivedSections({ + archClassrooms, + isAdmin, + }: { + archClassrooms: ClassroomWithMembers[]; + isAdmin: boolean; + }) { + console.log(archClassrooms); + if (!archClassrooms || archClassrooms.length <= 0) { + return; + } + return ( + + + +

+ Archived Classrooms +

+
+ +
    + {archClassrooms.map((cm) => ( +
  • +
    + {cm.name} +

    + Created:{" "} + {new Date(cm.created_at).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + })} +

    +
    + {isAdmin && ( + + )} +
  • + ))} +
+
+
+
+ ); + } + + return ( +
+ {/*

My Classrooms

+

Admin Classrooms

*/} + {/* ADMIN CLASSES */} +

Classrooms

+ + { + window.history.replaceState(null, "", `/classrooms?tab=${value}`); + setCurrentTab(value); + }} + defaultValue="enrolled" + // className="w-[75vw] bg-" + > +
+ + + Enrolled + + + Admin + + +
+ + + + + } + /> +
+
+ + +
+
+ {adminClasses + .filter((x) => !x.archived) + .map((classroom) => ( + + ))} +
+
+ x.archived)} + isAdmin + /> +
+ +
+
+ {memberClasses + .filter((x) => !x.archived) + .map((classroom) => ( + + ))} +
+
+ x.archived)} + isAdmin={false} + /> +
+
+
+ ); +} diff --git a/app/auth/unauthorized/page.tsx b/app/error/unauthorized/page.tsx similarity index 61% rename from app/auth/unauthorized/page.tsx rename to app/error/unauthorized/page.tsx index 131fb42..0fb2ff4 100644 --- a/app/auth/unauthorized/page.tsx +++ b/app/error/unauthorized/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useSearchParams } from "next/navigation"; import Link from "next/link"; import { Suspense } from "react"; @@ -15,19 +14,10 @@ export default function UnauthorizedPage() { } function AccessDeniedWithMessage() { - const searchParams = useSearchParams(); - const message = searchParams.get("message"); - - let errorMessage = "You are not authorized to access this page."; - - if (message === "ORGANIZATION_EMAIL_REQUIRED") { - errorMessage = `This application is only available to ${orgName}members with an approved email domain.`; - } - return (

Access Denied

-

{errorMessage}

+

{`This application is only available to ${orgName}members with an approved email domain.`}

Z+y5Ju@H9B<@tr~({pDF_|91C+uNTmnLufzY7?6#&2QhSWe5Pa=gA>GR<8 zJYuofzgx|C7(Vgu;bGuw!}l-4@Oc=9uYhX^Ab(fiK;J;$K;J;$K;OV+1A%XWbh+Kv z#~4?$JLKu)xElo~)!b5q@nYT2CoyWM#x2-Z-TyUHvw20vuBH9!H~9)S`MyZu;LkZQ$6A=Q@OrZaFl6fkI!k(^r!EOIqxT=+)r>LC&o7^l-XTjV6G=B=b! zaZzlZRi|W7CS?tb>>{h5Vf33s2$h_0a~?n{y?J)3G{md=>N&HwRL{QY4!k` z{|`KZd0s)p% zExqYHO6oQ4Z05vk&b2(lpCE(7M=R1`ZH_H{HD>`|2@d%5wzakqEA}dqu(iipZO54* zfk7?fqPGGL*A#h}<8qRhS7KG8fa=YfZM}&#InG=?BMwqPgF&87ju#4e>i%(Yf03|o?2TWfuU=KNz+7ABYno%7pbR>`@?Cea@EV-aETyb|^AVR-%7+=L{{m?`-)j&AH`go_!bP>u;d*Xc-MP udw>=>$@&edl@3JxBHT(wp>PmRpRT@vzJb1hzJb1hzJb1hzJdRA1AhStq_bxL literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m diff --git a/app/globals.css b/app/globals.css index f5bd287..493cde3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,103 +5,72 @@ @layer base { :root { --background: 0 0% 100%; - - --foreground: 0 0% 3.9%; - + --foreground: 240 10% 3.9%; --card: 0 0% 100%; - - --card-foreground: 0 0% 3.9%; - + --card-foreground: 240 10% 3.9%; --popover: 0 0% 100%; - - --popover-foreground: 0 0% 3.9%; - - --primary: 0 0% 9%; - + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; --primary-foreground: 0 0% 98%; - - --secondary: 0 0% 96.1%; - - --secondary-foreground: 0 0% 9%; - - --muted: 0 0% 96.1%; - - --muted-foreground: 0 0% 45.1%; - - --accent: 0 0% 96.1%; - - --accent-foreground: 0 0% 9%; - - --destructive: 0 84.2% 60.2%; - + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240, 5%, 85%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 72.22% 50.59%; --destructive-foreground: 0 0% 98%; - - --border: 0 0% 89.8%; - - --input: 0 0% 89.8%; - - --ring: 0 0% 3.9%; - + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5% 64.9%; + --radius: 0.5rem; --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - - --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240, 5%, 85%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 240 5% 64.9%; } .dark { - --background: 0 0% 3.9%; - + --background: 240 10% 3.9%; --foreground: 0 0% 98%; - - --card: 0 0% 3.9%; - + --card: 240 10% 3.9%; --card-foreground: 0 0% 98%; - - --popover: 0 0% 3.9%; - + --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - - --primary-foreground: 0 0% 9%; - - --secondary: 0 0% 14.9%; - + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; - - --muted: 0 0% 14.9%; - - --muted-foreground: 0 0% 63.9%; - - --accent: 0 0% 14.9%; - + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 5% 40%; --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - - --destructive-foreground: 0 0% 98%; - - --border: 0 0% 14.9%; - - --input: 0 0% 14.9%; - - --ring: 0 0% 83.1%; - + --destructive-foreground: 0 85.7% 97.3%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 5% 40%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 240 4.9% 83.9%; } } diff --git a/app/layout.tsx b/app/layout.tsx index 78d107e..8ffa535 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,20 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { Toaster } from "@/components/ui/toaster"; + +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@shared/components/ui/sidebar"; +import { ThemeProvider } from "@shared/components/ui/theme-provider"; +import { AppSidebar } from "@shared/components/ui/sidebar/app-sidebar"; +import UserContextProvider from "@shared/lib/userContext/userContext"; +import { getUserAndClassroomData } from "@shared/lib/userContext/contextFetcher"; +import { cn } from "@shared/lib/utils"; +import { Separator } from "@shared/components/ui/separator"; +import { Toaster } from "@/shared/components/ui/sonner"; +import { BackButton } from "@/shared/components/ui/sidebar/back-button"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -18,18 +31,71 @@ export const metadata: Metadata = { description: "Generated by create next app", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const userData = await getUserAndClassroomData(); + // TODO: change this? + return ( -
{children}
- + + {userData ? ( + + + + + +
+
+
+ + + +
+
+
+ {children} +
+
+
+
+
+ ) : ( +
{children}
+ )} +
); diff --git a/app/lib/utils.ts b/app/lib/utils.ts deleted file mode 100644 index 2f435ff..0000000 --- a/app/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: unknown[]) { - return twMerge(clsx(inputs)); -} diff --git a/app/auth/actions.ts b/app/login/actions.ts similarity index 86% rename from app/auth/actions.ts rename to app/login/actions.ts index 96bb775..81ab29d 100644 --- a/app/auth/actions.ts +++ b/app/login/actions.ts @@ -1,7 +1,7 @@ "use server"; import { redirect } from "next/navigation"; -import { createClient } from "@/utils/supabase/server"; +import { createClient } from "@shared/utils/supabase/server"; import { revalidatePath } from "next/cache"; export async function signInWithGoogle() { @@ -11,7 +11,7 @@ export async function signInWithGoogle() { const { data, error } = await supabase.auth.signInWithOAuth({ provider: "google", options: { - redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`, queryParams: { prompt: "select_account", ...(hostedDomain && { hd: hostedDomain }), // only add if orgName defined diff --git a/app/login/page.tsx b/app/login/page.tsx index 9bf26d5..9c943b1 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,13 +1,37 @@ -import { signInWithGoogle } from "../auth/actions"; -import GoogleSignInButton from "@/components/GoogleSignInButton"; +import { signInWithGoogle } from "@/app/login/actions"; +import GoogleSignInButton from "@/shared/components/GoogleSignInButton"; export default async function LoginPage() { return ( -
-
-
- - +
+
+
+

+ Learn and grow +

+

+ with your personalized classroom agent +

+
+
+

+ A collaborative space for students and teachers to interact with the + future of LLM-enhanced education. +

+ {/* */} +
+ + +
); diff --git a/app/page.tsx b/app/page.tsx index 740c557..162f0f6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,11 +1,41 @@ -import { logout } from "@/app/auth/actions"; -import Image from "next/image"; +import { Button } from "@/shared/components/ui/button"; +import { ArrowRightCircle } from "lucide-react"; +import Link from "next/link"; export default function Home() { return ( -
-
- +
+
+

+ Learn and grow +

+

+ with your personalized classroom agent +

+
+
+

+ A collaborative space for students and teachers to interact with the + future of LLM-enhanced education. +

+ +
+
+
+ ); + + { + /* Next.js logo My classrooms - - My chatrooms - -
- -
+
*/ + } + + { + /* -
- ); + */ + } } diff --git a/components.json b/components.json index 13f24bf..913f07a 100644 --- a/components.json +++ b/components.json @@ -11,11 +11,11 @@ "prefix": "" }, "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" + "components": "@shared/components", + "utils": "@shared/lib/utils", + "ui": "@shared/components/ui", + "lib": "@shared/lib", + "hooks": "@shared/hooks" }, "iconLibrary": "lucide" } diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx deleted file mode 100644 index dbd2202..0000000 --- a/components/ui/toast.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as ToastPrimitives from "@radix-ui/react-toast"; -import { cva, type VariantProps } from "class-variance-authority"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ); -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef; - -type ToastActionElement = React.ReactElement; - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -}; diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx deleted file mode 100644 index 364b797..0000000 --- a/components/ui/toaster.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { useToast } from "@/hooks/use-toast"; -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast"; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ); - })} - -
- ); -} diff --git a/eslint.config.mjs b/eslint.config.mjs index 81740c0..96fc19a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,9 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript", "prettier"), + { + ignores: ["*lock*", "**/node_modules/**"], + }, ]; export default eslintConfig; diff --git a/hooks/use-toast.ts b/hooks/use-toast.ts deleted file mode 100644 index b140bf6..0000000 --- a/hooks/use-toast.ts +++ /dev/null @@ -1,192 +0,0 @@ -"use client"; - -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/middleware.ts b/middleware.ts index 1c26edc..f05927c 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,5 @@ import { type NextRequest } from "next/server"; -import { updateSession } from "@/utils/supabase/middleware"; +import { updateSession } from "@shared/utils/supabase/middleware"; export async function middleware(request: NextRequest) { return await updateSession(request); diff --git a/next.config.ts b/next.config.ts index f218bf8..05a5f1a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -13,6 +13,11 @@ const nextConfig: NextConfig = { }, ], }, + experimental: { + serverActions: { + bodySizeLimit: "10mb", + }, + }, }; export default nextConfig; diff --git a/package.json b/package.json index 4826f48..acefa50 100644 --- a/package.json +++ b/package.json @@ -18,25 +18,38 @@ "format:check": "prettier --check ." }, "dependencies": { - "@headlessui/react": "^2.2.0", + "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.49.1", "@tanstack/react-table": "^8.21.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "input-otp": "^1.4.2", "lucide-react": "^0.483.0", + "motion": "^12.6.3", "next": "15.1.7", + "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "sonner": "^2.0.3", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3", + "@tailwindcss/typography": "^0.5.16", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.2.0", "@types/node": "^20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a43b6a..a23cadf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,21 +8,39 @@ importers: .: dependencies: - '@headlessui/react': - specifier: ^2.2.0 - version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-accordion': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-avatar': specifier: ^1.1.3 - version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collapsible': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.6 - version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.6 + version: 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-label': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-scroll-area': specifier: ^1.2.3 - version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-toast': - specifier: ^1.2.6 - version: 1.2.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-separator': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': + specifier: ^1.1.2 + version: 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-tabs': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-tooltip': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@supabase/ssr': specifier: ^0.5.2 version: 0.5.2(@supabase/supabase-js@2.49.1) @@ -38,18 +56,36 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) lucide-react: specifier: ^0.483.0 version: 0.483.0(react@19.0.0) + motion: + specifier: ^12.6.3 + version: 12.6.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.0.12)(react@19.0.0) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tailwind-merge: specifier: ^3.0.2 version: 3.0.2 @@ -60,21 +96,24 @@ importers: '@eslint/eslintrc': specifier: ^3 version: 3.2.0 + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@3.4.17) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 '@testing-library/react': specifier: ^16.2.0 - version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/node': specifier: ^20 version: 20.17.19 '@types/react': specifier: ^19 - version: 19.0.10 + version: 19.0.12 '@types/react-dom': specifier: ^19 - version: 19.0.4(@types/react@19.0.10) + version: 19.0.4(@types/react@19.0.12) '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.3.4(vite@6.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0)) @@ -125,7 +164,7 @@ importers: version: 5.1.4(typescript@5.7.3)(vite@6.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0)) vitest: specifier: ^3.0.7 - version: 3.0.7(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0) + version: 3.0.7(@types/debug@4.1.12)(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0) packages: @@ -458,22 +497,9 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.26.28': - resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@headlessui/react@2.2.0': - resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} - engines: {node: '>=10'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - react-dom: ^18 || ^19 || ^19.0.0-rc - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -712,6 +738,35 @@ packages: '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-accordion@1.2.4': + resolution: {integrity: sha512-SGCxlSBaMvEzDROzyZjsVNzu9XY5E28B3k8jOENyrz6csOv/pG1eHyYfLJai1n9tRjwG61coXDhfpgtxKxUv5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.2': + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-avatar@1.1.3': resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==} peerDependencies: @@ -725,6 +780,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.3': + resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.4': + resolution: {integrity: sha512-u7LCw1EYInQtBNLGjm9nZ89S/4GcvX1UR5XbekEgnQae2Hkpq39ycJ1OhdeN1/JDfVNG91kWaWoest127TaEKQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -738,6 +819,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.3': + resolution: {integrity: sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.1': resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} peerDependencies: @@ -747,6 +841,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: @@ -756,6 +859,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.6': resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==} peerDependencies: @@ -778,6 +890,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.5': resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} peerDependencies: @@ -791,6 +912,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.6': + resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -822,6 +956,54 @@ packages: '@types/react': optional: true + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.2': + resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.6': + resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.2': + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.4': resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} peerDependencies: @@ -848,6 +1030,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.3': + resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.2': resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} peerDependencies: @@ -861,6 +1056,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.3': + resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.2': + resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-scroll-area@1.2.3': resolution: {integrity: sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==} peerDependencies: @@ -874,6 +1095,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-separator@1.1.2': + resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.2': resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} peerDependencies: @@ -883,8 +1117,30 @@ packages: '@types/react': optional: true - '@radix-ui/react-toast@1.2.6': - resolution: {integrity: sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==} + '@radix-ui/react-slot@1.2.0': + resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.3': + resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.1.8': + resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -905,6 +1161,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.1.0': resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: @@ -914,6 +1179,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.1.1': + resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} peerDependencies: @@ -932,55 +1206,48 @@ packages: '@types/react': optional: true - '@radix-ui/react-visually-hidden@1.1.2': - resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: '@types/react': '*' - '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - '@types/react-dom': - optional: true - - '@react-aria/focus@3.20.1': - resolution: {integrity: sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/interactions@3.24.1': - resolution: {integrity: sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==} + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/ssr@3.9.7': - resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} - engines: {node: '>= 12'} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@react-aria/utils@3.28.1': - resolution: {integrity: sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==} + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/flags@3.1.0': - resolution: {integrity: sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==} + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@react-stately/utils@3.10.5': - resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==} + '@radix-ui/react-visually-hidden@1.1.2': + resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@react-types/shared@3.28.0': - resolution: {integrity: sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} '@rollup/rollup-android-arm-eabi@4.34.8': resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} @@ -1116,6 +1383,11 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tanstack/react-table@8.21.2': resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} engines: {node: '>=12'} @@ -1123,19 +1395,10 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.6': - resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/table-core@8.21.2': resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.6': - resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==} - '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -1173,15 +1436,30 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.17.19': resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==} @@ -1193,8 +1471,14 @@ packages: peerDependencies: '@types/react': ^19.0.0 - '@types/react@19.0.10': - resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==} + '@types/react@19.0.12': + resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@types/ws@8.5.14': resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} @@ -1246,6 +1530,9 @@ packages: resolution: {integrity: sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@4.3.4': resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1418,6 +1705,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1475,6 +1765,9 @@ packages: caniuse-lite@1.0.30001700: resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} @@ -1487,6 +1780,18 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -1521,6 +1826,12 @@ packages: resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==} engines: {node: ^18.17.0 || >=20.5.0} + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1542,6 +1853,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} @@ -1619,6 +1933,9 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1649,6 +1966,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1859,6 +2179,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1877,6 +2200,9 @@ packages: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1947,6 +2273,20 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + framer-motion@12.6.3: + resolution: {integrity: sha512-2hsqknz23aloK85bzMc9nSR2/JP+fValQ459ZTVElFQ0xgwR2YqNjYSuDZdFBPOwVCt4Q9jgyTt6hg6sVOALzw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2057,6 +2397,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -2064,6 +2410,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2097,10 +2446,25 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + input-otp@1.4.2: + resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2143,6 +2507,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2171,6 +2538,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2183,6 +2553,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2336,6 +2710,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2343,6 +2723,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2379,6 +2762,30 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2386,6 +2793,69 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2429,6 +2899,26 @@ packages: engines: {node: '>=10'} hasBin: true + motion-dom@12.6.3: + resolution: {integrity: sha512-gRY08RjcnzgFYLemUZ1lo/e9RkBxR+6d4BRvoeZDSeArG4XQXERSPapKl3LNQRu22Sndjf1h+iavgY0O4NrYqA==} + + motion-utils@12.6.3: + resolution: {integrity: sha512-R/b3Ia2VxtTNZ4LTEO5pKYau1OUNHOuUfxuP0WFCTDYdHkeTBR9UtxR1cc8mDmKr8PEhmmfnTKGz3rSMjNRoRg==} + + motion@12.6.3: + resolution: {integrity: sha512-zw/vqUgv5F5m9fkvOl/eCv2AF1+tkeZl3fu2uIlisIaip8sm5e0CouAl6GkdiRoF+G7s29CjqMdIyPMirwUGHA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -2447,6 +2937,12 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@15.1.7: resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -2561,6 +3057,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} @@ -2648,6 +3147,10 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -2738,6 +3241,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2756,6 +3262,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -2816,6 +3328,12 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2951,10 +3469,19 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -3007,6 +3534,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3027,6 +3557,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.16: + resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -3061,9 +3597,6 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - tailwind-merge@3.0.2: resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} @@ -3148,6 +3681,12 @@ packages: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.0.1: resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} engines: {node: '>=18.12'} @@ -3205,6 +3744,24 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -3237,6 +3794,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.0.7: resolution: {integrity: sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3429,6 +3992,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3723,25 +4289,8 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@floating-ui/react@0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': - dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@floating-ui/utils': 0.2.9 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - tabbable: 6.2.0 - '@floating-ui/utils@0.2.9': {} - '@headlessui/react@2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': - dependencies: - '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/focus': 3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@tanstack/react-virtual': 3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -3909,269 +4458,488 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@polka/url@1.0.0-next.28': {} + '@polka/url@1.0.0-next.28': {} + + '@radix-ui/number@1.1.0': {} + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-accordion@1.2.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collapsible': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collection': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-collapsible@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-collapsible@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-collection@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.0(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 + + '@radix-ui/react-context@1.1.1(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 + + '@radix-ui/react-context@1.1.2(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 + + '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.3(@types/react@19.0.12)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-direction@1.1.0(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 - '@radix-ui/number@1.1.0': {} + '@radix-ui/react-direction@1.1.1(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 - '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-menu': 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.12)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 - '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-id@1.1.0(@types/react@19.0.12)(react@19.0.0)': dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - '@radix-ui/react-context@1.1.1(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-id@1.1.1(@types/react@19.0.12)(react@19.0.0)': dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-label@2.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) - aria-hidden: 1.2.4 + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-direction@1.1.0(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) + aria-hidden: 1.2.4 react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.3(@types/react@19.0.12)(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-arrow': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/rect': 1.1.0 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-id@1.1.0(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-presence@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-primitive@2.0.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-slot': 1.2.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-scroll-area@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-scroll-area@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.0 '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-direction': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-slot@1.1.2(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-toast@1.2.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-slot@1.1.2(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 - '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-slot@1.2.0(@types/react@19.0.12)(react@19.0.0)': dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-tabs@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.10)(react@19.0.0)': + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.12)(react@19.0.0)': dependencies: react: 19.0.0 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 - '@react-aria/focus@3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-types/shared': 3.28.0(react@19.0.0) - '@swc/helpers': 0.5.15 - clsx: 2.1.1 + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 - '@react-aria/interactions@3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-use-controllable-state@1.1.1(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) - '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-stately/flags': 3.1.0 - '@react-types/shared': 3.28.0(react@19.0.0) - '@swc/helpers': 0.5.15 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 - '@react-aria/ssr@3.9.7(react@19.0.0)': + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.15 + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 - '@react-aria/utils@3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) - '@react-stately/flags': 3.1.0 - '@react-stately/utils': 3.10.5(react@19.0.0) - '@react-types/shared': 3.28.0(react@19.0.0) - '@swc/helpers': 0.5.15 - clsx: 2.1.1 react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 - '@react-stately/flags@3.1.0': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.15 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 - '@react-stately/utils@3.10.5(react@19.0.0)': + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.12)(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.15 + '@radix-ui/rect': 1.1.0 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 + + '@radix-ui/react-use-size@1.1.0(@types/react@19.0.12)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0) react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.12 - '@react-types/shared@3.28.0(react@19.0.0)': + '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + + '@radix-ui/rect@1.1.0': {} '@rollup/rollup-android-arm-eabi@4.34.8': optional: true @@ -4288,22 +5056,22 @@ snapshots: dependencies: tslib: 2.8.1 - '@tanstack/react-table@8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': dependencies: - '@tanstack/table-core': 8.21.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.17 - '@tanstack/react-virtual@3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-table@8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@tanstack/virtual-core': 3.13.6 + '@tanstack/table-core': 8.21.2 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) '@tanstack/table-core@8.21.2': {} - '@tanstack/virtual-core@3.13.6': {} - '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 @@ -4315,15 +5083,15 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.9 '@testing-library/dom': 10.4.0 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 - '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) '@types/aria-query@5.0.4': {} @@ -4350,26 +5118,48 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + '@types/estree@1.0.6': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@20.17.19': dependencies: undici-types: 6.19.8 '@types/phoenix@1.6.6': {} - '@types/react-dom@19.0.4(@types/react@19.0.10)': + '@types/react-dom@19.0.4(@types/react@19.0.12)': dependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - '@types/react@19.0.10': + '@types/react@19.0.12': dependencies: csstype: 3.1.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/ws@8.5.14': dependencies: '@types/node': 20.17.19 @@ -4451,6 +5241,8 @@ snapshots: '@typescript-eslint/types': 8.24.1 eslint-visitor-keys: 4.2.0 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@4.3.4(vite@6.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.9 @@ -4476,7 +5268,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.7(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0) + vitest: 3.0.7(@types/debug@4.1.12)(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -4523,7 +5315,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.12 tinyrainbow: 2.0.0 - vitest: 3.0.7(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0) + vitest: 3.0.7(@types/debug@4.1.12)(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0) '@vitest/utils@3.0.7': dependencies: @@ -4663,6 +5455,8 @@ snapshots: axobject-query@4.1.0: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} bin-links@5.0.0: @@ -4724,6 +5518,8 @@ snapshots: caniuse-lite@1.0.30001700: {} + ccount@2.0.1: {} + chai@5.2.0: dependencies: assertion-error: 2.0.1 @@ -4739,6 +5535,14 @@ snapshots: chalk@5.4.1: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + check-error@2.1.1: {} chokidar@3.6.0: @@ -4774,6 +5578,18 @@ snapshots: cmd-shim@7.0.0: {} + cmdk@1.1.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.12)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4798,6 +5614,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@13.1.0: {} commander@4.1.1: {} @@ -4860,6 +5678,10 @@ snapshots: decimal.js@10.5.0: {} + decode-named-character-reference@1.1.0: + dependencies: + character-entities: 2.0.2 + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -4885,6 +5707,10 @@ snapshots: detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -5250,6 +6076,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -5272,6 +6100,8 @@ snapshots: expect-type@1.1.0: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -5349,6 +6179,15 @@ snapshots: dependencies: fetch-blob: 3.2.0 + framer-motion@12.6.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + motion-dom: 12.6.3 + motion-utils: 12.6.3 + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + fsevents@2.3.3: optional: true @@ -5457,12 +6296,38 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.16 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 @@ -5494,12 +6359,26 @@ snapshots: imurmurhash@0.1.4: {} + inline-style-parser@0.2.4: {} + + input-otp@1.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -5551,6 +6430,8 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -5576,6 +6457,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-number-object@1.1.1: @@ -5585,6 +6468,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-regex@1.2.1: @@ -5772,6 +6657,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.castarray@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + lodash.merge@4.6.2: {} log-update@6.1.0: @@ -5782,6 +6671,8 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5816,10 +6707,232 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge-stream@2.0.0: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.0 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5854,6 +6967,20 @@ snapshots: mkdirp@3.0.1: {} + motion-dom@12.6.3: + dependencies: + motion-utils: 12.6.3 + + motion-utils@12.6.3: {} + + motion@12.6.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + framer-motion: 12.6.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + mrmime@2.0.1: {} ms@2.1.3: {} @@ -5868,6 +6995,11 @@ snapshots: natural-compare@1.4.0: {} + next-themes@0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + next@15.1.7(@babel/core@7.26.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.1.7 @@ -5993,6 +7125,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.1.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse5@7.2.1: dependencies: entities: 4.5.0 @@ -6052,6 +7194,11 @@ snapshots: postcss: 8.5.2 postcss-selector-parser: 6.1.2 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -6093,6 +7240,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.0.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -6106,34 +7255,52 @@ snapshots: react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@19.0.12)(react@19.0.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.0.12 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.8(@types/react@19.0.10)(react@19.0.0): + react-remove-scroll-bar@2.3.8(@types/react@19.0.12)(react@19.0.0): dependencies: react: 19.0.0 - react-style-singleton: 2.2.3(@types/react@19.0.10)(react@19.0.0) + react-style-singleton: 2.2.3(@types/react@19.0.12)(react@19.0.0) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - react-remove-scroll@2.6.3(@types/react@19.0.10)(react@19.0.0): + react-remove-scroll@2.6.3(@types/react@19.0.12)(react@19.0.0): dependencies: react: 19.0.0 - react-remove-scroll-bar: 2.3.8(@types/react@19.0.10)(react@19.0.0) - react-style-singleton: 2.2.3(@types/react@19.0.10)(react@19.0.0) + react-remove-scroll-bar: 2.3.8(@types/react@19.0.12)(react@19.0.0) + react-style-singleton: 2.2.3(@types/react@19.0.12)(react@19.0.0) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.0.10)(react@19.0.0) - use-sidecar: 1.1.3(@types/react@19.0.10)(react@19.0.0) + use-callback-ref: 1.3.3(@types/react@19.0.12)(react@19.0.0) + use-sidecar: 1.1.3(@types/react@19.0.12)(react@19.0.0) optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0): + react-style-singleton@2.2.3(@types/react@19.0.12)(react@19.0.0): dependencies: get-nonce: 1.0.1 react: 19.0.0 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 react@19.0.0: {} @@ -6169,6 +7336,23 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -6368,8 +7552,15 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + sonner@2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + stable-hash@0.0.4: {} stackback@0.0.2: {} @@ -6448,6 +7639,11 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -6462,6 +7658,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.16: + dependencies: + style-to-object: 1.0.8 + + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-jsx@5.1.6(@babel/core@7.26.9)(react@19.0.0): dependencies: client-only: 0.0.1 @@ -6496,8 +7700,6 @@ snapshots: symbol-tree@3.2.4: {} - tabbable@6.2.0: {} - tailwind-merge@3.0.2: {} tailwindcss-animate@1.0.7(tailwindcss@3.4.17): @@ -6598,6 +7800,10 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.0.1(typescript@5.7.3): dependencies: typescript: 5.7.3 @@ -6665,6 +7871,39 @@ snapshots: undici-types@6.19.8: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -6675,23 +7914,33 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0): + use-callback-ref@1.3.3(@types/react@19.0.12)(react@19.0.0): dependencies: react: 19.0.0 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 - use-sidecar@1.1.3(@types/react@19.0.10)(react@19.0.0): + use-sidecar@1.1.3(@types/react@19.0.12)(react@19.0.0): dependencies: detect-node-es: 1.1.0 react: 19.0.0 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.0.10 + '@types/react': 19.0.12 util-deprecate@1.0.2: {} + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + vite-node@3.0.7(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0): dependencies: cac: 6.7.14 @@ -6735,7 +7984,7 @@ snapshots: jiti: 1.21.7 yaml: 2.7.0 - vitest@3.0.7(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0): + vitest@3.0.7(@types/debug@4.1.12)(@types/node@20.17.19)(@vitest/ui@3.0.7)(jiti@1.21.7)(jsdom@26.0.0)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.7 '@vitest/mocker': 3.0.7(vite@6.1.1(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0)) @@ -6758,6 +8007,7 @@ snapshots: vite-node: 3.0.7(@types/node@20.17.19)(jiti@1.21.7)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.12 '@types/node': 20.17.19 '@vitest/ui': 3.0.7(vitest@3.0.7) jsdom: 26.0.0 @@ -6888,3 +8138,5 @@ snapshots: yaml@2.7.0: {} yocto-queue@0.1.0: {} + + zwitch@2.0.4: {} diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..82c11aa --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,12 @@ + + + {" "} + + + + + + \ No newline at end of file diff --git a/components/GoogleSignInButton.tsx b/shared/components/GoogleSignInButton.tsx similarity index 100% rename from components/GoogleSignInButton.tsx rename to shared/components/GoogleSignInButton.tsx diff --git a/shared/components/Logo.tsx b/shared/components/Logo.tsx new file mode 100644 index 0000000..b14748b --- /dev/null +++ b/shared/components/Logo.tsx @@ -0,0 +1,18 @@ +// Converted using https://react-svgr.com/playground/ + +import * as React from "react"; +const LogoComponent = ({ className }: { className: string }) => ( + + {" "} + + + + + + +); +export default LogoComponent; diff --git a/shared/components/ui/accordion.tsx b/shared/components/ui/accordion.tsx new file mode 100644 index 0000000..dbbc400 --- /dev/null +++ b/shared/components/ui/accordion.tsx @@ -0,0 +1,65 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@shared/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + chevronClassName?: string; + } +>(({ className, children, chevronClassName, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/shared/components/ui/alert-dialog.tsx b/shared/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..a72cb2f --- /dev/null +++ b/shared/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@shared/lib/utils"; +import { buttonVariants } from "@shared/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/components/ui/avatar.tsx b/shared/components/ui/avatar.tsx similarity index 97% rename from components/ui/avatar.tsx rename to shared/components/ui/avatar.tsx index d260623..82084cd 100644 --- a/components/ui/avatar.tsx +++ b/shared/components/ui/avatar.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@/lib/utils"; +import { cn } from "@shared/lib/utils"; const Avatar = React.forwardRef< React.ElementRef, diff --git a/shared/components/ui/badge.tsx b/shared/components/ui/badge.tsx new file mode 100644 index 0000000..754ea4b --- /dev/null +++ b/shared/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@shared/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/shared/components/ui/button.tsx b/shared/components/ui/button.tsx new file mode 100644 index 0000000..09c012f --- /dev/null +++ b/shared/components/ui/button.tsx @@ -0,0 +1,134 @@ +import * as React from "react"; +import { Slot, Slottable } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@shared/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + destructiveGhost: "text-destructive hover:bg-accent", + link: "text-primary underline-offset-4 hover:underline", + }, + effect: { + expandIcon: "group gap-0 relative", + ringHover: + "transition-all duration-300 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2", + shine: + "before:animate-shine relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-no-repeat background-position_0s_ease", + shineHover: + "relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-[position:200%_0,0_0] before:bg-no-repeat before:transition-[background-position_0s_ease] hover:before:bg-[position:-100%_0,0_0] before:duration-1000", + shineHoverExpand: + "group gap-0 relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-[position:200%_0,0_0] before:bg-no-repeat before:transition-[background-position_0s_ease] hover:before:bg-[position:-100%_0,0_0] before:duration-1000", + + gooeyRight: + "relative z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-white/40 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%]", + gooeyLeft: + "relative z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-white/40 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%]", + underline: + "relative !no-underline after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300", + hoverUnderline: + "relative !no-underline after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300", + hoverUnderlineInvert: + "relative !no-underline after:absolute after:bg-secondary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300", + hoverUnderlineWhiteExpand: + "group gap-0 relative !no-underline after:absolute after:bg-white after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300", + gradientSlideShow: + "bg-[size:400%] bg-[linear-gradient(-45deg,var(--gradient-lime),var(--gradient-ocean),var(--gradient-wine),var(--gradient-rust))] animate-gradient-flow", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + iconLg: "h-12 w-12", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +interface IconProps { + icon: React.ElementType; + iconPlacement: "left" | "right"; +} + +interface IconRefProps { + icon?: never; + iconPlacement?: undefined; +} + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export type ButtonIconProps = IconProps | IconRefProps; + +const Button = React.forwardRef< + HTMLButtonElement, + ButtonProps & ButtonIconProps +>( + ( + { + className, + variant, + effect, + size, + icon: Icon, + iconPlacement, + asChild = false, + ...props + }, + ref + ) => { + const Comp = asChild ? Slot : "button"; + return ( + + {Icon && + iconPlacement === "left" && + (effect === "expandIcon" || + effect === "shineHoverExpand" || + effect === "hoverUnderlineWhiteExpand" ? ( +
+ +
+ ) : ( + + ))} + {props.children} + {Icon && + iconPlacement === "right" && + (effect === "expandIcon" || + effect === "shineHoverExpand" || + effect === "hoverUnderlineWhiteExpand" ? ( +
+ +
+ ) : ( + + ))} +
+ ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/shared/components/ui/card.tsx b/shared/components/ui/card.tsx new file mode 100644 index 0000000..c8c2164 --- /dev/null +++ b/shared/components/ui/card.tsx @@ -0,0 +1,122 @@ +import * as React from "react"; + +import { cn } from "@shared/lib/utils"; + +export interface CardProps extends React.HTMLAttributes { + animated?: boolean; +} + +const Card = React.forwardRef( + ({ className, animated = false, ...props }, ref) => ( +
+ ) +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef( + ({ className, animated = false, ...props }, ref) => ( +
+ ) +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef( + ({ className, animated = false, ...props }, ref) => ( +
+ ) +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef( + ({ className, animated = false, ...props }, ref) => ( +
+ ) +); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef( + ({ className, animated = false, ...props }, ref) => ( +
+ ) +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef( + ({ className, animated = false, ...props }, ref) => ( +
+ ) +); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/shared/components/ui/chat/chat-bubble.tsx b/shared/components/ui/chat/chat-bubble.tsx new file mode 100644 index 0000000..11d30b9 --- /dev/null +++ b/shared/components/ui/chat/chat-bubble.tsx @@ -0,0 +1,223 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@shared/lib/utils"; +import { + Avatar, + AvatarImage, + AvatarFallback, +} from "@shared/components/ui/avatar"; +import MessageLoading from "./message-loading"; +import { Button, ButtonProps } from "../button"; +import { GraduationCap } from "lucide-react"; + +// ChatBubble +const chatBubbleVariant = cva( + "flex gap-2 max-w-[60%] items-end relative group", + { + variants: { + variant: { + received: "self-start", + sent: "self-end flex-row-reverse", + }, + layout: { + default: "", + ai: "max-w-full w-full items-center", + }, + }, + defaultVariants: { + variant: "received", + layout: "default", + }, + } +); + +interface ChatBubbleProps + extends React.HTMLAttributes, + VariantProps {} + +const ChatBubble = React.forwardRef( + ({ className, variant, layout, children, ...props }, ref) => ( +
+ {React.Children.map(children, (child) => + React.isValidElement(child) && typeof child.type !== "string" + ? React.cloneElement(child, { + variant, + layout, + } as React.ComponentProps) + : child + )} +
+ ) +); +ChatBubble.displayName = "ChatBubble"; + +// ChatBubbleAvatar +interface ChatBubbleAvatarProps { + src?: string; + fallback?: string; + className?: string; + svg?: React.ReactNode; +} + +const ChatBubbleAvatar: React.FC = ({ + src, + fallback, + className, + svg, +}) => ( + + {svg ?? ( + <> + + {fallback} + + )} + +); + +const AIAvatar = () => { + return ( + } + /> + ); +}; + +// ChatBubbleMessage +const chatBubbleMessageVariants = cva("p-4", { + variants: { + variant: { + received: + "bg-secondary text-secondary-foreground rounded-r-lg rounded-tl-lg self-start", + sent: "bg-primary text-primary-foreground rounded-l-lg rounded-tr-lg self-end flex-row-reverse", + }, + layout: { + default: "", + ai: "border-t w-full rounded-none bg-transparent", + }, + }, + defaultVariants: { + variant: "received", + layout: "default", + }, +}); + +interface ChatBubbleMessageProps + extends React.HTMLAttributes, + VariantProps { + isLoading?: boolean; +} + +const ChatBubbleMessage = React.forwardRef< + HTMLDivElement, + ChatBubbleMessageProps +>( + ( + { className, variant, layout, isLoading = false, children, ...props }, + ref + ) => ( +
+ {isLoading ? ( +
+ +
+ ) : ( + children + )} +
+ ) +); +ChatBubbleMessage.displayName = "ChatBubbleMessage"; + +// ChatBubbleTimestamp +interface ChatBubbleTimestampProps + extends React.HTMLAttributes { + timestamp: string; +} + +const ChatBubbleTimestamp: React.FC = ({ + timestamp, + className, + ...props +}) => ( +
+ {timestamp} +
+); + +// ChatBubbleAction +type ChatBubbleActionProps = ButtonProps & { + icon: React.ReactNode; +}; + +const ChatBubbleAction: React.FC = ({ + icon, + onClick, + className, + variant = "ghost", + size = "icon", + ...props +}) => ( + +); + +interface ChatBubbleActionWrapperProps + extends React.HTMLAttributes { + variant?: "sent" | "received"; + className?: string; +} + +const ChatBubbleActionWrapper = React.forwardRef< + HTMLDivElement, + ChatBubbleActionWrapperProps +>(({ variant, className, children, ...props }, ref) => ( +
+ {children} +
+)); +ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper"; + +export { + ChatBubble, + ChatBubbleAvatar, + ChatBubbleMessage, + ChatBubbleTimestamp, + chatBubbleVariant, + chatBubbleMessageVariants, + ChatBubbleAction, + ChatBubbleActionWrapper, + AIAvatar, +}; diff --git a/shared/components/ui/chat/chat-input.tsx b/shared/components/ui/chat/chat-input.tsx new file mode 100644 index 0000000..7d90862 --- /dev/null +++ b/shared/components/ui/chat/chat-input.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { Textarea } from "@shared/components/ui/textarea"; +import { cn } from "@shared/lib/utils"; + +interface ChatInputProps + extends React.TextareaHTMLAttributes { + //leave as optional + onEnter?: () => void; +} + +const ChatInput = React.forwardRef( + ({ className, onKeyDown, onEnter, ...props }, ref) => { + const handleKeyDown = (event: React.KeyboardEvent) => { + //if it is only the enter key + if (event.key === "Enter" && !event.shiftKey) { + //prevent new line + event.preventDefault(); + if (onEnter) { + onEnter(); + } + } + if (onKeyDown) { + onKeyDown(event); + } + }; + return ( +