From 234034763b2cf31065fb1f8b7123355035a5206a Mon Sep 17 00:00:00 2001 From: Prince Baghel Date: Wed, 25 Sep 2024 17:56:15 +0530 Subject: [PATCH 1/2] init: article generation --- .github/workflows/main.yml | 6 ++ package.json | 4 +- src/app/api/storm/route.ts | 77 +++++++++++++++++++++++ src/app/env.mjs | 12 ++++ src/app/layout.tsx | 2 + src/app/page.tsx | 5 +- src/components/chat.tsx | 104 ++++++++++++++++++++++++------- src/components/inputBar.tsx | 11 +--- src/components/inputBar2.tsx | 4 ++ src/components/modelswitcher.tsx | 8 +++ src/components/ui/sooner.tsx | 31 +++++++++ src/lib/types/index.ts | 9 ++- 12 files changed, 236 insertions(+), 37 deletions(-) create mode 100644 src/app/api/storm/route.ts create mode 100644 src/components/ui/sooner.tsx diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ccff1590..d12ebfa9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,12 @@ jobs: - name: Lint and fix run: npm run lint:fix env: + STORM_ENDPOINT: ${{secrets.STORM_ENDPOINT}} + QSTASH_TOKEN: ${{secrets.QSTASH_TOKEN}} + KEYCLOAK_CLIENT_ID: ${{secrets.KEYCLOAK_CLIENT_ID}} + KEYCLOAK_CLIENT_SECRET: ${{secrets.KEYCLOAK_CLIENT_SECRET}} + KEYCLOAK_BASE_URL: ${{secrets.KEYCLOAK_BASE_URL}} + KEYCLOAK_REALM: ${{secrets.KEYCLOAK_REALM}} ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}} TURSO_DB_URL: ${{secrets.TURSO_DB_URL}} TURSO_DB_AUTH_TOKEN: ${{secrets.TURSO_DB_AUTH_TOKEN}} diff --git a/package.json b/package.json index c9dd1707..a8c05226 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/react-dom": "18.0.11", "@uidotdev/usehooks": "2.4.1", "@uploadthing/react": "5.2.0", + "@upstash/qstash": "^2.7.9", "@upstash/redis": "1.24.3", "ably": "1.2.48", "ai": "2.2.20", @@ -95,7 +96,7 @@ "langsmith": "0.0.48", "lucide-react": "0.228.0", "next": "14.0.0", - "next-themes": "0.2.1", + "next-themes": "^0.3.0", "next-transpile-modules": "^10.0.1", "next-usequerystate": "1.13.1", "openai": "4.17.4", @@ -115,6 +116,7 @@ "remark-gfm": "3.0.1", "remark-math": "^6.0.0", "remark-rehype": "10.1.0", + "sonner": "^1.5.0", "superagentai-js": "0.1.44", "tailwind-merge": "1.12.0", "tailwindcss-animate": "1.0.5", diff --git a/src/app/api/storm/route.ts b/src/app/api/storm/route.ts new file mode 100644 index 00000000..acbeaa60 --- /dev/null +++ b/src/app/api/storm/route.ts @@ -0,0 +1,77 @@ +import { serve } from "@upstash/qstash/nextjs"; +import axios from "axios"; +import { env } from "@/app/env.mjs"; +import { saveToDB } from "@/utils/apiHelper"; +import { auth } from "@clerk/nextjs"; +import { nanoid } from "ai"; +import { NextRequest } from "next/server"; + +export const POST = async (request: NextRequest) => { + const url = request.url; + const urlArray = url.split("/"); + const { orgSlug } = auth(); + + const handler = serve(async (context) => { + const body = context.requestPayload as { topic: string }; + const stormResponse = await context.run("Initiating Storm to generate article", async () => { + const response = await axios.post( + `${env.KEYCLOAK_BASE_URL}/realms/${env.KEYCLOAK_REALM}/protocol/openid-connect/token`, + new URLSearchParams({ + client_id: env.KEYCLOAK_CLIENT_ID, + client_secret: env.KEYCLOAK_CLIENT_SECRET, + grant_type: "client_credentials", + }), + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }, + ); + const accessToken = response.data.access_token; + + const stormResponse = await axios.post( + `${env.STORM_ENDPOINT}`, + { + topic: body.topic, + search_top_k: 5, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return stormResponse.data; + }); + + await context.run("Saving Article to db", async () => { + const body = context.requestPayload as { + topic: string; + chatId: string; + orgId: string; + userId: string; + }; + const chatId = body.chatId; + const orgId = body.orgId; + const userId = body.userId; + const latestResponse = { + id: nanoid(), + role: "assistant" as const, + content: stormResponse.content, + createdAt: new Date(), + audio: "", + }; + + await saveToDB({ + _chat: [], + chatId: Number(chatId), + orgSlug: orgSlug as string, + latestResponse: latestResponse, + userId: userId, + orgId: orgId, + urlArray: urlArray, + }); + }); + }); + return await handler(request); +}; diff --git a/src/app/env.mjs b/src/app/env.mjs index 2ec4d7ca..bf3ed3e6 100644 --- a/src/app/env.mjs +++ b/src/app/env.mjs @@ -3,6 +3,12 @@ import { z } from "zod"; export const env = createEnv({ server: { + STORM_ENDPOINT: z.string().min(1), + QSTASH_TOKEN: z.string().min(1), + KEYCLOAK_CLIENT_ID: z.string().min(1), + KEYCLOAK_CLIENT_SECRET: z.string().min(1), + KEYCLOAK_BASE_URL: z.string().min(1), + KEYCLOAK_REALM: z.string().min(1), // Anthropic ANTHROPIC_API_KEY: z.string().min(10), // OpenAI @@ -64,6 +70,12 @@ export const env = createEnv({ }, runtimeEnv: { + STORM_ENDPOINT: process.env.STORM_ENDPOINT, + QSTASH_TOKEN: process.env.QSTASH_TOKEN, + KEYCLOAK_CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID, + KEYCLOAK_CLIENT_SECRET: process.env.KEYCLOAK_CLIENT_SECRET, + KEYCLOAK_BASE_URL: process.env.KEYCLOAK_BASE_URL, + KEYCLOAK_REALM: process.env.KEYCLOAK_REALM, // Anthropic ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, // Clerk (Auth) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 093a56a9..25cf2fd4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import { dark } from "@clerk/themes"; import { Inter } from "next/font/google"; import QueryProviders from "@/app/queryProvider"; import { Toaster } from "@/components/ui/toaster"; +import { Toaster as SoonerToaster } from "@/components/ui/sooner"; import { Providers } from "@/app/providers"; const inter = Inter({ subsets: ["latin"] }); @@ -260,6 +261,7 @@ export default function RootLayout({ > {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index cf9f85ac..438650d3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -97,7 +97,10 @@ export default function Home() { try { const res = await fetch(`/api/generateNewChatId/${orgId}`, { method: "POST", - body: JSON.stringify({ type: chatType || "chat" }), + body: + chatType === "storm" + ? JSON.stringify({ type: chatType || "chat", title: input }) + : JSON.stringify({ type: chatType || "chat" }), }); const data = await res.json(); diff --git a/src/components/chat.tsx b/src/components/chat.tsx index 73065880..b58aecfe 100644 --- a/src/components/chat.tsx +++ b/src/components/chat.tsx @@ -15,6 +15,7 @@ import { useDropzone } from "react-dropzone"; import { X } from "lucide-react"; import { useImageState } from "@/store/tlDrawImage"; import { useQueryState } from "next-usequerystate"; +import { toast as soonerToast } from "sonner"; interface ChatProps { orgId: string; @@ -52,6 +53,58 @@ export default function Chat(props: ChatProps) { const [chattype, setChattype] = useState( props?.type || incomingModel || "chat", ); + const [isNewChat, setIsNewChat] = useQueryState("new"); + const [incomingInput] = useQueryState("input"); + const { mutate: InitArticleGeneration } = useMutation({ + mutationFn: async ({ + topic, + chatId, + orgId, + userId, + }: { + topic: string; + chatId: string; + orgId: string; + userId: string; + }) => { + const resp = await axios.post("/api/storm", { + topic: topic, + chatId: chatId, + orgId: orgId, + userId: userId, + }); + return resp.data; + }, + onSuccess: (data, vars, context) => { + soonerToast("Generating your article", { + description: "Please wait for 2 mins", + duration: 300 * 1000, + }); + + //TODO: set workflow id in state and make query to start invalidating + console.log("workflow id", data.workflowRunId); + }, + onError: (error: any, vars, context) => { + console.error(error?.message); + soonerToast("Something went wrong ", { + description: "Sunday, December 03, 2023 at 9:00 AM", + duration: 5 * 1000, + }); + }, + }); + + useEffect(() => { + if (isNewChat === "true" && incomingInput && incomingModel === "storm") { + // make a call to storm endpoint + InitArticleGeneration({ + chatId: props.chatId, + topic: incomingInput, + orgId: props.orgId, + userId: props.uid, + }); + setIsNewChat("false"); + } + }, [isNewChat]); const onDrop = useCallback(async (acceptedFiles: File[]) => { if (acceptedFiles && acceptedFiles[0]?.type.startsWith("image/")) { @@ -99,6 +152,7 @@ export default function Chat(props: ChatProps) { } = useQuery({ queryKey: ["chats", props.chatId], queryFn: chatFetcher, + refetchInterval: chattype === "storm" ? 50 * 1000 : false, initialData: props.dbChat, refetchOnWindowFocus: false, }); @@ -299,30 +353,32 @@ export default function Chat(props: ChatProps) { /> )} - + {chattype !== "storm" ? ( + + ) : null} )} diff --git a/src/components/inputBar.tsx b/src/components/inputBar.tsx index 052b64e0..fe0dd8a6 100644 --- a/src/components/inputBar.tsx +++ b/src/components/inputBar.tsx @@ -234,7 +234,7 @@ const InputBar = (props: InputBarProps) => { useEffect(() => { if (isNewChat === "true" && incomingInput) { //TODO: use types for useQueryState - if (incomingInput && chattype !== "tldraw") { + if (incomingInput && chattype !== "tldraw" && chattype !== "storm") { const params = new URLSearchParams(window.location.search); if ( params.get("imageUrl") && @@ -259,16 +259,7 @@ const InputBar = (props: InputBarProps) => { setIsFromClipboard("false"); setIsNewChat("false"); }, [isFromClipboard, isNewChat]); - // const ably = useAbly(); - // console.log( - // "ably", - // ably.channels - // .get(`channel_${props.chatId}`) - // .presence.get({ clientId: `room_${props.chatId}` }), - // ); - - // const { presenceData, updateStatus } = usePresence(`channel_${props.chatId}`); const preferences = usePreferences(); const { presenceData, updateStatus } = usePresence( `channel_${props.chatId}`, diff --git a/src/components/inputBar2.tsx b/src/components/inputBar2.tsx index 8b0ddf79..c818fbe0 100644 --- a/src/components/inputBar2.tsx +++ b/src/components/inputBar2.tsx @@ -23,6 +23,7 @@ import ModelSwitcher from "./modelswitcher"; import VadAudio from "./VadAudio"; import { useDropzone } from "react-dropzone"; import { X } from "lucide-react"; +import { parseAsString, useQueryState } from "next-usequerystate"; const isValidImageType = (value: string) => /^image\/(jpeg|png|jpg|webp)$/.test(value); @@ -102,6 +103,7 @@ const InputBar = (props: InputBarProps) => { const [disableInputs, setDisableInputs] = useState(false); const [isRagLoading, setIsRagLoading] = useState(false); const queryClient = useQueryClient(); + const [chatType] = useQueryState("model", parseAsString.withDefault("chat")); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); @@ -449,6 +451,8 @@ const InputBar = (props: InputBarProps) => { ? "" : props.dropZoneActive ? "Ask question about image" + : chatType === "storm" + ? "Enter the topic" : "Type your message here..." } autoFocus diff --git a/src/components/modelswitcher.tsx b/src/components/modelswitcher.tsx index 0710c4b3..61780fa6 100644 --- a/src/components/modelswitcher.tsx +++ b/src/components/modelswitcher.tsx @@ -14,6 +14,7 @@ import { import { Button } from "@/components/button"; import { Cpu, Layers, PenTool, Settings } from "lucide-react"; import { ChatType } from "@/lib/types"; +import { BookOpenText } from "@phosphor-icons/react"; export interface InputBarActionProps extends React.ButtonHTMLAttributes { @@ -34,6 +35,8 @@ const ModelSwitcher = React.forwardRef( ) : chattype === "tldraw" ? ( + ) : chattype === "storm" ? ( + ) : ( ); @@ -62,6 +65,11 @@ const ModelSwitcher = React.forwardRef( Simple Ella + {isHome ? ( + + Article + + ) : null} AIModels diff --git a/src/components/ui/sooner.tsx b/src/components/ui/sooner.tsx new file mode 100644 index 00000000..549cf841 --- /dev/null +++ b/src/components/ui/sooner.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index bc2d5a33..ae5879c2 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -20,7 +20,14 @@ export type SnapShot = { tldraw_snapshot: Array; }; -export const chattype = z.enum(["chat", "tldraw", "rag", "ella", "advanced"]); +export const chattype = z.enum([ + "chat", + "tldraw", + "rag", + "ella", + "advanced", + "storm", +]); export type ChatType = z.infer; export interface PostBody { user_id: string; From acf1863e8267bd7f8bd7b9f115d6ee030ca240aa Mon Sep 17 00:00:00 2001 From: Prince Baghel Date: Thu, 17 Oct 2024 12:49:35 +0530 Subject: [PATCH 2/2] chore: article generation --- src/app/api/storm/route.ts | 110 ++++++++++++++++-------------------- src/components/chat.tsx | 17 +++--- src/components/chatcard.tsx | 18 +++++- 3 files changed, 75 insertions(+), 70 deletions(-) diff --git a/src/app/api/storm/route.ts b/src/app/api/storm/route.ts index acbeaa60..73dbb201 100644 --- a/src/app/api/storm/route.ts +++ b/src/app/api/storm/route.ts @@ -1,77 +1,65 @@ -import { serve } from "@upstash/qstash/nextjs"; import axios from "axios"; import { env } from "@/app/env.mjs"; import { saveToDB } from "@/utils/apiHelper"; import { auth } from "@clerk/nextjs"; import { nanoid } from "ai"; -import { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; export const POST = async (request: NextRequest) => { const url = request.url; const urlArray = url.split("/"); const { orgSlug } = auth(); + const body = await request.json(); - const handler = serve(async (context) => { - const body = context.requestPayload as { topic: string }; - const stormResponse = await context.run("Initiating Storm to generate article", async () => { - const response = await axios.post( - `${env.KEYCLOAK_BASE_URL}/realms/${env.KEYCLOAK_REALM}/protocol/openid-connect/token`, - new URLSearchParams({ - client_id: env.KEYCLOAK_CLIENT_ID, - client_secret: env.KEYCLOAK_CLIENT_SECRET, - grant_type: "client_credentials", - }), - { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }, - ); - const accessToken = response.data.access_token; + const response = await axios.post( + `${env.KEYCLOAK_BASE_URL}/realms/${env.KEYCLOAK_REALM}/protocol/openid-connect/token`, + new URLSearchParams({ + client_id: env.KEYCLOAK_CLIENT_ID, + client_secret: env.KEYCLOAK_CLIENT_SECRET, + grant_type: "client_credentials", + }), + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }, + ); + const accessToken = response.data.access_token; - const stormResponse = await axios.post( - `${env.STORM_ENDPOINT}`, - { - topic: body.topic, - search_top_k: 5, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - return stormResponse.data; - }); + const stormResponse = await axios.post( + `${env.STORM_ENDPOINT}`, + { + topic: body.topic, + search_top_k: 5, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); - await context.run("Saving Article to db", async () => { - const body = context.requestPayload as { - topic: string; - chatId: string; - orgId: string; - userId: string; - }; - const chatId = body.chatId; - const orgId = body.orgId; - const userId = body.userId; - const latestResponse = { - id: nanoid(), - role: "assistant" as const, - content: stormResponse.content, - createdAt: new Date(), - audio: "", - }; + const chatId = body.chatId; + const orgId = body.orgId; + const userId = body.userId; + const latestResponse = { + id: nanoid(), + role: "assistant" as const, + content: stormResponse?.data?.content, + createdAt: new Date(), + audio: "", + }; - await saveToDB({ - _chat: [], - chatId: Number(chatId), - orgSlug: orgSlug as string, - latestResponse: latestResponse, - userId: userId, - orgId: orgId, - urlArray: urlArray, - }); - }); + await saveToDB({ + _chat: [], + chatId: Number(chatId), + orgSlug: orgSlug as string, + latestResponse: latestResponse, + userId: userId, + orgId: orgId, + urlArray: urlArray, + }); + return NextResponse.json({ + data: stormResponse.data, }); - return await handler(request); }; diff --git a/src/components/chat.tsx b/src/components/chat.tsx index b58aecfe..931fc28f 100644 --- a/src/components/chat.tsx +++ b/src/components/chat.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { ChatType } from "@/lib/types"; import InputBar from "@/components/inputBar"; import { Message, useChat } from "ai/react"; @@ -55,6 +55,7 @@ export default function Chat(props: ChatProps) { ); const [isNewChat, setIsNewChat] = useQueryState("new"); const [incomingInput] = useQueryState("input"); + const soonToastRef = useRef(); const { mutate: InitArticleGeneration } = useMutation({ mutationFn: async ({ topic, @@ -67,6 +68,11 @@ export default function Chat(props: ChatProps) { orgId: string; userId: string; }) => { + soonToastRef.current = soonerToast("Generating your article", { + description: "Please wait for 2 mins", + duration: 300 * 1000, + }); + console.log("storm"); const resp = await axios.post("/api/storm", { topic: topic, chatId: chatId, @@ -76,16 +82,11 @@ export default function Chat(props: ChatProps) { return resp.data; }, onSuccess: (data, vars, context) => { - soonerToast("Generating your article", { - description: "Please wait for 2 mins", - duration: 300 * 1000, - }); - //TODO: set workflow id in state and make query to start invalidating - console.log("workflow id", data.workflowRunId); + soonerToast.dismiss(soonToastRef.current); }, onError: (error: any, vars, context) => { - console.error(error?.message); + soonerToast.dismiss(soonToastRef.current); soonerToast("Something went wrong ", { description: "Sunday, December 03, 2023 at 9:00 AM", duration: 5 * 1000, diff --git a/src/components/chatcard.tsx b/src/components/chatcard.tsx index 622bf7d0..79f08443 100644 --- a/src/components/chatcard.tsx +++ b/src/components/chatcard.tsx @@ -13,7 +13,7 @@ import { } from "@/components/card"; import Chatusers, { getUserIdList } from "@/components/chatusersavatars"; import { CircleNotch } from "@phosphor-icons/react"; -import { ChatEntry, ChatLog } from "@/lib/types"; +import { ChatEntry, ChatLog, ChatType } from "@/lib/types"; import Image from "next/image"; import AudioButton from "@/components//audioButton"; import { useRouter } from "next/navigation"; @@ -30,6 +30,22 @@ type Props = { isHome?: boolean; }; +const getChatType = (type: ChatType) => { + if (type === "tldraw") { + return "Tldraw"; + } else if (type === "advanced") { + return "Advanced"; + } else if (type === "ella") { + return "Ella"; + } else if (type === "rag") { + return "Rag"; + } else if (type === "storm") { + return "Article"; + } else { + return "Simple Chat"; + } +}; + const Chatcard = ({ chat, uid,