diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx index e03582f350..cca311b4f3 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx @@ -6,43 +6,48 @@ import { SidebarProvider } from "@/context/sidebar-context"; import { cookies } from "next/headers"; import dynamic from "next/dynamic"; import { redirect } from "next/navigation"; +import { UserProvider } from "@/store/user/provider"; +import { AssistantSheet } from "@/components/sheets/assistant-sheet"; const HotKeys = dynamic( - () => import("@/components/hot-keys").then((mod) => mod.HotKeys), - { - ssr: true, - }, + () => import("@/components/hot-keys").then((mod) => mod.HotKeys), + { + ssr: true, + }, ); export default async function Layout({ - children, - params, + children, + params, }: { - children: React.ReactNode; - params: Promise<{ orgId: string }>; + children: React.ReactNode; + params: Promise<{ orgId: string }>; }) { - const session = await auth(); - const orgId = (await params).orgId; - const cookieStore = await cookies(); - const isCollapsed = cookieStore.get("sidebar-collapsed")?.value === "true"; + const session = await auth(); + const orgId = (await params).orgId; + const cookieStore = await cookies(); + const isCollapsed = cookieStore.get("sidebar-collapsed")?.value === "true"; - if (!session) { - redirect("/auth"); - } + if (!session) { + redirect("/auth"); + } - if (!orgId) { - redirect("/"); - } + if (!orgId) { + redirect("/"); + } - return ( - - } isCollapsed={isCollapsed}> -
-
-
{children}
-
-
- -
- ); + return ( + + + } isCollapsed={isCollapsed}> +
+
+
{children}
+
+ +
+ +
+
+ ); } diff --git a/apps/app/src/app/[locale]/layout.tsx b/apps/app/src/app/[locale]/layout.tsx index f0d874cb71..1eefc9897d 100644 --- a/apps/app/src/app/[locale]/layout.tsx +++ b/apps/app/src/app/[locale]/layout.tsx @@ -10,104 +10,101 @@ import localFont from "next/font/local"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import { Toaster } from "sonner"; import { Providers } from "./providers"; -import { ChatProvider } from "@/components/chat/ChatProvider"; export const metadata: Metadata = { - metadataBase: new URL("https://app.trycomp.ai"), - title: "Comp AI | Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - twitter: { - title: "Comp AI | Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - images: [ - { - url: "https://cdn.trycomp.ai/opengraph-image.jpg", - width: 800, - height: 600, - }, - { - url: "https://cdn.trycomp.ai/opengraph-image.jpg", - width: 1800, - height: 1600, - }, - ], - }, - openGraph: { - title: "Comp AI | Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - url: "https://app.trycomp.ai", - siteName: "Comp AI", - images: [ - { - url: "https://cdn.trycomp.ai/opengraph-image.jpg", - width: 800, - height: 600, - }, - { - url: "https://cdn.trycomp.ai/opengraph-image.jpg", - width: 1800, - height: 1600, - }, - ], - locale: "en_US", - type: "website", - }, + metadataBase: new URL("https://app.trycomp.ai"), + title: "Comp AI | Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + twitter: { + title: "Comp AI | Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + images: [ + { + url: "https://cdn.trycomp.ai/opengraph-image.jpg", + width: 800, + height: 600, + }, + { + url: "https://cdn.trycomp.ai/opengraph-image.jpg", + width: 1800, + height: 1600, + }, + ], + }, + openGraph: { + title: "Comp AI | Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + url: "https://app.trycomp.ai", + siteName: "Comp AI", + images: [ + { + url: "https://cdn.trycomp.ai/opengraph-image.jpg", + width: 800, + height: 600, + }, + { + url: "https://cdn.trycomp.ai/opengraph-image.jpg", + width: 1800, + height: 1600, + }, + ], + locale: "en_US", + type: "website", + }, }; export const viewport = { - width: "device-width", - initialScale: 1, - maximumScale: 1, - userScalable: false, - themeColor: [ - { media: "(prefers-color-scheme: light)" }, - { media: "(prefers-color-scheme: dark)" }, - ], + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, + themeColor: [ + { media: "(prefers-color-scheme: light)" }, + { media: "(prefers-color-scheme: dark)" }, + ], }; const font = localFont({ - src: "/../../../public/fonts/GeneralSans-Variable.ttf", - display: "swap", - variable: "--font-general-sans", + src: "/../../../public/fonts/GeneralSans-Variable.ttf", + display: "swap", + variable: "--font-general-sans", }); export const preferredRegion = ["auto"]; export const maxDuration = 5; if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) { - initializeServer({ - apiKey: env.NEXT_PUBLIC_POSTHOG_KEY, - apiHost: env.NEXT_PUBLIC_POSTHOG_HOST, - }); + initializeServer({ + apiKey: env.NEXT_PUBLIC_POSTHOG_KEY, + apiHost: env.NEXT_PUBLIC_POSTHOG_HOST, + }); } export default async function Layout(props: { - children: React.ReactNode; - params: Promise<{ locale: string }>; + children: React.ReactNode; + params: Promise<{ locale: string }>; }) { - const params = await props.params; - const { locale } = params; - const { children } = props; + const params = await props.params; + const { locale } = params; + const { children } = props; - return ( - - - - - - -
{children}
-
-
-
-
- - - - ); + return ( + + + + + +
{children}
+
+
+
+ + + + ); } diff --git a/apps/app/src/app/api/chat/route.ts b/apps/app/src/app/api/chat/route.ts index 232fac3edb..f4a0f85739 100644 --- a/apps/app/src/app/api/chat/route.ts +++ b/apps/app/src/app/api/chat/route.ts @@ -1,33 +1,73 @@ -import { openai } from "@ai-sdk/openai"; -import { streamText } from "ai"; -import { env } from "@/env.mjs"; +import { auth } from "@/auth"; +import { model, type modelID } from "@/hooks/ai/providers"; +import { + getOrgTool, + getVendorsTool, + getPoliciesTool, + getRisksTool, + getCloudTestsTool, + getFrameworkTool, + getVendorDetailsTool, + getPolicyDetails, +} from "@/hooks/ai/tools"; +import { db } from "@bubba/db"; +import { streamText, type UIMessage } from "ai"; + +export const maxDuration = 30; export async function POST(req: Request) { - const { messages } = await req.json(); + const { + messages, + selectedModel, + }: { messages: UIMessage[]; selectedModel: modelID } = await req.json(); + + const session = await auth(); - if (!env.OPENAI_API_KEY) { - return new Response( - JSON.stringify({ error: "OpenAI API key is not configured" }), - { status: 500 }, - ); + if (!session?.user.organizationId) { + return new Response("Unauthorized", { status: 401 }); } - try { - const result = await streamText({ - model: openai("gpt-4o-mini"), - system: - "You are a helpful assistant. Provide concise, accurate, and helpful responses to user queries.", - messages, - }); - - return result.toDataStreamResponse(); - } catch (error) { - console.error("Error in chat API:", error); - return new Response( - JSON.stringify({ - error: "An error occurred while processing your request", - }), - { status: 500 }, - ); + const org = await db.organization.findUnique({ + where: { + id: session.user.organizationId, + }, + }); + + if (!org) { + return new Response("Organization not found", { status: 404 }); } + + const systemPrompt = ` + You are an expert in governance, risk, and compliance. + + You're a helpful assistant in Comp AI, a platform that helps companies + get compliant with frameworks like SOC 2, ISO 27001 and GDPR - similar to Vanta and Drata. + + You have access to various tools to help gather information. You can: + 1. Use multiple tools in sequence to gather comprehensive information + 2. Chain tool calls together to get more detailed information + 3. Use tools to verify or expand upon information from previous tool calls + + When using tools: + - Start with broader queries to get an overview + - Then use specific queries to get detailed information +`; + + const result = streamText({ + model: model.languageModel(selectedModel), + system: systemPrompt, + messages, + tools: { + getOrgTool, + getVendorsTool, + getPoliciesTool, + getRisksTool, + getCloudTestsTool, + getFrameworkTool, + getVendorDetailsTool, + getPolicyDetails, + }, + }); + + return result.toDataStreamResponse({ sendReasoning: true }); } diff --git a/apps/app/src/components/ai/chat-empty.tsx b/apps/app/src/components/ai/chat-empty.tsx new file mode 100644 index 0000000000..57a048247f --- /dev/null +++ b/apps/app/src/components/ai/chat-empty.tsx @@ -0,0 +1,17 @@ +import { LogoSpinner } from "../logo-spinner"; + +type Props = { + firstName: string; +}; + +export function ChatEmpty({ firstName }: Props) { + return ( +
+ + + Hi {firstName}, how can I help
+ you today? +
+
+ ); +} diff --git a/apps/app/src/components/ai/chat-text-area.tsx b/apps/app/src/components/ai/chat-text-area.tsx new file mode 100644 index 0000000000..d80423ee0b --- /dev/null +++ b/apps/app/src/components/ai/chat-text-area.tsx @@ -0,0 +1,117 @@ +import type { modelID } from "@/hooks/ai/providers"; +import { Icons } from "@bubba/ui/icons"; +import { Popover, PopoverTrigger, PopoverContent } from "@bubba/ui/popover"; +import { Textarea as ShadcnTextarea } from "@bubba/ui/textarea"; +import { ArrowUp } from "lucide-react"; +import app from "next/app"; +import { useRouter } from "next/navigation"; + +interface InputProps { + input: string; + handleInputChange: (event: React.ChangeEvent) => void; + isLoading: boolean; + status: string; + stop: () => void; + selectedModel: modelID; + setSelectedModel: (model: modelID) => void; +} + +export const ChatTextarea = ({ + input, + handleInputChange, + isLoading, + status, + stop, + selectedModel, + setSelectedModel, +}: InputProps) => { + const router = useRouter(); + + const handleOpenUrl = (url: string) => { + router.push(url); + }; + return ( +
+ { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (input.trim() && !isLoading) { + // @ts-expect-error err + const form = e.target.closest("form"); + if (form) form.requestSubmit(); + } + } + }} + /> + +
+ + +
+ +
+
+ + +
    +
  • + +
  • +
  • + +
  • + +
  • + +
  • +
+
+
+ +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/apps/app/src/components/ai/chat.tsx b/apps/app/src/components/ai/chat.tsx new file mode 100644 index 0000000000..4f8f16a6c3 --- /dev/null +++ b/apps/app/src/components/ai/chat.tsx @@ -0,0 +1,63 @@ +"use client"; + +import type { modelID } from "@/hooks/ai/providers"; +import { useChat } from "@ai-sdk/react"; +import { useState } from "react"; +import { Messages } from "./messages"; +import { ChatEmpty } from "./chat-empty"; +import { useSession } from "next-auth/react"; +import { ChatTextarea } from "./chat-text-area"; +import { ScrollArea } from "@bubba/ui/scroll-area"; + +export default function Chat() { + const { data: session } = useSession(); + + const [selectedModel, setSelectedModel] = useState( + "deepseek-r1-distill-llama-70b", + ); + + const { + messages, + input, + handleInputChange, + handleSubmit, + error, + status, + stop, + } = useChat({ + maxSteps: 5, + body: { + selectedModel, + }, + }); + + const isLoading = status === "streaming" || status === "submitted"; + + if (error) return
{error.message}
; + + return ( +
+ + {messages.length === 0 ? ( +
+ +
+ ) : ( + + )} +
+ +
+ + +
+ ); +} \ No newline at end of file diff --git a/apps/app/src/components/ai/icons.tsx b/apps/app/src/components/ai/icons.tsx new file mode 100644 index 0000000000..89efe5e9e0 --- /dev/null +++ b/apps/app/src/components/ai/icons.tsx @@ -0,0 +1,158 @@ +import Link from "next/link"; +import * as React from "react"; +import type { SVGProps } from "react"; + +export const VercelIcon = ({ size = 17 }) => { + return ( + + + + ); +}; + +export const SpinnerIcon = ({ size = 16 }: { size?: number }) => ( + + + + + + + + + + + + + + + + + + + +); + +export const Github = (props: SVGProps) => ( + + + +); + +export function StarButton() { + return ( + + + Star on GitHub + + ); +} + +export const GroqIcon = ({ size = 16 }) => { + return ( + + + + + + + + + ); +}; \ No newline at end of file diff --git a/apps/app/src/components/ai/markdown.tsx b/apps/app/src/components/ai/markdown.tsx new file mode 100644 index 0000000000..c561c16f1b --- /dev/null +++ b/apps/app/src/components/ai/markdown.tsx @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { memo } from "react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import remarkGfm from "remark-gfm"; + +const components: Partial = { + pre: ({ children }) => <>{children}, + ol: ({ node, children, ...props }) => { + return ( +
    + {children} +
+ ); + }, + li: ({ node, children, ...props }) => { + return ( +
  • + {children} +
  • + ); + }, + ul: ({ node, children, ...props }) => { + return ( +
      + {children} +
    + ); + }, + p: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + strong: ({ node, children, ...props }) => { + return ( + + {children} + + ); + }, + a: ({ children, ...props }) => { + return ( + + {children} + + ); + }, + h1: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h2: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h3: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h4: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h5: ({ node, children, ...props }) => { + return ( +
    + {children} +
    + ); + }, + h6: ({ node, children, ...props }) => { + return ( +
    + {children} +
    + ); + }, +}; + +const remarkPlugins = [remarkGfm]; + +const NonMemoizedMarkdown = ({ children }: { children: string }) => { + return ( + + {children} + + ); +}; + +export const Markdown = memo( + NonMemoizedMarkdown, + (prevProps, nextProps) => prevProps.children === nextProps.children, +); \ No newline at end of file diff --git a/apps/app/src/components/ai/message.tsx b/apps/app/src/components/ai/message.tsx new file mode 100644 index 0000000000..4023d407c3 --- /dev/null +++ b/apps/app/src/components/ai/message.tsx @@ -0,0 +1,245 @@ +"use client"; + +import type { Message as TMessage } from "ai"; +import { AnimatePresence, motion } from "motion/react"; +import { memo, useCallback, useEffect, useState } from "react"; +import equal from "fast-deep-equal"; + +import { Markdown } from "./markdown"; +import { cn } from "@bubba/ui/cn"; +import { + CheckCircle, + ChevronDownIcon, + ChevronUpIcon, + Loader2, + PocketKnife, + SparklesIcon, + StopCircle, +} from "lucide-react"; +import { SpinnerIcon } from "./icons"; + +interface ReasoningPart { + type: "reasoning"; + reasoning: string; + details: Array<{ type: "text"; text: string }>; +} + +interface ReasoningMessagePartProps { + part: ReasoningPart; + isReasoning: boolean; +} + +export function ReasoningMessagePart({ + part, + isReasoning, +}: ReasoningMessagePartProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const variants = { + collapsed: { + height: 0, + opacity: 0, + marginTop: 0, + marginBottom: 0, + }, + expanded: { + height: "auto", + opacity: 1, + marginTop: "1rem", + marginBottom: 0, + }, + }; + + const memoizedSetIsExpanded = useCallback((value: boolean) => { + setIsExpanded(value); + }, []); + + useEffect(() => { + memoizedSetIsExpanded(isReasoning); + }, [isReasoning, memoizedSetIsExpanded]); + + return ( +
    + {isReasoning ? ( +
    +
    Reasoning
    +
    + +
    +
    + ) : ( +
    +
    Reasoned for a few seconds
    + +
    + )} + + + {isExpanded && ( + + {part.details.map((detail, detailIndex) => + detail.type === "text" ? ( + {detail.text} + ) : ( + "" + ), + )} + + )} + +
    + ); +} + +const PurePreviewMessage = ({ + message, + isLatestMessage, + status, +}: { + message: TMessage; + isLoading: boolean; + status: "error" | "submitted" | "streaming" | "ready"; + isLatestMessage: boolean; +}) => { + return ( + + +
    + {message.role === "assistant" && ( +
    +
    + +
    +
    + )} + +
    + {message.parts?.map((part, i) => { + switch (part.type) { + case "text": + return ( + +
    + {part.text} +
    +
    + ); + case "tool-invocation": { + const { toolName, state } = part.toolInvocation; + + return ( + +
    +
    + +
    +
    +
    + {state === "call" ? "Calling" : "Called"}{" "} + + {toolName} + +
    +
    +
    + {state === "call" ? ( + isLatestMessage && status !== "ready" ? ( + + ) : ( + + ) + ) : state === "result" ? ( + + ) : null} +
    +
    +
    + ); + } + case "reasoning": { + return ( + + ); + } + default: + return null; + } + })} +
    +
    +
    +
    + ); +}; + +export const Message = memo(PurePreviewMessage, (prevProps, nextProps) => { + if (prevProps.status !== nextProps.status) return false; + + if (prevProps.message.annotations !== nextProps.message.annotations) + return false; + + if (!equal(prevProps.message.parts, nextProps.message.parts)) return false; + + return true; +}); \ No newline at end of file diff --git a/apps/app/src/components/ai/messages.tsx b/apps/app/src/components/ai/messages.tsx new file mode 100644 index 0000000000..ab8476bb81 --- /dev/null +++ b/apps/app/src/components/ai/messages.tsx @@ -0,0 +1,34 @@ +import type { Message as TMessage } from "ai"; +import { Message } from "./message"; +import { useScrollToBottom } from "@/hooks/use-scroll-to-bottom"; + +export const Messages = ({ + messages, + isLoading, + status, +}: { + messages: TMessage[]; + isLoading: boolean; + status: "error" | "submitted" | "streaming" | "ready"; +}) => { + const [containerRef, endRef] = useScrollToBottom(); + return ( +
    +
    + {messages.map((m, i) => ( + + ))} +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/apps/app/src/components/ai/model-picker.tsx b/apps/app/src/components/ai/model-picker.tsx new file mode 100644 index 0000000000..4de16e4497 --- /dev/null +++ b/apps/app/src/components/ai/model-picker.tsx @@ -0,0 +1,43 @@ +"use client"; +import type { modelID } from "@/hooks/ai/providers"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@bubba/ui/select"; + +interface ModelPickerProps { + selectedModel: modelID; + setSelectedModel: (model: modelID) => void; +} + +const MODELS: Record = { + "deepseek-r1-distill-llama-70b": "A reasoning model", +}; + +export const ModelPicker = ({ + selectedModel, + setSelectedModel, +}: ModelPickerProps) => { + return ( +
    + +
    + ); +}; \ No newline at end of file diff --git a/apps/app/src/components/animated-layout.tsx b/apps/app/src/components/animated-layout.tsx index 559d0cfe3b..08c4419c47 100644 --- a/apps/app/src/components/animated-layout.tsx +++ b/apps/app/src/components/animated-layout.tsx @@ -1,27 +1,27 @@ -import { cn } from "../../../../packages/ui/src/utils"; +import { cn } from "@bubba/ui/cn"; interface AnimatedLayoutProps { - children: React.ReactNode; - sidebar: React.ReactNode; - isCollapsed: boolean; + children: React.ReactNode; + sidebar: React.ReactNode; + isCollapsed: boolean; } export function AnimatedLayout({ - children, - sidebar, - isCollapsed, + children, + sidebar, + isCollapsed, }: AnimatedLayoutProps) { - return ( -
    - -
    {children}
    -
    - ); + return ( +
    + +
    {children}
    +
    + ); } diff --git a/apps/app/src/components/chat/ChatButton.tsx b/apps/app/src/components/chat/ChatButton.tsx deleted file mode 100644 index fb6ef0ec51..0000000000 --- a/apps/app/src/components/chat/ChatButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ChatBubbleIcon } from "@radix-ui/react-icons"; -import { ChatInterface } from "./ChatInterface"; -import { Button } from "@bubba/ui/button"; -import { cn } from "@bubba/ui/cn"; - -interface ChatButtonProps { - className?: string; -} - -export function ChatButton({ className }: ChatButtonProps) { - const [isOpen, setIsOpen] = useState(false); - - const toggleChat = () => { - setIsOpen(!isOpen); - }; - - return ( -
    - {isOpen && setIsOpen(false)} />} - -
    - ); -} diff --git a/apps/app/src/components/chat/ChatInterface.tsx b/apps/app/src/components/chat/ChatInterface.tsx deleted file mode 100644 index 92d93f9be9..0000000000 --- a/apps/app/src/components/chat/ChatInterface.tsx +++ /dev/null @@ -1,151 +0,0 @@ -"use client"; - -import { useChat } from "@/hooks/chat/useChat"; -import type { ChatMessage } from "@/hooks/chat/useChat"; -import { useRef, useEffect } from "react"; -import { Cross2Icon, PaperPlaneIcon, PersonIcon } from "@radix-ui/react-icons"; -import { Button } from "@bubba/ui/button"; -import { Input } from "@bubba/ui/input"; -import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; -import { cn } from "@bubba/ui/cn"; -import { Avatar, AvatarFallback, AvatarImage } from "@bubba/ui/avatar"; - -interface ChatInterfaceProps { - onClose: () => void; -} - -export function ChatInterface({ onClose }: ChatInterfaceProps) { - const { messages, input, handleInputChange, handleSubmit, isLoading } = - useChat(); - const messagesEndRef = useRef(null); - - // Scroll to bottom when messages change - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); - - // Helper to determine if a specific message is currently being generated - const isGeneratingMessage = (message: ChatMessage, index: number) => { - return ( - isLoading && - index === messages.length - 1 && - message.role === "assistant" && - message.content === "" - ); - }; - - // Format timestamp - const formatTime = () => { - const now = new Date(); - return now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); - }; - - return ( - - -
    - Chat Assistant - -
    -
    - - - {messages.length === 0 ? ( -
    -

    How can I help you today?

    -
    - ) : ( - messages.map((message: ChatMessage, index) => ( -
    -
    - {message.role === "user" ? ( - - - U - - - ) : ( - - - AI - - - )} - -
    -
    - - {message.role === "user" ? "User" : "Assistant"} - - - {formatTime()} - -
    - - {isGeneratingMessage(message, index) ? ( -
    -
    -
    -
    -
    -
    - - Generating response... - -
    - ) : ( -

    - {message.content} -

    - )} -
    -
    -
    - )) - )} -
    - - -
    -
    - - -
    -
    - - ); -} diff --git a/apps/app/src/components/chat/ChatProvider.tsx b/apps/app/src/components/chat/ChatProvider.tsx deleted file mode 100644 index bb4d2eda7e..0000000000 --- a/apps/app/src/components/chat/ChatProvider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import { ChatButton } from "./ChatButton"; - -interface ChatProviderProps { - children: ReactNode; -} - -export function ChatProvider({ children }: ChatProviderProps) { - return ( - <> - {children} - - - ); -} diff --git a/apps/app/src/components/hot-keys.tsx b/apps/app/src/components/hot-keys.tsx index 23ac6fcc02..19d0a162b4 100644 --- a/apps/app/src/components/hot-keys.tsx +++ b/apps/app/src/components/hot-keys.tsx @@ -1,13 +1,17 @@ "use client"; -import { useAssistantStore } from "@/store/assistant"; import { signOut } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useHotkeys } from "react-hotkeys-hook"; +import { useQueryState } from "nuqs"; export function HotKeys() { const router = useRouter(); - const { setOpen } = useAssistantStore(); + const [, setAssistantOpen] = useQueryState("assistant", { + history: "push", + parse: (value) => value === "true", + serialize: (value) => value.toString(), + }); const handleSignOut = async () => { await signOut(); @@ -56,7 +60,7 @@ export function HotKeys() { useHotkeys("meta+k", (evt) => { evt.preventDefault(); - setOpen(true); + setAssistantOpen(true); }); return null; diff --git a/apps/app/src/components/sheets/assistant-sheet.tsx b/apps/app/src/components/sheets/assistant-sheet.tsx new file mode 100644 index 0000000000..1bf3e7cec6 --- /dev/null +++ b/apps/app/src/components/sheets/assistant-sheet.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Sheet, SheetContent } from "@bubba/ui/sheet"; +import { useQueryState } from "nuqs"; +import Chat from "../ai/chat"; + +export function AssistantSheet() { + const [isOpen, setIsOpen] = useQueryState("assistant", { + history: "push", + parse: (value) => value === "true", + serialize: (value) => value.toString(), + }); + + return ( + setIsOpen(open)}> + + + + + ); +} \ No newline at end of file diff --git a/apps/app/src/env.mjs b/apps/app/src/env.mjs index 7a8f2848a5..9d5e041b92 100644 --- a/apps/app/src/env.mjs +++ b/apps/app/src/env.mjs @@ -26,6 +26,7 @@ export const env = createEnv({ AWS_SECRET_ACCESS_KEY: z.string(), AWS_REGION: z.string(), AWS_BUCKET_NAME: z.string(), + GROQ_API_KEY: z.string(), }, client: { @@ -63,6 +64,7 @@ export const env = createEnv({ AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, AWS_REGION: process.env.AWS_REGION, AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME, + GROQ_API_KEY: process.env.GROQ_API_KEY, }, skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, diff --git a/apps/app/src/hooks/ai/providers.ts b/apps/app/src/hooks/ai/providers.ts new file mode 100644 index 0000000000..f8d742fa42 --- /dev/null +++ b/apps/app/src/hooks/ai/providers.ts @@ -0,0 +1,19 @@ +import { groq } from "@ai-sdk/groq"; +import { + customProvider, + extractReasoningMiddleware, + wrapLanguageModel, +} from "ai"; + +export const model = customProvider({ + languageModels: { + "deepseek-r1-distill-llama-70b": wrapLanguageModel({ + middleware: extractReasoningMiddleware({ + tagName: "think", + }), + model: groq("deepseek-r1-distill-llama-70b"), + }), + }, +}); + +export type modelID = Parameters<(typeof model)["languageModel"]>["0"]; diff --git a/apps/app/src/hooks/ai/tools.ts b/apps/app/src/hooks/ai/tools.ts new file mode 100644 index 0000000000..5940c6a206 --- /dev/null +++ b/apps/app/src/hooks/ai/tools.ts @@ -0,0 +1,361 @@ +import { auth } from "@/auth"; +import { db } from "@bubba/db"; +import { tool } from "ai"; +import { z } from "zod"; + +// Organization Tools +export const getOrgTool = tool({ + description: "Get the organization details", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const org = await db.organization.findUnique({ + where: { id: session.user.organizationId }, + select: { + id: true, + name: true, + website: true, + tier: true, + setup: true, + frameworkId: true, + }, + }); + + if (!org) { + return { error: "Organization not found" }; + } + + return org; + }, +}); + +// Vendor Tools +export const getVendorsTool = tool({ + description: "Get all vendors for the organization", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const vendors = await db.vendor.findMany({ + where: { organizationId: session.user.organizationId }, + select: { + id: true, + name: true, + description: true, + category: true, + status: true, + inherentRisk: true, + residualRisk: true, + createdAt: true, + updatedAt: true, + owner: { + select: { + name: true, + email: true, + }, + }, + contacts: { + select: { + name: true, + email: true, + phone: true, + }, + }, + }, + }); + + return vendors; + }, +}); + +export const getVendorDetailsTool = tool({ + description: "Get detailed information about a specific vendor", + parameters: z.object({ + vendorId: z.string(), + }), + execute: async ({ vendorId }) => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const vendor = await db.vendor.findFirst({ + where: { + id: vendorId, + organizationId: session.user.organizationId, + }, + select: { + id: true, + name: true, + description: true, + category: true, + status: true, + inherentRisk: true, + residualRisk: true, + createdAt: true, + updatedAt: true, + owner: { + select: { + name: true, + email: true, + }, + }, + contacts: { + select: { + name: true, + email: true, + phone: true, + }, + }, + tasks: { + select: { + id: true, + title: true, + description: true, + status: true, + dueDate: true, + completedAt: true, + owner: { + select: { + name: true, + email: true, + }, + }, + }, + }, + comments: { + select: { + content: true, + createdAt: true, + owner: { + select: { + name: true, + email: true, + }, + }, + }, + }, + }, + }); + + if (!vendor) { + return { error: "Vendor not found" }; + } + + return vendor; + }, +}); + +// Risk Tools +export const getRisksTool = tool({ + description: "Get all risks for the organization", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const risks = await db.risk.findMany({ + where: { organizationId: session.user.organizationId }, + select: { + id: true, + title: true, + description: true, + category: true, + department: true, + status: true, + probability: true, + impact: true, + residual_probability: true, + residual_impact: true, + createdAt: true, + updatedAt: true, + owner: { + select: { + name: true, + email: true, + }, + }, + mitigationTasks: { + select: { + id: true, + title: true, + status: true, + dueDate: true, + completedAt: true, + }, + }, + }, + }); + + return risks; + }, +}); + +// Policy Tools +export const getPoliciesTool = tool({ + description: "Get all policies for the organization", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const policies = await db.organizationPolicy.findMany({ + where: { organizationId: session.user.organizationId }, + select: { + id: true, + policy: { + select: { + name: true, + }, + }, + }, + }); + + return policies; + }, +}); + +export const getPolicyDetails = tool({ + description: "Get detailed information about a specific policy", + parameters: z.object({ + policyName: z.string(), + }), + execute: async ({ policyName }) => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const policy = await db.organizationPolicy.findFirst({ + where: { + organizationId: session.user.organizationId, + policy: { + name: policyName, + }, + isArchived: false, + }, + select: { + id: true, + status: true, + createdAt: true, + updatedAt: true, + policy: { + select: { + id: true, + name: true, + description: true, + content: true, + version: true, + frequency: true, + department: true, + isRequiredToSign: true, + }, + }, + }, + }); + + if (!policy) { + return { error: "Policy not found" }; + } + + return policy; + }, +}); + +// Framework Tools +export const getFrameworkTool = tool({ + description: "Get the organization's framework details", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const orgFramework = await db.organizationFramework.findFirst({ + where: { organizationId: session.user.organizationId }, + select: { + id: true, + status: true, + adoptedAt: true, + lastAssessed: true, + nextAssessment: true, + framework: { + select: { + name: true, + description: true, + version: true, + categories: { + select: { + name: true, + description: true, + controls: { + select: { + code: true, + name: true, + description: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!orgFramework) { + return { error: "Framework not found" }; + } + + return orgFramework; + }, +}); + +// Cloud Test Tools +export const getCloudTestsTool = tool({ + description: "Get all cloud tests for the organization", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + const tests = await db.organizationIntegrationResults.findMany({ + where: { organizationId: session.user.organizationId }, + select: { + id: true, + title: true, + status: true, + severity: true, + remediation: true, + resultDetails: true, + completedAt: true, + organizationIntegrationId: true, + organizationId: true, + assignedUserId: true, + }, + }); + + return tests; + }, +}); diff --git a/apps/app/src/hooks/chat/useChat.ts b/apps/app/src/hooks/chat/useChat.ts deleted file mode 100644 index a7821cdbc9..0000000000 --- a/apps/app/src/hooks/chat/useChat.ts +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import { useChat as useAIChat } from "@ai-sdk/react"; -import { v4 as uuid } from "uuid"; - -export interface ChatMessage { - id: string; - role: "user" | "assistant"; - content: string; -} - -export function useChat() { - const { - messages: aiMessages, - input, - handleInputChange, - handleSubmit, - isLoading, - } = useAIChat({ - api: "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/api/chat", - }); - - // Convert AI SDK messages to our ChatMessage format - const messages: ChatMessage[] = aiMessages.map((msg) => ({ - id: msg.id || uuid(), - role: msg.role as "user" | "assistant", - content: msg.content, - })); - - return { - messages, - input, - handleInputChange, - handleSubmit, - isLoading, - }; -} diff --git a/apps/app/src/hooks/use-scroll-to-bottom.ts b/apps/app/src/hooks/use-scroll-to-bottom.ts new file mode 100644 index 0000000000..9710001855 --- /dev/null +++ b/apps/app/src/hooks/use-scroll-to-bottom.ts @@ -0,0 +1,32 @@ +import { useEffect, useRef, type RefObject } from "react"; + +export function useScrollToBottom(): [ + RefObject, + RefObject, +] { + const containerRef = useRef(null); + const endRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + const end = endRef.current; + + if (container && end) { + const observer = new MutationObserver(() => { + end.scrollIntoView({ behavior: "instant", block: "end" }); + }); + + observer.observe(container, { + childList: true, + subtree: true, + attributes: true, + characterData: true, + }); + + return () => observer.disconnect(); + } + }, []); + + // @ts-expect-error error + return [containerRef, endRef]; +} diff --git a/apps/app/src/store/assistant.ts b/apps/app/src/store/assistant.ts index a9abfe9ce3..f20af4f564 100644 --- a/apps/app/src/store/assistant.ts +++ b/apps/app/src/store/assistant.ts @@ -1,19 +1,13 @@ import { create } from "zustand"; interface AssistantState { - isOpen: boolean; - message?: string; - setOpen: (value: boolean | string | undefined) => void; - setClose: () => void; + isOpen: boolean; + message?: string; + setOpen: (message?: string) => void; } export const useAssistantStore = create()((set) => ({ - isOpen: false, - message: undefined, - setOpen: (value) => - set({ - isOpen: typeof value === "boolean" ? value : true, - message: typeof value === "string" ? value : undefined, - }), - setClose: () => set({ isOpen: false, message: undefined }), + isOpen: false, + message: undefined, + setOpen: (message) => set((state) => ({ isOpen: !state.isOpen, message })), })); diff --git a/apps/app/src/store/user/store.ts b/apps/app/src/store/user/store.ts index ae4f96ab8a..c3ab9e69af 100644 --- a/apps/app/src/store/user/store.ts +++ b/apps/app/src/store/user/store.ts @@ -1,27 +1,20 @@ -import type { User as NextAuthUser } from "next-auth"; +import type { User } from "next-auth"; import { createContext } from "react"; import { createStore } from "zustand"; -type User = { - id: string; - organizationId?: string; - full_name?: string; - avatar_url?: string; -} & NextAuthUser; - export interface UserProps { - data: User | null; + data: User | null; } export interface UserState extends UserProps { - setUser: (user: User) => void; + setUser: (user: User) => void; } export const createUserStore = (initProps: UserProps) => { - return createStore()((set) => ({ - data: initProps?.data, - setUser: (user: User) => set({ data: user }), - })); + return createStore()((set) => ({ + data: initProps?.data, + setUser: (user: User) => set({ data: user }), + })); }; export type UserStore = ReturnType; diff --git a/biome.json b/biome.json index 902a681eb6..9572599eea 100644 --- a/biome.json +++ b/biome.json @@ -15,6 +15,7 @@ "noExplicitAny": "off" }, "a11y": { + "useValidAriaRole": "off", "noSvgWithoutTitle": "off", "useKeyWithClickEvents": "off" }, diff --git a/bun.lock b/bun.lock index 0559d10c5b..e3e926a4e7 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "comp", "dependencies": { + "@ai-sdk/groq": "^1.2.1", "@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-securityhub": "^3.758.0", "@azure/core-http": "^3.0.5", @@ -17,6 +18,7 @@ "d3": "^7.9.0", "gitmoji": "^1.1.1", "gray-matter": "^4.0.3", + "motion": "^12.6.2", "pg": "^8.14.1", "sharp": "^0.33.5", "use-debounce": "^10.0.4", @@ -310,11 +312,13 @@ }, }, "packages": { + "@ai-sdk/groq": ["@ai-sdk/groq@1.2.1", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.1" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-e9Vn6sE6u+pm97YSK9+xiTgQ2ScRdipE5gAwXj/9HdgMnUyp3mDpWjFsmDM6bzyeb2iKOGv6f3eiRsLxOAPv4A=="], + "@ai-sdk/openai": ["@ai-sdk/openai@1.2.6", "", { "dependencies": { "@ai-sdk/provider": "1.0.12", "@ai-sdk/provider-utils": "2.1.14" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-0HxWjE3NDABAxu6u33Q253a4N+xxOCD/M2HeZuALC1O2gjmzLeytxjSbDA6QU0PnVN0uaYLOc2YT/keFF5eGrg=="], "@ai-sdk/provider": ["@ai-sdk/provider@1.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-88Uu1zJIE1UUOVJWfE2ybJXgiH8JJ97QY9fbmplErEbfa/k/1kF+tWMVAAJolF2aOGmazQGyQLhv4I9CCuVACw=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.1", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-BuExLp+NcpwsAVj1F4bgJuQkSqO/+roV9wM7RdIO+NVrcT8RBUTdXzf5arHt5T58VpK7bZyB2V9qigjaPHE+Dg=="], "@ai-sdk/react": ["@ai-sdk/react@1.1.24", "", { "dependencies": { "@ai-sdk/provider-utils": "2.1.14", "@ai-sdk/ui-utils": "1.1.20", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-20rVK34QCaEl/4u87/Y+JXt7eCzF60UwoJOjHJhlCdmFrbk6AfRsijv0ludA9ELArE+ulxkSf21L3FokkZ2Gmg=="], @@ -2694,6 +2698,8 @@ "module-details-from-path": ["module-details-from-path@1.0.3", "", {}, "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="], + "motion": ["motion@12.6.2", "", { "dependencies": { "framer-motion": "^12.6.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-8OBjjuC59WuWHKmPzVWT5M0t5kDxtkfMfHF1M7Iey6F/nvd0AI15YlPnpGlcagW/eOfkdWDO90U/K5LF/k55Yw=="], + "motion-dom": ["motion-dom@12.5.0", "", { "dependencies": { "motion-utils": "^12.5.0" } }, "sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ=="], "motion-utils": ["motion-utils@12.5.0", "", {}, "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA=="], @@ -3638,9 +3644,11 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], + "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.14", "", { "dependencies": { "@ai-sdk/provider": "1.0.12", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-0fNQwstwqHP+cE9bP9nQXeEhY/qJRGW5Y+AQrvwzd0EI2+gH1GmwI1IC/gIOksmz/gRy4G+kXXD+Fbq4T/jCbA=="], - "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], "@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -3648,8 +3656,16 @@ "@ai-sdk/react/@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.1.20", "", { "dependencies": { "@ai-sdk/provider": "1.0.12", "@ai-sdk/provider-utils": "2.1.14", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-k+qQh1YA8/kH12d8KQFWjr0UxyS9nLibzbLr2OY6fzpfZg8xIAKlZeH952JIp2XwAp72tesgbsAmA15cDldtyw=="], + "@ai-sdk/solid/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], + + "@ai-sdk/svelte/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], + "@ai-sdk/ui-utils/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + "@ai-sdk/ui-utils/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], + + "@ai-sdk/vue/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@auth/core/preact": ["preact@10.11.3", "", {}, "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="], @@ -3796,6 +3812,8 @@ "ai/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "0.0.26", "eventsource-parser": "^1.1.2", "nanoid": "^3.3.7", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ=="], + "ai/@ai-sdk/react": ["@ai-sdk/react@0.0.70", "", { "dependencies": { "@ai-sdk/provider-utils": "1.0.22", "@ai-sdk/ui-utils": "0.0.50", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ=="], "ansi-escapes/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], @@ -3942,6 +3960,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "motion/framer-motion": ["framer-motion@12.6.2", "", { "dependencies": { "motion-dom": "^12.6.1", "motion-utils": "^12.5.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-7LgPRlPs5aG8UxeZiMCMZz8firC53+2+9TnWV22tuSi38D3IFRxHRUqOREKckAkt6ztX+Dn6weLcatQilJTMcg=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "next-auth/next": ["next@15.2.3", "", { "dependencies": { "@next/env": "15.2.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.3", "@next/swc-darwin-x64": "15.2.3", "@next/swc-linux-arm64-gnu": "15.2.3", "@next/swc-linux-arm64-musl": "15.2.3", "@next/swc-linux-x64-gnu": "15.2.3", "@next/swc-linux-x64-musl": "15.2.3", "@next/swc-win32-arm64-msvc": "15.2.3", "@next/swc-win32-x64-msvc": "15.2.3", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w=="], @@ -4074,6 +4094,20 @@ "@ai-sdk/react/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "@ai-sdk/solid/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + + "@ai-sdk/solid/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "@ai-sdk/svelte/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + + "@ai-sdk/svelte/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "@ai-sdk/ui-utils/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "@ai-sdk/vue/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@0.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg=="], + + "@ai-sdk/vue/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "@auth/prisma-adapter/@auth/core/jose": ["jose@6.0.10", "", {}, "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw=="], "@auth/prisma-adapter/@auth/core/preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="], @@ -4164,6 +4198,8 @@ "@uploadthing/react/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "ai/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "are-we-there-yet/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "are-we-there-yet/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -4252,6 +4288,8 @@ "languine/@trigger.dev/sdk/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.6.1", "", { "dependencies": { "motion-utils": "^12.5.0" } }, "sha512-8XVsriTUEVOepoIDgE/LDGdg7qaKXWdt+wQA/8z0p8YzJDLYL8gbimZ3YkCLlj7bB2i/4UBD/g+VO7y9ZY0zHQ=="], + "next-auth/next/@next/env": ["@next/env@15.2.3", "", {}, "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw=="], "next-auth/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw=="], diff --git a/package.json b/package.json index 08bdb4863e..a0743d5abe 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "packages/*" ], "dependencies": { + "@ai-sdk/groq": "^1.2.1", "@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-securityhub": "^3.758.0", "@azure/core-http": "^3.0.5", @@ -40,6 +41,7 @@ "d3": "^7.9.0", "gitmoji": "^1.1.1", "gray-matter": "^4.0.3", + "motion": "^12.6.2", "pg": "^8.14.1", "sharp": "^0.33.5", "use-debounce": "^10.0.4" diff --git a/turbo.json b/turbo.json index ce35a37bd6..34f7398d2b 100644 --- a/turbo.json +++ b/turbo.json @@ -37,7 +37,8 @@ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "ANTHROPIC_API_KEY", - "REVALIDATION_SECRET" + "REVALIDATION_SECRET", + "GROQ_API_KEY" ], "inputs": ["$TURBO_DEFAULT$", ".env"], "dependsOn": ["^build", "^db:generate", "clean-react"], diff --git a/yarn.lock b/yarn.lock index 99668788fc..d7c6121273 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,6 +3,14 @@ # bun ./bun.lockb --hash: 0000000000000000-0000000000000000-0000000000000000-0000000000000000 +"@ai-sdk/groq@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@ai-sdk/groq/-/groq-1.2.1.tgz" + integrity sha512-e9Vn6sE6u+pm97YSK9+xiTgQ2ScRdipE5gAwXj/9HdgMnUyp3mDpWjFsmDM6bzyeb2iKOGv6f3eiRsLxOAPv4A== + dependencies: + "@ai-sdk/provider" "1.1.0" + "@ai-sdk/provider-utils" "2.2.1" + "@ai-sdk/openai@^0.0.66": version "0.0.66" resolved "https://registry.npmjs.org/@ai-sdk/openai/-/openai-0.0.66.tgz" @@ -40,6 +48,13 @@ dependencies: json-schema "^0.4.0" +"@ai-sdk/provider@1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz" + integrity sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew== + dependencies: + json-schema "^0.4.0" + "@ai-sdk/provider-utils@1.0.20": version "1.0.20" resolved "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.20.tgz" @@ -70,6 +85,15 @@ nanoid "^3.3.8" secure-json-parse "^2.7.0" +"@ai-sdk/provider-utils@2.2.1": + version "2.2.1" + resolved "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.1.tgz" + integrity sha512-BuExLp+NcpwsAVj1F4bgJuQkSqO/+roV9wM7RdIO+NVrcT8RBUTdXzf5arHt5T58VpK7bZyB2V9qigjaPHE+Dg== + dependencies: + "@ai-sdk/provider" "1.1.0" + nanoid "^3.3.8" + secure-json-parse "^2.7.0" + "@ai-sdk/react@0.0.70": version "0.0.70" resolved "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.70.tgz" @@ -1223,14 +1247,14 @@ formdata-node "^4.3.2" node-fetch "^2.6.7" -"@bubba/analytics@^workspace:packages/analytics": +"@bubba/analytics@packages/analytics": version "workspace:packages/analytics" resolved "workspace:packages/analytics" dependencies: posthog-js "^1.215.3" posthog-node "^4.4.1" -"@bubba/app@^workspace:apps/app", "@bubba/app@workspace:*": +"@bubba/app@apps/app", "@bubba/app@workspace:*": version "workspace:apps/app" resolved "workspace:apps/app" devDependencies: @@ -1321,11 +1345,11 @@ yarn "^1.22.22" zustand "^5.0.3" -"@bubba/data@^workspace:packages/data": +"@bubba/data@packages/data": version "workspace:packages/data" resolved "workspace:packages/data" -"@bubba/db@^workspace:packages/db", "@bubba/db@workspace:*": +"@bubba/db@packages/db", "@bubba/db@workspace:*": version "workspace:packages/db" resolved "workspace:packages/db" devDependencies: @@ -1340,7 +1364,7 @@ "@prisma/extension-accelerate" "^1.2.2" prisma "^6.5.0" -"@bubba/email@^workspace:packages/email": +"@bubba/email@packages/email": version "workspace:packages/email" resolved "workspace:packages/email" devDependencies: @@ -1360,7 +1384,7 @@ react-email "3.0.4" responsive-react-email "^0.0.5" -"@bubba/integrations@^workspace:packages/integrations": +"@bubba/integrations@packages/integrations": version "workspace:packages/integrations" resolved "workspace:packages/integrations" devDependencies: @@ -1383,14 +1407,14 @@ sharp "^0.33.5" zod "^3.24.2" -"@bubba/kv@^workspace:packages/kv": +"@bubba/kv@packages/kv": version "workspace:packages/kv" resolved "workspace:packages/kv" dependencies: "@upstash/redis" "^1.34.2" server-only "0.0.1" -"@bubba/notifications@^workspace:packages/notifications", "@bubba/notifications@workspace:*": +"@bubba/notifications@packages/notifications", "@bubba/notifications@workspace:*": version "workspace:packages/notifications" resolved "workspace:packages/notifications" devDependencies: @@ -1399,11 +1423,11 @@ "@novu/node" "^2.0.1" nanoid "5.1.0" -"@bubba/tsconfig@^workspace:packages/tsconfig", "@bubba/tsconfig@workspace:*": +"@bubba/tsconfig@packages/tsconfig", "@bubba/tsconfig@workspace:*": version "workspace:packages/tsconfig" resolved "workspace:packages/tsconfig" -"@bubba/ui@^workspace:packages/ui", "@bubba/ui@workspace:*": +"@bubba/ui@packages/ui", "@bubba/ui@workspace:*": version "workspace:packages/ui" resolved "workspace:packages/ui" devDependencies: @@ -1467,7 +1491,7 @@ use-callback-ref "^1.3.3" vaul "^1.0.0" -"@bubba/utils@^workspace:packages/utils", "@bubba/utils@workspace:*": +"@bubba/utils@packages/utils", "@bubba/utils@workspace:*": version "workspace:packages/utils" resolved "workspace:packages/utils" devDependencies: @@ -8190,6 +8214,15 @@ framer-motion@^12.5.0: motion-utils "^12.5.0" tslib "^2.4.0" +framer-motion@^12.6.2: + version "12.6.2" + resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.2.tgz" + integrity sha512-7LgPRlPs5aG8UxeZiMCMZz8firC53+2+9TnWV22tuSi38D3IFRxHRUqOREKckAkt6ztX+Dn6weLcatQilJTMcg== + dependencies: + tslib "^2.4.0" + motion-dom "^12.6.1" + motion-utils "^12.5.0" + framework-utils@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/framework-utils/-/framework-utils-1.1.0.tgz" @@ -10471,6 +10504,14 @@ module-details-from-path@^1.0.3: resolved "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz" integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== +motion@^12.6.2: + version "12.6.2" + resolved "https://registry.npmjs.org/motion/-/motion-12.6.2.tgz" + integrity sha512-8OBjjuC59WuWHKmPzVWT5M0t5kDxtkfMfHF1M7Iey6F/nvd0AI15YlPnpGlcagW/eOfkdWDO90U/K5LF/k55Yw== + dependencies: + tslib "^2.4.0" + framer-motion "^12.6.2" + motion-dom@^12.5.0: version "12.5.0" resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.5.0.tgz" @@ -10478,6 +10519,13 @@ motion-dom@^12.5.0: dependencies: motion-utils "^12.5.0" +motion-dom@^12.6.1: + version "12.6.1" + resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.1.tgz" + integrity sha512-8XVsriTUEVOepoIDgE/LDGdg7qaKXWdt+wQA/8z0p8YzJDLYL8gbimZ3YkCLlj7bB2i/4UBD/g+VO7y9ZY0zHQ== + dependencies: + motion-utils "^12.5.0" + motion-utils@^12.5.0: version "12.5.0" resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-12.5.0.tgz" @@ -11326,7 +11374,7 @@ plist@^3.1.0: base64-js "^1.5.1" xmlbuilder "^15.1.1" -portal@^workspace:apps/portal: +"portal@apps/portal": version "workspace:apps/portal" resolved "workspace:apps/portal" devDependencies: @@ -14432,7 +14480,7 @@ zod@3.23.8: resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== -"zod@>= 3", zod@^3.0.0, zod@^3.18.0, zod@^3.20.2, zod@^3.24.0, zod@^3.24.1, zod@^3.24.2: +"zod@>= 3", zod@^3.0.0, zod@^3.18.0, zod@^3.20.2, zod@^3.23.8, zod@^3.24.0, zod@^3.24.1, zod@^3.24.2: version "3.24.2" resolved "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz" integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==