"use client"; import { useChat } from "@ai-sdk/react"; import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown"; import { CustomButton, CustomTextarea } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form } from "@/components/ui/form"; interface SuggestedAction { title: string; label: string; action: string; } interface ChatProps { hasConfig: boolean; isActive: boolean; } interface ChatFormData { message: string; } export const Chat = ({ hasConfig, isActive }: ChatProps) => { const [errorMessage, setErrorMessage] = useState(null); const { messages, handleSubmit, handleInputChange, append, status, error, setMessages, } = useChat({ api: "/api/lighthouse/analyst", credentials: "same-origin", experimental_throttle: 100, sendExtraMessageFields: true, onFinish: (message) => { // There is no specific way to output the error message from langgraph supervisor // Hence, all error messages are sent as normal messages with the prefix [LIGHTHOUSE_ANALYST_ERROR]: // Detect error messages sent from backend using specific prefix and display the error if (message.content?.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:")) { const errorText = message.content .replace("[LIGHTHOUSE_ANALYST_ERROR]:", "") .trim(); setErrorMessage(errorText); // Remove error message from chat history setMessages((prev) => prev.filter( (m) => !m.content?.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:"), ), ); } }, onError: (error) => { console.error("Chat error:", error); if ( error?.message?.includes("") && error?.message?.includes("403 Forbidden") ) { setErrorMessage("403 Forbidden"); return; } setErrorMessage( error?.message || "An error occurred. Please retry your message.", ); }, }); const form = useForm({ defaultValues: { message: "", }, }); const messageValue = form.watch("message"); const messagesContainerRef = useRef(null); const latestUserMsgRef = useRef(null); const messageValueRef = useRef(""); // Keep ref in sync with current value messageValueRef.current = messageValue; // Restore last user message to input when any error occurs useEffect(() => { if (errorMessage) { // Capture current messages to avoid dependency issues setMessages((currentMessages) => { const lastUserMessage = currentMessages .filter((m) => m.role === "user") .pop(); if (lastUserMessage) { form.setValue("message", lastUserMessage.content); // Remove the last user message from history since it's now in the input return currentMessages.slice(0, -1); } return currentMessages; }); } }, [errorMessage, form, setMessages]); // Sync form value with chat input useEffect(() => { const syntheticEvent = { target: { value: messageValue }, } as React.ChangeEvent; handleInputChange(syntheticEvent); }, [messageValue, handleInputChange]); // Reset form when message is sent useEffect(() => { if (status === "submitted") { form.reset({ message: "" }); } }, [status, form]); const onFormSubmit = form.handleSubmit((data) => { if (data.message.trim()) { // Clear error on new submission setErrorMessage(null); handleSubmit(); } }); // Global keyboard shortcut handler useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); if (messageValue?.trim()) { onFormSubmit(); } } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [messageValue, onFormSubmit]); useEffect(() => { if (messagesContainerRef.current && latestUserMsgRef.current) { const container = messagesContainerRef.current; const userMsg = latestUserMsgRef.current; const containerPadding = 16; // p-4 in Tailwind = 16px container.scrollTop = userMsg.offsetTop - container.offsetTop - containerPadding; } }, [messages]); const suggestedActions: SuggestedAction[] = [ { title: "Are there any exposed S3", label: "buckets in my AWS accounts?", action: "List exposed S3 buckets in my AWS accounts", }, { title: "What is the risk of having", label: "RDS databases unencrypted?", action: "What is the risk of having RDS databases unencrypted?", }, { title: "What is the CIS 1.10 compliance status", label: "of my Kubernetes cluster?", action: "What is the CIS 1.10 compliance status of my Kubernetes cluster?", }, { title: "List my highest privileged", label: "AWS IAM users with full admin access?", action: "List my highest privileged AWS IAM users with full admin access", }, ]; // Determine if chat should be disabled const shouldDisableChat = !hasConfig || !isActive; return (
{shouldDisableChat && (

{!hasConfig ? "OpenAI API Key Required" : "OpenAI API Key Invalid"}

{!hasConfig ? "Please configure your OpenAI API key to use Lighthouse AI." : "OpenAI API key is invalid. Please update your key to use Lighthouse AI."}

Configure API Key
)} {/* Error Banner */} {(error || errorMessage) && (

Error

{errorMessage || error?.message || "An error occurred. Please retry your message."}

{/* Original error details for native errors */} {error && (error as any).status && (

Status: {(error as any).status}

)} {error && (error as any).body && (
Show details
                    {JSON.stringify((error as any).body, null, 2)}
                  
)}
)} {messages.length === 0 && !errorMessage && !error ? (

Suggestions

{suggestedActions.map((action, index) => ( { append({ role: "user", content: action.action, }); }} className="hover:bg-muted flex h-auto w-full flex-col items-start justify-start rounded-xl border bg-gray-50 px-4 py-3.5 text-left font-sans text-sm dark:bg-gray-900" > {action.title} {action.label} ))}
) : (
{messages.map((message, idx) => { const lastUserIdx = messages .map((m, i) => (m.role === "user" ? i : -1)) .filter((i) => i !== -1) .pop(); const isLatestUserMsg = message.role === "user" && lastUserIdx === idx; return (
); })} {status === "submitted" && (
Thinking...
)}
)}
{status === "submitted" ? ■ : ➤}
); }; export default Chat;