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 && (
+
)}
-
- )}
-
-
-
-
-
-
-
-
+ ) : (
+
+
+
+
+
+ What do you want to know today?
+
+
+ Understand and secure your cloud.
+
+
+
+
+ lastSubmittedText
+ ? void submitMessage(lastSubmittedText)
+ : undefined
+ }
+ />
+
+
+
+
+ Try Lighthouse for...
+
+ {LIGHTHOUSE_V2_SUGGESTIONS.map((suggestion) => {
+ const Icon = suggestion.icon;
+ return (
+
+ );
+ })}
+
+
+
+
+ )}
+
+ );
+}
+
+interface LighthouseV2FeedbackProps {
+ feedback: string | null;
+ canRetry: boolean;
+ onRetry: () => void;
+}
+
+function LighthouseV2Feedback({
+ feedback,
+ canRetry,
+ onRetry,
+}: LighthouseV2FeedbackProps) {
+ if (!feedback) return null;
+
+ return (
+
+ {feedback}
+ {canRetry && (
+
+ )}
);
}
+interface LighthouseV2ComposerProps {
+ canSend: boolean;
+ configurations: LighthouseV2Configuration[];
+ input: string;
+ isStreaming: boolean;
+ models: LighthouseV2SupportedModel[];
+ selectedConfigurationConnected: boolean;
+ selectedModel: string;
+ selectedProvider: LighthouseV2ProviderType;
+ onInputChange: (value: string) => void;
+ onModelChange: (value: string) => void;
+ onProviderChange: (provider: LighthouseV2ProviderType) => void;
+ onStop: () => void;
+ onSubmit: (event: FormEvent
) => void;
+ onSubmitText: (text: string) => Promise;
+}
+
+function LighthouseV2Composer({
+ canSend,
+ configurations,
+ input,
+ isStreaming,
+ models,
+ selectedConfigurationConnected,
+ selectedModel,
+ selectedProvider,
+ onInputChange,
+ onModelChange,
+ onProviderChange,
+ onStop,
+ onSubmit,
+ onSubmitText,
+}: LighthouseV2ComposerProps) {
+ return (
+
+ );
+}
+
function MessageBubble({ message }: { message: LighthouseV2Message }) {
const isUser = message.role === LIGHTHOUSE_V2_MESSAGE_ROLE.USER;
return (
diff --git a/ui/components/lighthouse-v2/history/lighthouse-v2-session-history.tsx b/ui/components/lighthouse-v2/history/lighthouse-v2-session-history.tsx
index 7c7d1cc5f8..70b3ba62c1 100644
--- a/ui/components/lighthouse-v2/history/lighthouse-v2-session-history.tsx
+++ b/ui/components/lighthouse-v2/history/lighthouse-v2-session-history.tsx
@@ -1,12 +1,20 @@
"use client";
-import { Archive, MessageSquare, Plus } from "lucide-react";
+import { Archive, Plus } from "lucide-react";
import { Button } from "@/components/shadcn/button/button";
import { SearchInput } from "@/components/shadcn/search-input/search-input";
import { cn } from "@/lib/utils";
import type { LighthouseV2Session } from "@/types/lighthouse-v2";
+const SESSION_GROUP_ORDER = [
+ "Today",
+ "Yesterday",
+ "Last 7 days",
+ "Last 30 days",
+ "Older",
+] as const;
+
interface LighthouseV2SessionHistoryProps {
sessions: LighthouseV2Session[];
activeSessionId?: string | null;
@@ -36,7 +44,7 @@ export function LighthouseV2SessionHistory({
onSearchChange(event.target.value)}
onClear={() => onSearchChange("")}
@@ -60,7 +68,7 @@ export function LighthouseV2SessionHistory({
{groups.map((group) => (
-
+
{group.label}
{group.sessions.map((session) => (
@@ -77,10 +85,12 @@ export function LighthouseV2SessionHistory({
className="hover:bg-bg-neutral-tertiary flex min-w-0 flex-1 items-center gap-2 rounded-[8px] px-2 py-2 text-left text-sm"
onClick={() => onOpenSession(session.id)}
>
-
-
+
{session.title || "Untitled chat"}
+
+ {formatAgeLabel(session.updatedAt)}
+