mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): tune lighthouse chat layout
This commit is contained in:
@@ -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(
|
||||
'<ContentLayout title="Lighthouse AI" icon={<LighthouseIcon />}>',
|
||||
);
|
||||
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"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-dvh min-h-0">
|
||||
<ContentLayout title="Lighthouse AI" icon={<LighthouseIcon />}>
|
||||
<LighthouseV2NavigationModeSync />
|
||||
<LighthouseV2ChatPage
|
||||
key={chatRouteKey}
|
||||
configurations={configurations}
|
||||
modelsByProvider={modelsByProvider}
|
||||
initialSessionId={activeSessionId}
|
||||
initialMessages={
|
||||
"data" in initialMessages ? initialMessages.data : []
|
||||
}
|
||||
initialPrompt={initialPrompt}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[calc(100dvh-6.5rem)] min-h-0">
|
||||
<LighthouseV2ChatPage
|
||||
key={chatRouteKey}
|
||||
configurations={configurations}
|
||||
modelsByProvider={modelsByProvider}
|
||||
initialSessionId={activeSessionId}
|
||||
initialMessages={
|
||||
"data" in initialMessages ? initialMessages.data : []
|
||||
}
|
||||
initialPrompt={initialPrompt}
|
||||
/>
|
||||
</div>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
|
||||
@@ -19,19 +19,39 @@ export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export type ConversationContentProps = ComponentProps<
|
||||
typeof StickToBottom.Content
|
||||
>;
|
||||
type ConversationContentChildren =
|
||||
| ReactNode
|
||||
| ((context: ReturnType<typeof useStickToBottomContext>) => ReactNode);
|
||||
|
||||
export type ConversationContentProps = Omit<
|
||||
ComponentProps<"div">,
|
||||
"children" | "ref"
|
||||
> & {
|
||||
children?: ConversationContentChildren;
|
||||
scrollClassName?: string;
|
||||
};
|
||||
|
||||
export const ConversationContent = ({
|
||||
children,
|
||||
className,
|
||||
scrollClassName,
|
||||
...props
|
||||
}: ConversationContentProps) => (
|
||||
<StickToBottom.Content
|
||||
className={cn("flex flex-col gap-8 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: ConversationContentProps) => {
|
||||
const context = useStickToBottomContext();
|
||||
const { contentRef, scrollRef } = context;
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className={cn("h-full w-full", scrollClassName)}>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn("flex flex-col gap-8 p-4", className)}
|
||||
{...props}
|
||||
>
|
||||
{typeof children === "function" ? children(context) : children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
|
||||
title?: string;
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<section className="bg-bg-neutral-primary flex h-full min-h-0 flex-col">
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden"
|
||||
>
|
||||
{hasConversation ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<Conversation className="min-h-0">
|
||||
<ConversationContent className="mx-auto w-full max-w-4xl gap-5 px-4 py-8 md:px-8">
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} />
|
||||
))}
|
||||
{streamState.assistantText && (
|
||||
<StreamingAssistantMessage streamState={streamState} />
|
||||
)}
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
<div className="bg-bg-neutral-primary px-4 pb-5 md:px-8">
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Conversation className="h-full min-h-0">
|
||||
<ConversationContent
|
||||
className="mx-auto w-full max-w-4xl gap-5 px-4 pt-8 pb-20 md:px-8"
|
||||
scrollClassName="minimal-scrollbar overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} />
|
||||
))}
|
||||
{streamState.assistantText && (
|
||||
<StreamingAssistantMessage streamState={streamState} />
|
||||
)}
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton className="z-20" />
|
||||
</Conversation>
|
||||
<div
|
||||
data-slot="lighthouse-v2-chat-scroll-fade"
|
||||
className="from-bg-neutral-secondary pointer-events-none absolute right-2 bottom-0 left-0 z-10 h-16 bg-gradient-to-t to-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-slot="lighthouse-v2-chat-composer-panel"
|
||||
className="bg-bg-neutral-secondary px-4 pb-5 md:px-8"
|
||||
>
|
||||
<div className="mx-auto w-full max-w-4xl">
|
||||
<LighthouseV2Feedback
|
||||
feedback={feedback}
|
||||
@@ -397,7 +414,7 @@ export function LighthouseV2ChatPage({
|
||||
})}
|
||||
<Button type="button" variant="outline" size="icon-sm" asChild>
|
||||
<Link
|
||||
href="/lighthouse/config"
|
||||
href="/lighthouse/settings"
|
||||
aria-label="Lighthouse settings"
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
@@ -407,7 +424,7 @@ export function LighthouseV2ChatPage({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user