diff --git a/package.json b/package.json index bd0935d2..2ee46f50 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@clerk/themes": "1.7.4", "@dqbd/tiktoken": "1.0.3", "@ducanh2912/next-pwa": "^10.0.0", + "@hookform/resolvers": "^3.3.2", "@libsql/client": "0.3.1", "@liveblocks/client": "1.1.6", "@liveblocks/react": "1.1.6", @@ -44,12 +45,14 @@ "@radix-ui/react-slider": "1.1.2", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-switch": "1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toggle": "1.0.3", "@radix-ui/react-tooltip": "1.0.6", "@sentry/nextjs": "7.61.0", "@t3-oss/env-core": "0.3.1", "@t3-oss/env-nextjs": "0.3.1", "@tanstack/react-query": "4.29.25", + "@tldraw/tldraw": "2.0.0-alpha.18", "@typeform/embed-react": "2.31.0", "@types/node": "18.15.11", "@types/react": "18.0.33", @@ -75,12 +78,15 @@ "langsmith": "0.0.48", "lucide-react": "0.224.0", "next": "13.5.5", + "next-usequerystate": "^1.13.1", "openai": "^4.17.4", "prettier": "3.0.0", "react": "18.2.0", + "react-device-detect": "^2.2.3", "react-div-100vh": "0.7.0", "react-dom": "18.2.0", "react-dropzone": "14.2.3", + "react-hook-form": "7.48.2", "react-intersection-observer": "9.5.2", "react-markdown": "8.0.7", "react-mic": "12.4.6", @@ -91,7 +97,7 @@ "tailwind-merge": "1.12.0", "tailwindcss-animate": "1.0.5", "typescript": "5.0.3", - "zod": "3.21.4", + "zod": "^3.22.4", "zustand": "^4.4.6" }, "devDependencies": { @@ -108,6 +114,7 @@ "postcss": "8.4.21", "pwa-asset-generator": "^6.3.1", "tailwindcss": "3.3.1", + "tailwindcss-safe-area": "^0.4.1", "ts-node": "10.9.1", "webpack": "^5.89.0" } diff --git a/public/default_tldraw.png b/public/default_tldraw.png new file mode 100644 index 00000000..c5e13106 Binary files /dev/null and b/public/default_tldraw.png differ diff --git a/public/manifest.json b/public/manifest.json index ed650125..2c879c16 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -31,8 +31,8 @@ ], "theme_color": "#03050A", "background_color": "#03050A", - "start_url": "/", + "start_url": "/dashboard", "scope": ".", "display": "standalone", "orientation": "portrait" -} \ No newline at end of file +} diff --git a/src/app/(auth)/organization-profile/[[...organization-profile]]/page.tsx b/src/app/(auth)/organization-profile/[[...organization-profile]]/page.tsx new file mode 100644 index 00000000..5342722f --- /dev/null +++ b/src/app/(auth)/organization-profile/[[...organization-profile]]/page.tsx @@ -0,0 +1,9 @@ +import { OrganizationProfile } from "@clerk/nextjs"; + +export default function OrganizationProfilePage() { + return ( +
+ +
+ ); +} diff --git a/src/app/(auth)/user-profile/[[...user-profile]]/page.tsx b/src/app/(auth)/user-profile/[[...user-profile]]/page.tsx new file mode 100644 index 00000000..d44b0fa5 --- /dev/null +++ b/src/app/(auth)/user-profile/[[...user-profile]]/page.tsx @@ -0,0 +1,9 @@ +import { UserProfile } from "@clerk/nextjs"; + +const UserProfilePage = () => ( +
+ +
+); + +export default UserProfilePage; diff --git a/src/app/api/chatmodel/[chatid]/route.ts b/src/app/api/chatmodel/[chatid]/route.ts index 9f0a3f21..5dd41b32 100644 --- a/src/app/api/chatmodel/[chatid]/route.ts +++ b/src/app/api/chatmodel/[chatid]/route.ts @@ -2,15 +2,13 @@ import { StreamingTextResponse, LangChainStream, nanoid } from "ai"; import { eq } from "drizzle-orm"; import { db } from "@/lib/db"; import { chats } from "@/lib/db/schema"; -import { CHAT_COMPLETION_CONTENT, ChatEntry, ChatLog } from "@/lib/types"; +import { ChatEntry, ChatLog } from "@/lib/types"; import { systemPrompt } from "@/utils/prompts"; import { - chooseModel, jsonToLangchain, openAIChatModel, OPEN_AI_MODELS, } from "@/utils/apiHelper"; -import { NextResponse } from "next/server"; import { env } from "@/app/env.mjs"; import { auth } from "@clerk/nextjs"; import axios from "axios"; @@ -27,9 +25,9 @@ export async function POST( console.log("orgSlug", orgSlug); const _chat = body.messages; - const isFast = body.isFast; let orgId = ""; orgId = body.orgId; + const userId = body.userId; const url = request.url; // getting main url const urlArray = url.split("/"); @@ -45,29 +43,6 @@ export async function POST( const msgs = jsonToLangchain(_chat, systemPrompt); const model = OPEN_AI_MODELS.gpt4Turbo; - const { error } = chooseModel(isFast, msgs, systemPrompt); - - if (error) { - const msg = { - content: CHAT_COMPLETION_CONTENT, - role: "assistant", - }; - _chat.push(msg); // pushing the final message to identify that the chat is completed - await db - .update(chats) - .set({ - messages: JSON.stringify({ log: _chat }), - updatedAt: new Date(), - }) - .where(eq(chats.id, Number(id))) - .run(); - return NextResponse.json( - { ...msg }, - { - status: 400, - }, - ); - } const postToAlgolia = ({ chats, @@ -131,6 +106,7 @@ export async function POST( .update(chats) .set({ messages: JSON.stringify({ log: _chat } as ChatLog), + creator: userId, }) .where(eq(chats.id, Number(id))) .run(); diff --git a/src/app/api/chats/[chatid]/route.tsx b/src/app/api/chats/[chatid]/route.tsx index c2de0999..e945eacb 100644 --- a/src/app/api/chats/[chatid]/route.tsx +++ b/src/app/api/chats/[chatid]/route.tsx @@ -20,5 +20,5 @@ export async function GET( const msg = fetchedChat[0]?.messages; const chatlog = JSON.parse(msg as string) as ChatLog; - return NextResponse.json({ chats: chatlog.log }); + return NextResponse.json({ chats: chatlog ? chatlog.log : [] }); } diff --git a/src/app/api/generateNewChatId/[org_id]/route.ts b/src/app/api/generateNewChatId/[org_id]/route.ts index b70b5ad4..5195758f 100644 --- a/src/app/api/generateNewChatId/[org_id]/route.ts +++ b/src/app/api/generateNewChatId/[org_id]/route.ts @@ -8,6 +8,9 @@ export async function POST( // const body = await request.json(); // const org_id = body.org_id; console.log("getting it in the route", params.params.org_id); + const body = await request.json(); + const chatType = body.type; + const title = body.title; const id = params.params.org_id; @@ -19,6 +22,8 @@ export async function POST( .insert(chats) .values({ user_id: id, + type: chatType, + title: title, }) .run(); console.log("generated New Chat id", data.toJSON()); diff --git a/src/app/api/getPaginatedChats/[org_id]/route.ts b/src/app/api/getPaginatedChats/[org_id]/route.ts index 01551a0c..cc91eeee 100644 --- a/src/app/api/getPaginatedChats/[org_id]/route.ts +++ b/src/app/api/getPaginatedChats/[org_id]/route.ts @@ -2,6 +2,9 @@ import { db } from "@/lib/db"; import { chats } from "@/lib/db/schema"; import { desc, eq, ne, and } from "drizzle-orm"; import { NextResponse } from "next/server"; +import * as z from "zod"; + +const chatquery = z.enum(["me", "org"]); export async function GET( request: Request, @@ -10,6 +13,8 @@ export async function GET( // handle if org_id is undefined or null const { searchParams } = new URL(request.url); let page = Number(searchParams.get("page")); + let userId = searchParams.get("userId"); + let chatsQuery = chatquery.parse(searchParams.get("chats")); const org_id = params.params.org_id; if (page === null || isNaN(page)) { @@ -18,17 +23,45 @@ export async function GET( } console.log("page", page); console.log("orgId", org_id); + console.log("userId", userId); + console.log("chats", chatsQuery); // need to improve the logic const offset = 25; const skip = offset * page; + let orgConversations: any; + if (chatsQuery === "me") { + orgConversations = await db + .select() + .from(chats) + .where( + and( + eq(chats.user_id, String(org_id)), + ne(chats.messages, "NULL"), + eq(chats.creator, String(userId)), + ), + ) + .orderBy(desc(chats.updatedAt)) + .offset(skip) + .limit(25) + .all(); + } else { + orgConversations = await db + .select() + .from(chats) + .where(and(eq(chats.user_id, String(org_id)), ne(chats.messages, "NULL"))) + .orderBy(desc(chats.updatedAt)) + .offset(skip) + .limit(25) + .all(); + } - let orgConversations = await db - .select() - .from(chats) - .where(and(eq(chats.user_id, String(org_id)), ne(chats.messages, "NULL"))) - .orderBy(desc(chats.updatedAt)) - .offset(skip) - .limit(25) - .all(); + // let orgConversations = await db + // .select() + // .from(chats) + // .where(and(eq(chats.user_id, String(org_id)), ne(chats.messages, "NULL"), eq(chats.creator, String(userId)))) + // .orderBy(desc(chats.updatedAt)) + // .offset(skip) + // .limit(25) + // .all(); return NextResponse.json({ conversations: orgConversations }); } diff --git a/src/app/api/tldraw/[chatId]/route.ts b/src/app/api/tldraw/[chatId]/route.ts new file mode 100644 index 00000000..3c3dbb0e --- /dev/null +++ b/src/app/api/tldraw/[chatId]/route.ts @@ -0,0 +1,38 @@ +import { db } from "@/lib/db"; +import { chats } from "@/lib/db/schema"; +import { Message, nanoid } from "ai"; +import { eq } from "drizzle-orm"; + +export async function POST( + req: Request, + params: { params: { chatId: string } }, +) { + const { chatId } = params.params; + const body = await req.json(); + const content = body.content; + const name = body.name; + console.log("name", name); + + const _chat = [ + { + content, + createdAt: new Date(), + id: nanoid(), + role: "user", + name, + } as Message, + ]; + + console.log("chatId", _chat); + + await db + .update(chats) + .set({ + messages: JSON.stringify({ log: _chat }), + updatedAt: new Date(), + }) + .where(eq(chats.id, Number(chatId))) + .run(); + + return new Response(JSON.stringify({ success: true })); +} diff --git a/src/app/usr/[uid]/chat/[chatid]/page.tsx b/src/app/dashboard/[slug]/chat/[chatid]/page.tsx similarity index 87% rename from src/app/usr/[uid]/chat/[chatid]/page.tsx rename to src/app/dashboard/[slug]/chat/[chatid]/page.tsx index df78685f..e0cfd130 100644 --- a/src/app/usr/[uid]/chat/[chatid]/page.tsx +++ b/src/app/dashboard/[slug]/chat/[chatid]/page.tsx @@ -1,4 +1,4 @@ -import { ChatLog } from "@/lib/types"; +import { ChatLog, ChatType } from "@/lib/types"; import { db } from "@/lib/db"; import { redirect } from "next/navigation"; import { Chat as ChatSchema, chats } from "@/lib/db/schema"; @@ -21,12 +21,7 @@ export default async function Page({ const fullname = ((user?.firstName as string) + " " + user?.lastName) as string; - if ( - !params.uid || - !params.chatid || - !userId || - sessionClaims?.org_slug !== params.uid - ) { + if (!sessionClaims?.org_slug) { console.log('redirecting to "/"'); redirect("/"); } @@ -58,12 +53,13 @@ export default async function Page({ orgId={sessionClaims.org_id ? sessionClaims.org_id : ""} chat={chatlog.log} chatId={params.chatid} - uid={userId} + uid={userId as string} username={fullname} chatAvatarData={fetchedChat[0]} - org_slug={params.uid} // here uid contains org_slug + org_slug={sessionClaims?.org_slug} // here uid contains org_slug chatTitle={fetchedChat[0]?.title as string} imageUrl={fetchedChat[0]?.image_url as string} + type={fetchedChat[0]?.type as ChatType} > ); diff --git a/src/app/usr/[uid]/error.tsx b/src/app/dashboard/[slug]/error.tsx similarity index 100% rename from src/app/usr/[uid]/error.tsx rename to src/app/dashboard/[slug]/error.tsx diff --git a/src/app/usr/[uid]/page.tsx b/src/app/dashboard/[slug]/page.tsx similarity index 51% rename from src/app/usr/[uid]/page.tsx rename to src/app/dashboard/[slug]/page.tsx index 23e89cf9..9bf6f468 100644 --- a/src/app/usr/[uid]/page.tsx +++ b/src/app/dashboard/[slug]/page.tsx @@ -1,6 +1,5 @@ import { Button } from "@/components/button"; import Link from "next/link"; -import { redirect } from "next/navigation"; import { db } from "@/lib/db"; import { chats, Chat as ChatSchema } from "@/lib/db/schema"; import { eq, desc, ne, and } from "drizzle-orm"; @@ -11,25 +10,35 @@ import ChatCardWrapper from "@/components/chatcardwrapper"; export const dynamic = "force-dynamic", revalidate = 0; -export default async function Page({ params }: { params: { uid: string } }) { - const { uid } = params; +export default async function Page({ + params, + searchParams, +}: { + params: { slug: string }; + searchParams: { [key: string]: string }; +}) { + const { slug } = params; const { userId, sessionClaims } = auth(); - if (!userId || !uid || userId !== uid) { - redirect("/"); - } let orgConversations = [] as ChatSchema[]; // fetch initial posts to start with const isOrgExist = Object.keys(sessionClaims?.organizations as Object).length; if (isOrgExist) { - orgConversations = await getConversations({ - orgId: String(sessionClaims?.org_id), - }); + if (searchParams.hasOwnProperty("chats") && searchParams.chats === "me") { + orgConversations = await getConversations({ + orgId: String(sessionClaims?.org_id), + userId: String(userId), + }); + } else { + orgConversations = await getConversations({ + orgId: String(sessionClaims?.org_id), + }); + } } return ( -
+
{!isOrgExist ? (
You are not a member in any Organisations.{" "} @@ -41,19 +50,10 @@ export default async function Page({ params }: { params: { uid: string } }) {
) : (
- {/*
- -
- -
-
*/}
@@ -64,18 +64,38 @@ export default async function Page({ params }: { params: { uid: string } }) { const getConversations = async ({ orgId, + userId, offset = 0, }: { orgId: string; + userId?: string; offset?: number; }): Promise => { - let orgConversations = await db - .select() - .from(chats) - .where(and(eq(chats.user_id, String(orgId)), ne(chats.messages, "NULL"))) - .orderBy(desc(chats.updatedAt)) - .offset(offset) - .limit(25) - .all(); - return orgConversations; + if (!userId) { + let orgConversations = await db + .select() + .from(chats) + .where(and(eq(chats.user_id, String(orgId)), ne(chats.messages, "NULL"))) + .orderBy(desc(chats.updatedAt)) + .offset(offset) + .limit(25) + .all(); + return orgConversations; + } else { + let orgConversations = await db + .select() + .from(chats) + .where( + and( + eq(chats.user_id, String(orgId)), + ne(chats.messages, "NULL"), + eq(chats.creator, userId), + ), + ) + .orderBy(desc(chats.updatedAt)) + .offset(offset) + .limit(25) + .all(); + return orgConversations; + } }; diff --git a/src/app/usr/layout.tsx b/src/app/dashboard/layout.tsx similarity index 58% rename from src/app/usr/layout.tsx rename to src/app/dashboard/layout.tsx index 2e4aece3..f8a2a77d 100644 --- a/src/app/usr/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -7,11 +7,13 @@ import useStore from "@/store"; import { useRef, useEffect } from "react"; import AudioPlayer from "@/components/audioplayer"; import { usePathname } from "next/navigation"; -import { OrganizationSwitcher, useAuth } from "@clerk/nextjs"; +import { useAuth } from "@clerk/nextjs"; import Startnewchatbutton from "@/components/startnewchatbutton"; import useSlotStore from "@/store/slots"; import Search from "@/components/search"; - +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Building, User } from "lucide-react"; +import { useQueryState } from "next-usequerystate"; export default function LoggedInLayout({ children, team, // will be a page or nested layout @@ -24,6 +26,7 @@ export default function LoggedInLayout({ const audioRef = useRef(null); const pathname = usePathname(); const { orgSlug, orgId } = useAuth(); + const [cards, setCards] = useQueryState("chats"); useEffect(() => { if (store.audioSrc && audioRef.current) { @@ -38,17 +41,34 @@ export default function LoggedInLayout({ return (
- {pathname.includes("user_") ? ( + {pathname.includes("user") ? (
+
-
- -
+ { + console.log("onvalchange", val); + setCards(val); + }} + > + + + {" "} + Org Chats + + + + My Chats + + + + {/*
*/}
// slotStore.slot } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index e41dd454..06ea5abd 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,9 +1,5 @@ import { redirect } from "next/navigation"; -import { auth } from "@clerk/nextjs"; export default async function Page() { - const { sessionClaims } = auth(); - console.log("sessionClaims", sessionClaims?.organizations); - console.log(sessionClaims?.sub); - redirect(`/usr/${sessionClaims?.sub}`); + redirect(`/dashboard/user`); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 091f14f1..738ac370 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -248,11 +248,14 @@ export default function RootLayout({ Echoes - + -
{children}
+
+ {children} +
diff --git a/src/app/page.tsx b/src/app/page.tsx index 0d75ea59..2cff7d8c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,7 +10,7 @@ import { motion, AnimatePresence, useAnimation } from "framer-motion"; import { useInView } from "react-intersection-observer"; import { Key, LayoutDashboard } from "lucide-react"; import { useAuth } from "@clerk/nextjs"; - +import { redirect } from "next/navigation"; const handleSmoothScroll = (): void => { if (typeof window !== "undefined") { const hashId = window.location.hash; @@ -49,6 +49,9 @@ export default function Home() { }, [controls, inView]); const { isSignedIn } = useAuth(); + if (isSignedIn) { + redirect("/dashboard/user"); + } return (
@@ -80,7 +83,7 @@ export default function Home() {
diff --git a/src/components/ablyprovider.tsx b/src/components/ablyprovider.tsx index 5ec7bb75..da625679 100644 --- a/src/components/ablyprovider.tsx +++ b/src/components/ablyprovider.tsx @@ -13,6 +13,10 @@ export const AblyChannelProvider = ({ const client = new Ably.Realtime.Promise({ key: process.env.NEXT_PUBLIC_ABLY_API_KEY, clientId: clientId, + recover: (lastConnectionDetails, cb) => { + console.log("last connection details", lastConnectionDetails); + cb(true); + }, }); return {children}; }; diff --git a/src/components/audioplayer.tsx b/src/components/audioplayer.tsx index 4722d1f5..4cc917d9 100644 --- a/src/components/audioplayer.tsx +++ b/src/components/audioplayer.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { useStore } from "@/store"; -import { Pause, Play, X } from "lucide-react"; +import { Music, Pause, Play, X } from "lucide-react"; import { Slider } from "./slider"; import { Button } from "./button"; import { @@ -135,7 +135,7 @@ const AudioPlayer = (props: Props) => { <> - diff --git a/src/components/button.tsx b/src/components/button.tsx index d26b41ad..cabae682 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -20,7 +20,7 @@ const buttonVariants = cva( link: "underline-offset-4 hover:underline text-primary", }, size: { - default: "h-10 py-2 px-4", + default: "h-[32px] py-2 px-3", sm: "h-9 px-3 rounded-md", lg: "h-11 px-8 rounded-md", icon: "h-9 w-9", diff --git a/src/components/chat.tsx b/src/components/chat.tsx index 885126fa..a387c597 100644 --- a/src/components/chat.tsx +++ b/src/components/chat.tsx @@ -1,12 +1,13 @@ "use client"; import { useState, useEffect } from "react"; -import { AIType } from "@/lib/types"; +import { AIType, ChatType } from "@/lib/types"; import InputBar from "@/components/inputBar"; import { Message, useChat } from "ai/react"; import Startnewchatbutton from "@/components/startnewchatbutton"; import ChatMessageCombinator from "@/components/chatmessagecombinator"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; +import PersistenceExample from "@/components/tldraw"; interface ChatProps { orgId: string; @@ -17,6 +18,7 @@ interface ChatProps { org_slug: string; chatTitle: string; imageUrl: string; + type: ChatType; } export default function Chat(props: ChatProps) { @@ -55,9 +57,9 @@ export default function Chat(props: ChatProps) { initialMessages: chatsData, body: { orgId: props.orgId, - isFast: choosenAI === "universal" ? true : false, name: props.username, - }, // some conflicts in role + userId: props.uid, + }, onError: (error) => { console.log("got the error", error); setIsChatCompleted(true); @@ -99,43 +101,62 @@ export default function Chat(props: ChatProps) { return (
- - {/*
*/} - {isChatCompleted && ( -
- + {props.type === "tldraw" ? ( +
+
+ ) : ( + // {preferences.showSubRoll && } + <> + + {/*
*/} + {isChatCompleted && ( +
+ +
+ )} + + )} -
); } diff --git a/src/components/chatcard.tsx b/src/components/chatcard.tsx index 7a85fc46..b328a9b9 100644 --- a/src/components/chatcard.tsx +++ b/src/components/chatcard.tsx @@ -18,6 +18,7 @@ import Image from "next/image"; import AudioButton from "@/components//audioButton"; import { useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; +import { cn } from "@/lib/utils"; // import { dynamicBlurDataUrl } from "@/utils/helpers"; type Props = { @@ -26,13 +27,16 @@ type Props = { org_id: string; org_slug: string; priority: boolean; + type: string; }; -const Chatcard = ({ chat, uid, org_id, org_slug, priority }: Props) => { +const Chatcard = ({ chat, uid, org_id, org_slug, priority, type }: Props) => { const queryClient = useQueryClient(); const [showLoading, setShowLoading] = useState(false); const [isGeneratingImage, setIsGeneratingImage] = useState(false); - const [imageUrl, setImageUrl] = useState(chat.image_url); + const [imageUrl, setImageUrl] = useState( + type === "tldraw" ? "/default_tldraw.png" : chat.image_url, + ); // const [blurDataUrl, setBlurDataUrl] = useState(async () => { // const data = await dynamicBlurDataUrl(chat.image_url as string); // return data @@ -111,26 +115,12 @@ const Chatcard = ({ chat, uid, org_id, org_slug, priority }: Props) => { }} > - + {/*
*/} - + {title}{" "} - - + {title === "" ? ( No title{" "} @@ -145,10 +135,26 @@ const Chatcard = ({ chat, uid, org_id, org_slug, priority }: Props) => { <>{description} )} - {/*
*/} + {chat.type !== "tldraw" && ( + + )}
{ pathname: `${org_slug}/chat/${chat.id}`, }} key={chat.id} - className={buttonVariants({ variant: "secondary" })} + className={cn( + buttonVariants({ variant: "secondary" }), + "h-[32px]", + )} > {showLoading ? ( @@ -187,7 +196,7 @@ const Chatcard = ({ chat, uid, org_id, org_slug, priority }: Props) => {
{imageUrl && ( { if (org_id === undefined) { redirect(`${uid}`); } + const [chatsQuery] = useQueryState("chats"); const fetchChats = async ({ pageParam = 0 }) => { - console.log("pageParam", pageParam); const response = await fetch( - `/api/getPaginatedChats/${org_id}?page=${pageParam}`, + `/api/getPaginatedChats/${org_id}?page=${pageParam}&userId=${uid}&chats=${ + chatsQuery ? chatsQuery : "org" + }`, { method: "GET", }, ); const chats = await response.json(); - console.log(chats); return chats.conversations; }; const { data, fetchNextPage, isFetchingNextPage, hasNextPage } = useInfiniteQuery({ - queryKey: ["projects"], + queryKey: ["chatcards", org_id, chatsQuery], queryFn: fetchChats, getNextPageParam: (lastPage, pages) => lastPage.length < 25 ? undefined : pages.length, @@ -45,7 +48,9 @@ const ChatCardWrapper = ({ org_id, org_slug, uid, initialData }: Props) => { pageParams: [0], pages: [initialData], }, - enabled: false, + refetchOnWindowFocus: false, + refetchInterval: 1000 * 60 * 5, + // enabled: false, }); const lastPostRef = React.useRef(null); @@ -62,11 +67,16 @@ const ChatCardWrapper = ({ org_id, org_slug, uid, initialData }: Props) => { return (
-
+
{allCards?.map((chat, i) => { return ( -
+
+ ) : (
{ }} > - @@ -89,8 +92,8 @@ const Search = (props: Props) => { setOpen(false)} key={result.objectID} diff --git a/src/components/startnewchatbutton.tsx b/src/components/startnewchatbutton.tsx index 1b796897..218091cf 100644 --- a/src/components/startnewchatbutton.tsx +++ b/src/components/startnewchatbutton.tsx @@ -1,46 +1,191 @@ "use client"; -import { useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { buttonVariants } from "@/components/button"; import { useRouter } from "next/navigation"; import { Plus, CircleNotch } from "@phosphor-icons/react"; +import { ChatType } from "@/lib/types"; +import { + Dialog, + DialogContent, + DialogTrigger, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/button"; +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { MessageSquarePlus, PenTool } from "lucide-react"; interface Props { org_slug: string; org_id: string; } +const formSchema = z.object({ + title: z.string().min(2).max(100), +}); + const Startnewchatbutton = (props: Props) => { const [showLoading, setShowLoading] = useState(false); + const [isBoardCreating, setIsBoardCreating] = useState(false); + const [showTitleInput, setShowTitleInput] = useState(false); + const [title, setTitle] = useState(""); + + const [isOpen, setIsOpen] = useState(false); const router = useRouter(); - const handleNavigate = async () => { + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: "", + }, + }); + + const handleNavigate = async ( + type: ChatType, + isLoding: Dispatch>, + title?: string, + ) => { + if (type === "tldraw" && !title) { + setShowTitleInput(true); + return; + } + isLoding(true); + const res = await fetch(`/api/generateNewChatId/${props.org_id}`, { method: "POST", + body: JSON.stringify({ type: type, title: title }), }); const data = await res.json(); - router.push(`/usr/${props.org_slug}/chat/${Number(data.newChatId)}`); + isLoding(false); + router.push(`/dashboard/${props.org_slug}/chat/${Number(data.newChatId)}`); }; return ( - + + + + { + if (showLoading || isBoardCreating) { + e.preventDefault(); + } + }} + > + + Start New Chat + + {/* show two boxes. one for a chat another for a tldraw */} + {!showTitleInput && ( +
+ + +
+ )} + {showTitleInput && ( +
+ { + handleNavigate("tldraw", setIsBoardCreating, vals.title); + })} + className="space-y-4" + > + ( + + Title + + + + + + )} + /> +
+ +
+ + + )} +
+ ); }; diff --git a/src/components/tldraw.tsx b/src/components/tldraw.tsx new file mode 100644 index 00000000..2bdf15a9 --- /dev/null +++ b/src/components/tldraw.tsx @@ -0,0 +1,164 @@ +import { useQuery } from "@tanstack/react-query"; +import { + Tldraw, + createTLStore, + defaultShapeUtils, + useEditor, +} from "@tldraw/tldraw"; +import "@tldraw/tldraw/tldraw.css"; +import { Message } from "ai"; +import axios from "axios"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Button } from "@/components/button"; +import { Save } from "lucide-react"; +const PERSISTENCE_KEY = "example-3"; + +interface PersistenceExampleProps { + initialData?: any; + org_id: string; + org_slug: string; + username: string; + chatId: string; + uid: string; + dbChat: Message[]; +} + +export default function PersistenceExample(props: PersistenceExampleProps) { + const [store] = useState(() => + createTLStore({ shapeUtils: defaultShapeUtils }), + ); + + const tlDrawFetcher = async () => { + const res = await axios.get(`/api/chats/${props.chatId}`); + const chats = res.data.chats as Message[]; + // return chats as Message[]; + return chats.length ? chats[0].content : null; + }; + + const { data, isLoading, isError, error } = useQuery( + ["tldraw", props.chatId], + tlDrawFetcher, + { + initialData: props.dbChat.length && props.dbChat[0].content, + refetchOnWindowFocus: false, + refetchInterval: Infinity, + onSuccess: (data) => { + // console.log("data", data); + if (data) { + store.loadSnapshot(JSON.parse(data)); + } + }, + }, + ); + const [timer, setTimer] = useState(0); + const [isAutoSaving, setIsAutoSaving] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + if (isLoading) { + return ( +
+

Loading...

+
+ ); + } + + if (isError) { + return ( +
+

Error!

+

{error as string}

+
+ ); + } + + const save = async ( + content: string, + saving: Dispatch>, + ) => { + setTimer(0); + saving(true); + const res = await axios.post(`/api/tldraw/${props.chatId}`, { + content: content, + name: `${props.username},${props.uid}`, + }); + setTimer(0); + saving(false); + }; + + const handleSave = async () => { + const snapshot = JSON.stringify(store.getSnapshot()); + await save(snapshot, setIsSaving); + }; + + return ( +
+ + + + +
+ ); +} + +const InsideOfEditorContext = ({ + timer, + setTimer, + isAutoSaving, + setIsAutoSaving, + isSaving, + save, +}: { + timer: number; + setTimer: Dispatch>; + isAutoSaving: boolean; + setIsAutoSaving: Dispatch>; + isSaving: boolean; + save: ( + content: string, + saving: Dispatch>, + ) => Promise; +}) => { + useEffect(() => { + if (timer >= 15) { + return; + } + const interval = setInterval(() => { + if (timer < 15) { + console.log("interval", timer); + setTimer((timer) => timer + 1); + } + }, 1000); + return () => clearInterval(interval); + }, [timer]); + + const editor = useEditor(); + + useEffect(() => { + const listener = editor.store.listen((snapshot) => { + // after every 15 secs, save the snapshot + if (timer === 15 && !isAutoSaving && !isSaving) { + const snapshot = JSON.stringify(editor.store.getSnapshot()); + save(snapshot, setIsAutoSaving); + } + }); + return () => listener(); + }, [timer]); + + return null; +}; diff --git a/src/components/ui/dropdownmeu.tsx b/src/components/ui/dropdownmeu.tsx index 21685195..ab115d60 100644 --- a/src/components/ui/dropdownmeu.tsx +++ b/src/components/ui/dropdownmeu.tsx @@ -84,7 +84,7 @@ const DropdownMenuItem = React.forwardRef< = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +