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;