fix(ui): tune lighthouse chat layout

This commit is contained in:
alejandrobailo
2026-06-25 16:04:13 +02:00
parent 7cb192f69b
commit bb7eb06746
5 changed files with 152 additions and 49 deletions
+9
View File
@@ -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"');
});
});
+17 -15
View File
@@ -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 (
+29 -9
View File
@@ -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>
);
}