diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx index f72bc8195d..2860776ca1 100644 --- a/ui/app/(prowler)/lighthouse/page.tsx +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -7,12 +7,12 @@ import { import { getLighthouseV2Configurations, getLighthouseV2Messages, - getLighthouseV2Sessions, getLighthouseV2SupportedModels, } from "@/actions/lighthouse-v2/lighthouse-v2"; import { LighthouseIcon } from "@/components/icons/Icons"; import { Chat } from "@/components/lighthouse-v1"; import { LighthouseV2ChatPage } from "@/components/lighthouse-v2/chat"; +import { LighthouseV2NavigationModeSync } from "@/components/lighthouse-v2/navigation"; import { ContentLayout } from "@/components/ui"; import { isCloud } from "@/lib/shared/env"; import type { @@ -34,11 +34,7 @@ export default async function AIChatbot({ typeof params.session === "string" ? params.session : undefined; if (isCloud()) { - const [configurationsResult, sessionsResult] = await Promise.all([ - getLighthouseV2Configurations(), - getLighthouseV2Sessions(), - ]); - + const configurationsResult = await getLighthouseV2Configurations(); const configurations = "data" in configurationsResult ? configurationsResult.data : []; const connectedConfigurations = configurations.filter( @@ -67,26 +63,23 @@ export default async function AIChatbot({ LighthouseV2ProviderType, LighthouseV2SupportedModel[] >; - const initialMessages = - activeSessionId && "data" in sessionsResult - ? await getLighthouseV2Messages(activeSessionId) - : { data: [] }; + const initialMessages = activeSessionId + ? await getLighthouseV2Messages(activeSessionId) + : { data: [] }; return ( - }> -
- -
-
+
+ + +
); } diff --git a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx index 3716e8152d..1903ccfa59 100644 --- a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx +++ b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx @@ -1,23 +1,33 @@ "use client"; -import { Bot, Loader2, Send, Square, UserRound } from "lucide-react"; +import { + ArrowRight, + BookOpen, + Bot, + FileCheck2, + Loader2, + Network, + Settings, + ShieldAlert, + Square, + UserRound, +} from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { type FormEvent, useRef, useState } from "react"; import { - archiveLighthouseV2Session, cancelLighthouseV2Run, createLighthouseV2Session, getLighthouseV2Messages, - getLighthouseV2Sessions, sendLighthouseV2Message, } from "@/actions/lighthouse-v2/lighthouse-v2"; import { Conversation, ConversationContent, - ConversationEmptyState, ConversationScrollButton, } from "@/components/ai-elements/conversation"; +import { LighthouseIcon } from "@/components/icons/Icons"; import { Button } from "@/components/shadcn/button/button"; import { Select, @@ -33,6 +43,7 @@ import { type LighthouseV2StreamState, reduceLighthouseV2Event, } from "@/lib/lighthouse-v2/event-reducer"; +import { notifyLighthouseV2SessionsChanged } from "@/lib/lighthouse-v2/session-events"; import { cn } from "@/lib/utils"; import { LIGHTHOUSE_V2_MESSAGE_ROLE, @@ -42,34 +53,50 @@ import { type LighthouseV2Configuration, type LighthouseV2Message, type LighthouseV2ProviderType, - type LighthouseV2Session, type LighthouseV2SSEEvent, type LighthouseV2SupportedModel, } from "@/types/lighthouse-v2"; -import { LighthouseV2SessionHistory } from "../history"; - interface LighthouseV2ChatPageProps { configurations: LighthouseV2Configuration[]; modelsByProvider: Record< LighthouseV2ProviderType, LighthouseV2SupportedModel[] >; - sessions: LighthouseV2Session[]; initialSessionId?: string; initialMessages: LighthouseV2Message[]; initialPrompt?: string; - showHistory?: boolean; } +const LIGHTHOUSE_V2_SUGGESTIONS = [ + { + label: "Critical findings", + prompt: "Summarize my most critical open findings and what to fix first.", + icon: ShieldAlert, + }, + { + label: "Compliance gaps", + prompt: "What are my highest-impact compliance gaps right now?", + icon: FileCheck2, + }, + { + label: "Attack paths", + prompt: "Find risky attack paths and explain the exposure.", + icon: Network, + }, + { + label: "Docs", + prompt: "Point me to the relevant Prowler documentation for this task.", + icon: BookOpen, + }, +] as const; + export function LighthouseV2ChatPage({ configurations, modelsByProvider, - sessions, initialSessionId, initialMessages, initialPrompt, - showHistory = true, }: LighthouseV2ChatPageProps) { const router = useRouter(); const eventSourceRef = useRef(null); @@ -88,13 +115,11 @@ export function LighthouseV2ChatPage({ modelsByProvider[initialProvider]?.[0]?.id ?? "", ); - const [localSessions, setLocalSessions] = useState(sessions); const [activeSessionId, setActiveSessionId] = useState( initialSessionId ?? null, ); const [messages, setMessages] = useState(initialMessages); const [input, setInput] = useState(""); - const [search, setSearch] = useState(""); const [feedback, setFeedback] = useState(null); const [blockedByConflict, setBlockedByConflict] = useState(false); const [lastSubmittedText, setLastSubmittedText] = useState( @@ -130,15 +155,6 @@ export function LighthouseV2ChatPage({ } }; - const refreshSessions = async (nextSearch = search) => { - const result = await getLighthouseV2Sessions( - nextSearch ? { search: nextSearch } : undefined, - ); - if ("data" in result) { - setLocalSessions(result.data); - } - }; - const closeStream = () => { eventSourceRef.current?.close(); eventSourceRef.current = null; @@ -156,7 +172,7 @@ export function LighthouseV2ChatPage({ closeStream(); setBlockedByConflict(false); await refreshMessages(sessionId); - await refreshSessions(); + notifyLighthouseV2SessionsChanged(); } }; @@ -221,7 +237,7 @@ export function LighthouseV2ChatPage({ } setActiveSessionId(result.data.id); - setLocalSessions((current) => [result.data, ...current]); + notifyLighthouseV2SessionsChanged(); router.push(`/lighthouse?session=${encodeURIComponent(result.data.id)}`); return result.data.id; }; @@ -262,7 +278,7 @@ export function LighthouseV2ChatPage({ if (result.data.streamUrl) { startStream(result.data.streamUrl, sessionId); } - await refreshSessions(); + notifyLighthouseV2SessionsChanged(); }; const handleSubmit = (event: FormEvent) => { @@ -283,51 +299,12 @@ export function LighthouseV2ChatPage({ ); setBlockedByConflict(false); await refreshMessages(activeSessionId); + notifyLighthouseV2SessionsChanged(); if ("error" in result) { setFeedback(result.error); } }; - const handleOpenSession = async (sessionId: string) => { - closeStream(); - setActiveSessionId(sessionId); - setStreamState(createInitialLighthouseV2StreamState()); - setBlockedByConflict(false); - setFeedback(null); - router.push(`/lighthouse?session=${encodeURIComponent(sessionId)}`); - await refreshMessages(sessionId); - }; - - const handleNewSession = () => { - closeStream(); - setActiveSessionId(null); - setMessages([]); - setInput(""); - setFeedback(null); - setBlockedByConflict(false); - setStreamState(createInitialLighthouseV2StreamState()); - router.push("/lighthouse"); - }; - - const handleArchiveSession = async (sessionId: string) => { - const result = await archiveLighthouseV2Session(sessionId); - if ("error" in result) { - setFeedback(result.error); - return; - } - setLocalSessions((current) => - current.filter((session) => session.id !== sessionId), - ); - if (sessionId === activeSessionId) { - handleNewSession(); - } - }; - - const handleSearchChange = (value: string) => { - setSearch(value); - void refreshSessions(value); - }; - useMountEffect(() => { if (initialPrompt && !initialPromptSentRef.current) { initialPromptSentRef.current = true; @@ -335,129 +312,285 @@ export function LighthouseV2ChatPage({ } }); + const hasConversation = + messages.length > 0 || Boolean(streamState.assistantText); + return ( -
- {showHistory && ( - - )} - -
- - - {messages.length === 0 && !streamState.assistantText ? ( - - ) : ( - <> - {messages.map((message) => ( - - ))} - {streamState.assistantText && ( - - )} - - )} - - - - -
- {feedback && ( -
- {feedback} - {streamState.status === "disconnected" && lastSubmittedText && ( - +
+ {hasConversation ? ( +
+ + + {messages.map((message) => ( + + ))} + {streamState.assistantText && ( + )} -
- )} - -
- - -
- -
-