From 234034763b2cf31065fb1f8b7123355035a5206a Mon Sep 17 00:00:00 2001 From: Prince Baghel Date: Wed, 25 Sep 2024 17:56:15 +0530 Subject: [PATCH] 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;