diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 16ad54c082..69ef633968 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to the **Prowler UI** are documented in this file. +## [1.10.0] (Prowler v5.10.0 - UNRELEASED) + +### Added + +- Lighthouse banner [(#8259)](https://github.com/prowler-cloud/prowler/pull/8259) +- Integration with Amazon S3, enabling storage and retrieval of scan data via S3 buckets [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056) + +___ + +## [v1.9.3] (Prowler v5.9.3) + +### 🐞 Fixed + +- Display error messages and allow editing last message in Lighthouse [(#8358)](https://github.com/prowler-cloud/prowler/pull/8358) + ## [v1.9.0] (Prowler v5.9.0) ### 🚀 Added diff --git a/ui/app/api/lighthouse/analyst/route.ts b/ui/app/api/lighthouse/analyst/route.ts index 0d24b02e50..6bbf076f6e 100644 --- a/ui/app/api/lighthouse/analyst/route.ts +++ b/ui/app/api/lighthouse/analyst/route.ts @@ -1,6 +1,7 @@ import { LangChainAdapter, Message } from "ai"; import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse"; +import { getErrorMessage } from "@/lib/helper"; import { getCurrentDataSection } from "@/lib/lighthouse/data"; import { convertLangChainMessageToVercelMessage, @@ -73,22 +74,38 @@ export async function POST(req: Request) { const stream = new ReadableStream({ async start(controller) { - for await (const { event, data, tags } of agentStream) { - if (event === "on_chat_model_stream") { - if (data.chunk.content && !!tags && tags.includes("supervisor")) { - const chunk = data.chunk; - const aiMessage = convertLangChainMessageToVercelMessage(chunk); - controller.enqueue(aiMessage); + try { + for await (const { event, data, tags } of agentStream) { + if (event === "on_chat_model_stream") { + if (data.chunk.content && !!tags && tags.includes("supervisor")) { + const chunk = data.chunk; + const aiMessage = convertLangChainMessageToVercelMessage(chunk); + controller.enqueue(aiMessage); + } } } + controller.close(); + } catch (error) { + const errorName = + error instanceof Error ? error.constructor.name : "UnknownError"; + const errorMessage = + error instanceof Error ? error.message : String(error); + controller.enqueue({ + id: "error-" + Date.now(), + role: "assistant", + content: `[LIGHTHOUSE_ANALYST_ERROR]: ${errorName}: ${errorMessage}`, + }); + controller.close(); } - controller.close(); }, }); return LangChainAdapter.toDataStreamResponse(stream); } catch (error) { console.error("Error in POST request:", error); - return Response.json({ error: "An error occurred" }, { status: 500 }); + return Response.json( + { error: await getErrorMessage(error) }, + { status: 500 }, + ); } } diff --git a/ui/components/lighthouse/chat.tsx b/ui/components/lighthouse/chat.tsx index c31c6a08b9..6234c3a390 100644 --- a/ui/components/lighthouse/chat.tsx +++ b/ui/components/lighthouse/chat.tsx @@ -2,7 +2,7 @@ import { useChat } from "@ai-sdk/react"; import Link from "next/link"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown"; @@ -25,20 +25,43 @@ interface ChatFormData { } export const Chat = ({ hasConfig, isActive }: ChatProps) => { - const { messages, handleSubmit, handleInputChange, append, status } = useChat( - { - api: "/api/lighthouse/analyst", - credentials: "same-origin", - experimental_throttle: 100, - sendExtraMessageFields: true, - onFinish: () => { - // Handle chat completion - }, - onError: (error) => { - console.error("Chat error:", error); - }, + 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) => { + // Detect error messages sent from backend using specific prefix + 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); + setErrorMessage( + error?.message || "An error occurred. Please retry your message.", + ); + }, + }); const form = useForm({ defaultValues: { @@ -65,8 +88,29 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => { } }, [status, form]); + // Populate input with last user message when any error occurs + useEffect(() => { + if (errorMessage && messages.length > 0) { + // Filter out the error message itself before finding the last user message + const nonErrorMessages = messages.filter( + (m) => !m.content?.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:"), + ); + if (nonErrorMessages.length > 0) { + const lastUserMessage = nonErrorMessages + .filter((m) => m.role === "user") + .pop(); + if (lastUserMessage && !messageValue) { + form.setValue("message", lastUserMessage.content); + setMessages(nonErrorMessages.slice(0, -1)); + } + } + } + }, [errorMessage, messages, messageValue, form, setMessages]); + const onFormSubmit = form.handleSubmit((data) => { if (data.message.trim()) { + // Clear error on new submission + setErrorMessage(null); handleSubmit(); } }); @@ -148,7 +192,54 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => { )} - {messages.length === 0 ? ( + {/* 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

@@ -232,7 +323,11 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => { control={form.control} name="message" label="" - placeholder="Type your message..." + placeholder={ + error || errorMessage + ? "Edit your message and try again..." + : "Type your message..." + } variant="bordered" minRows={1} maxRows={6}