import { ArrowPathIcon, ArrowUpIcon, HandThumbDownIcon, HandThumbUpIcon, StopIcon, } from "@heroicons/react/20/solid"; import { cn } from "~/utils/cn"; import { type FeedbackComment, KapaProvider, type QA, useChat } from "@kapaai/react-sdk"; import { useSearchParams } from "@remix-run/react"; import DOMPurify from "dompurify"; import { motion } from "framer-motion"; import { marked } from "marked"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTypedRouteLoaderData } from "remix-typedjson"; import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; import { SparkleListIcon } from "~/assets/icons/SparkleListIcon"; import { useFeatures } from "~/hooks/useFeatures"; import { type loader } from "~/root"; import { Button } from "./primitives/Buttons"; import { Callout } from "./primitives/Callout"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog"; import { Header2 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; import { ShortcutKey } from "./primitives/ShortcutKey"; import { Spinner } from "./primitives/Spinner"; import { SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./primitives/Tooltip"; import { ClientOnly } from "remix-utils/client-only"; function useKapaWebsiteId() { const routeMatch = useTypedRouteLoaderData("root"); return routeMatch?.kapa.websiteId; } export function AskAI({ isCollapsed = false }: { isCollapsed?: boolean }) { const { isManagedCloud } = useFeatures(); const websiteId = useKapaWebsiteId(); if (!isManagedCloud || !websiteId) { return null; } return ( } > {() => } ); } type AskAIProviderProps = { websiteId: string; isCollapsed?: boolean; }; function AskAIProvider({ websiteId, isCollapsed = false }: AskAIProviderProps) { const [isOpen, setIsOpen] = useState(false); const [initialQuery, setInitialQuery] = useState(); const [searchParams, setSearchParams] = useSearchParams(); const openAskAI = useCallback((question?: string) => { if (question) { setInitialQuery(question); } else { setInitialQuery(undefined); } setIsOpen(true); }, []); const closeAskAI = useCallback(() => { setIsOpen(false); setInitialQuery(undefined); }, []); // Handle URL param functionality useEffect(() => { const aiHelp = searchParams.get("aiHelp"); if (aiHelp) { // Delay to avoid hCaptcha bot detection window.setTimeout(() => openAskAI(aiHelp), 1000); // Clone instead of mutating in place const next = new URLSearchParams(searchParams); next.delete("aiHelp"); setSearchParams(next); } }, [searchParams, openAskAI]); return ( openAskAI(), onAnswerGenerationCompleted: () => openAskAI(), }, }} botProtectionMechanism="hcaptcha" > Ask AI ); } type AskAIDialogProps = { initialQuery?: string; isOpen: boolean; onOpenChange: (open: boolean) => void; closeAskAI: () => void; }; function AskAIDialog({ initialQuery, isOpen, onOpenChange, closeAskAI }: AskAIDialogProps) { const handleOpenChange = (open: boolean) => { if (!open) { closeAskAI(); } else { onOpenChange(open); } }; return (
Ask AI
); } function ChatMessages({ conversation, isPreparingAnswer, isGeneratingAnswer, onReset, onExampleClick, error, addFeedback, }: { conversation: QA[]; isPreparingAnswer: boolean; isGeneratingAnswer: boolean; onReset: () => void; onExampleClick: (question: string) => void; error: string | null; addFeedback: ( questionAnswerId: string, reaction: "upvote" | "downvote", comment?: FeedbackComment ) => void; }) { const [feedbackGivenForQAs, setFeedbackGivenForQAs] = useState>(new Set()); // Reset feedback state when conversation is reset useEffect(() => { if (conversation.length === 0) { setFeedbackGivenForQAs(new Set()); } }, [conversation.length]); // Check if feedback has been given for the latest QA const latestQA = conversation[conversation.length - 1]; const hasFeedbackForLatestQA = latestQA?.id ? feedbackGivenForQAs.has(latestQA.id) : false; const exampleQuestions = [ "How do I increase my concurrency limit?", "How do I debug errors in my task?", "How do I deploy my task?", ]; return (
{conversation.length === 0 ? ( I'm trained on docs, examples, and other content. Ask me anything about Trigger.dev. {exampleQuestions.map((question, index) => ( onExampleClick(question)} variants={{ hidden: { opacity: 0, x: 20, }, visible: { opacity: 1, x: 0, transition: { opacity: { duration: 0.5, ease: "linear", }, x: { type: "spring", stiffness: 300, damping: 25, }, }, }, }} > {question} ))} ) : ( conversation.map((qa) => (
{qa.question}
)) )} {conversation.length > 0 && !isPreparingAnswer && !isGeneratingAnswer && !error && !latestQA?.id && (
Answer generation was stopped
)} {conversation.length > 0 && !isPreparingAnswer && !isGeneratingAnswer && !error && latestQA?.id && (
{hasFeedbackForLatestQA ? ( Thanks for your feedback! ) : (
Was this helpful?
)}
)} {isPreparingAnswer && (
Preparing answer…
)} {error && (
Error generating answer: {error} If the problem persists after retrying, please contact support.
)}
); } function ChatInterface({ initialQuery }: { initialQuery?: string }) { const [message, setMessage] = useState(""); const [isExpanded, setIsExpanded] = useState(false); const hasSubmittedInitialQuery = useRef(false); const { conversation, submitQuery, isGeneratingAnswer, isPreparingAnswer, resetConversation, stopGeneration, error, addFeedback, } = useChat(); useEffect(() => { if (initialQuery && !hasSubmittedInitialQuery.current) { hasSubmittedInitialQuery.current = true; setIsExpanded(true); submitQuery(initialQuery); } }, [initialQuery, submitQuery]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (message.trim()) { setIsExpanded(true); submitQuery(message); setMessage(""); } }; const handleExampleClick = (question: string) => { setIsExpanded(true); submitQuery(question); }; const handleReset = () => { resetConversation(); setIsExpanded(false); }; return (
setMessage(e.target.value)} placeholder="Ask a question..." disabled={isGeneratingAnswer} autoFocus className="flex-1 rounded-md border border-grid-bright bg-background-dimmed px-3 py-2 text-text-bright placeholder:text-text-dimmed focus-visible:focus-custom" /> {isGeneratingAnswer ? ( stopGeneration()} className="group relative z-10 flex size-10 min-w-10 cursor-pointer items-center justify-center" > } content="Stop generating" /> ) : isPreparingAnswer ? ( ) : (
); } function GradientSpinnerBackground({ children, className, hoverEffect = false, }: { children?: React.ReactNode; className?: string; hoverEffect?: boolean; }) { return (
{children}
); }