diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5a7615cb..0df65776 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}}
LITELLM_BASE_URL: ${{secrets.LITELLM_BASE_URL}}
LITELLM_API_KEY: ${{secrets.LITELLM_API_KEY}}
ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}}
diff --git a/package.json b/package.json
index e52c2687..141dc556 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",
@@ -97,7 +98,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",
@@ -117,6 +118,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..73dbb201
--- /dev/null
+++ b/src/app/api/storm/route.ts
@@ -0,0 +1,65 @@
+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, 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 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}`,
+ },
+ },
+ );
+
+ 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,
+ });
+ return NextResponse.json({
+ data: stormResponse.data,
+ });
+};
diff --git a/src/app/env.mjs b/src/app/env.mjs
index fc7aee84..fc5ba2fd 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),
// LITELLM
LITELLM_BASE_URL: z.string().min(1),
LITELLM_API_KEY: z.string().min(10),
@@ -67,10 +73,15 @@ 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,
// LITELLM
LITELLM_BASE_URL: process.env.LITELLM_BASE_URL,
- LITELLM_API_KEY: process.env.LITELLM_API_KEY,
- // Anthropic
+ LITELLM_API_KEY: process.env.LITELLM_API_KEY, // Anthropic
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
// Clerk (Auth)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
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 9a579a9c..b33624b9 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -101,7 +101,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..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";
@@ -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,59 @@ export default function Chat(props: ChatProps) {
const [chattype, setChattype] = useState(
props?.type || incomingModel || "chat",
);
+ const [isNewChat, setIsNewChat] = useQueryState("new");
+ const [incomingInput] = useQueryState("input");
+ const soonToastRef = useRef();
+ const { mutate: InitArticleGeneration } = useMutation({
+ mutationFn: async ({
+ topic,
+ chatId,
+ orgId,
+ userId,
+ }: {
+ topic: string;
+ chatId: string;
+ 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,
+ orgId: orgId,
+ userId: userId,
+ });
+ return resp.data;
+ },
+ onSuccess: (data, vars, context) => {
+ //TODO: set workflow id in state and make query to start invalidating
+ soonerToast.dismiss(soonToastRef.current);
+ },
+ onError: (error: any, vars, context) => {
+ soonerToast.dismiss(soonToastRef.current);
+ 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 +153,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 +354,32 @@ export default function Chat(props: ChatProps) {
/>
)}
-
+ {chattype !== "storm" ? (
+
+ ) : null}
>
)}
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,
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;