From bb7eb06746ec249602f608998311681eb0d6f275 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Thu, 25 Jun 2026 16:04:13 +0200 Subject: [PATCH] fix(ui): tune lighthouse chat layout --- ui/app/(prowler)/lighthouse/page.test.ts | 9 +++ ui/app/(prowler)/lighthouse/page.tsx | 32 ++++---- ui/components/ai-elements/conversation.tsx | 38 +++++++--- .../chat/lighthouse-v2-chat-page.test.tsx | 75 ++++++++++++++++--- .../chat/lighthouse-v2-chat-page.tsx | 47 ++++++++---- 5 files changed, 152 insertions(+), 49 deletions(-) diff --git a/ui/app/(prowler)/lighthouse/page.test.ts b/ui/app/(prowler)/lighthouse/page.test.ts index e7260e2506..8283b20551 100644 --- a/ui/app/(prowler)/lighthouse/page.test.ts +++ b/ui/app/(prowler)/lighthouse/page.test.ts @@ -16,4 +16,13 @@ describe("Lighthouse page", () => { ); expect(source).toContain("key={chatRouteKey}"); }); + + it("renders the Cloud chat inside the shared application layout", () => { + // Given / When / Then + expect(source).toContain( + '}>', + ); + expect(source).toContain('className="h-[calc(100dvh-6.5rem)] min-h-0"'); + expect(source).not.toContain('className="h-dvh min-h-0 p-4 pr-6"'); + }); }); diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx index 396b9c243f..a5b8e4d05a 100644 --- a/ui/app/(prowler)/lighthouse/page.tsx +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -42,7 +42,7 @@ export default async function AIChatbot({ ); if (connectedConfigurations.length === 0) { - return redirect("/lighthouse/config"); + return redirect("/lighthouse/settings"); } const modelsEntries = await Promise.all( @@ -69,26 +69,28 @@ export default async function AIChatbot({ const chatRouteKey = activeSessionId ?? initialPrompt ?? "new"; return ( -
+ }> - -
+
+ +
+
); } const hasConfig = await isLighthouseConfigured(); if (!hasConfig) { - return redirect("/lighthouse/config"); + return redirect("/lighthouse/settings"); } // Fetch provider configuration with default models @@ -96,7 +98,7 @@ export default async function AIChatbot({ // Handle errors or missing configuration if (providersConfig.errors || !providersConfig.providers) { - return redirect("/lighthouse/config"); + return redirect("/lighthouse/settings"); } return ( diff --git a/ui/components/ai-elements/conversation.tsx b/ui/components/ai-elements/conversation.tsx index 44916f06d1..ec96807893 100644 --- a/ui/components/ai-elements/conversation.tsx +++ b/ui/components/ai-elements/conversation.tsx @@ -19,19 +19,39 @@ export const Conversation = ({ className, ...props }: ConversationProps) => ( /> ); -export type ConversationContentProps = ComponentProps< - typeof StickToBottom.Content ->; +type ConversationContentChildren = + | ReactNode + | ((context: ReturnType) => ReactNode); + +export type ConversationContentProps = Omit< + ComponentProps<"div">, + "children" | "ref" +> & { + children?: ConversationContentChildren; + scrollClassName?: string; +}; export const ConversationContent = ({ + children, className, + scrollClassName, ...props -}: ConversationContentProps) => ( - -); +}: ConversationContentProps) => { + const context = useStickToBottomContext(); + const { contentRef, scrollRef } = context; + + return ( +
+
+ {typeof children === "function" ? children(context) : children} +
+
+ ); +}; export type ConversationEmptyStateProps = ComponentProps<"div"> & { title?: string; diff --git a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.test.tsx b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.test.tsx index f0e2d76bd3..9318ce7305 100644 --- a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.test.tsx +++ b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.test.tsx @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { LighthouseV2Configuration, + LighthouseV2Message, LighthouseV2SupportedModel, } from "@/types/lighthouse-v2"; @@ -109,15 +110,6 @@ describe("LighthouseV2ChatPage", () => { vi.unstubAllGlobals(); }); - it("uses the neutral page background instead of the global app background token", () => { - // Given / When - const { container } = renderPage(); - - // Then - expect(container.firstElementChild).toHaveClass("bg-bg-neutral-primary"); - expect(container.firstElementChild).not.toHaveClass("bg-background"); - }); - it("does not render provider or model selectors in the chat composer", () => { // Given / When renderPage(); @@ -126,7 +118,46 @@ describe("LighthouseV2ChatPage", () => { expect(screen.queryByRole("combobox")).not.toBeInTheDocument(); expect( screen.getByRole("link", { name: "Lighthouse settings" }), - ).toHaveAttribute("href", "/lighthouse/config"); + ).toHaveAttribute("href", "/lighthouse/settings"); + }); + + it("uses the tuned scrollbar and bottom fade without a composer separator", () => { + // Given / When + const { container } = renderPage({ + initialMessages: [message("message-1", "assistant", "Existing answer")], + }); + + // Then + const conversation = screen.getByRole("log"); + const scrollViewport = conversation.firstElementChild as HTMLElement; + const content = scrollViewport.firstElementChild as HTMLElement; + const scrollFade = container.querySelector( + '[data-slot="lighthouse-v2-chat-scroll-fade"]', + ); + + expect(conversation).toHaveClass("h-full", "min-h-0"); + expect(conversation.parentElement).toHaveClass("flex", "overflow-hidden"); + expect(scrollViewport).toHaveClass( + "minimal-scrollbar", + "overflow-x-hidden", + "overflow-y-auto", + ); + expect(content).toHaveClass("pb-20"); + expect(scrollFade).toHaveClass( + "pointer-events-none", + "absolute", + "bottom-0", + "right-2", + "h-16", + "bg-gradient-to-t", + "from-bg-neutral-secondary", + "to-transparent", + ); + expect( + container.querySelector( + '[data-slot="lighthouse-v2-chat-composer-panel"]', + ), + ).not.toHaveClass("border-t"); }); it("sends messages with the connected default provider and model from configuration", async () => { @@ -215,3 +246,27 @@ function model(id: string): LighthouseV2SupportedModel { supportsReasoning: null, }; } + +function message( + id: string, + role: LighthouseV2Message["role"], + content: string, +): LighthouseV2Message { + return { + id, + role, + model: null, + tokenUsage: null, + insertedAt: "2026-06-25T10:00:00Z", + parts: [ + { + id: `${id}-part`, + type: "text", + content, + toolCallOutcome: null, + insertedAt: "2026-06-25T10:00:00Z", + updatedAt: "2026-06-25T10:00:00Z", + }, + ], + }; +} 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 dca6f11f3b..f3677bb5c1 100644 --- a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx +++ b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx @@ -50,6 +50,8 @@ import { type LighthouseV2SupportedModel, } from "@/types/lighthouse-v2"; +import { Card } from "../../shadcn"; + interface LighthouseV2ChatPageProps { configurations: LighthouseV2Configuration[]; modelsByProvider: Record< @@ -295,21 +297,36 @@ export function LighthouseV2ChatPage({ messages.length > 0 || Boolean(streamState.assistantText); return ( -
+ {hasConversation ? (
- - - {messages.map((message) => ( - - ))} - {streamState.assistantText && ( - - )} - - - -
+
+ + + {messages.map((message) => ( + + ))} + {streamState.assistantText && ( + + )} + + + +
+
+
@@ -407,7 +424,7 @@ export function LighthouseV2ChatPage({
)} -
+ ); }