mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
fix(ui): simplify Lighthouse chat composer
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type {
|
||||
LighthouseV2Configuration,
|
||||
LighthouseV2SupportedModel,
|
||||
} from "@/types/lighthouse-v2";
|
||||
|
||||
import { LighthouseV2ChatPage } from "./lighthouse-v2-chat-page";
|
||||
|
||||
const {
|
||||
cancelRunMock,
|
||||
createSessionMock,
|
||||
getMessagesMock,
|
||||
pushMock,
|
||||
sendMessageMock,
|
||||
} = vi.hoisted(() => ({
|
||||
cancelRunMock: vi.fn(),
|
||||
createSessionMock: vi.fn(),
|
||||
getMessagesMock: vi.fn(),
|
||||
pushMock: vi.fn(),
|
||||
sendMessageMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: pushMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/actions/lighthouse-v2/lighthouse-v2", () => ({
|
||||
cancelLighthouseV2Run: cancelRunMock,
|
||||
createLighthouseV2Session: createSessionMock,
|
||||
getLighthouseV2Messages: getMessagesMock,
|
||||
sendLighthouseV2Message: sendMessageMock,
|
||||
}));
|
||||
|
||||
const configurations: LighthouseV2Configuration[] = [
|
||||
{
|
||||
id: "config-openai",
|
||||
providerType: "openai",
|
||||
baseUrl: null,
|
||||
defaultModel: "gpt-5.1",
|
||||
businessContext: "Production account",
|
||||
connected: true,
|
||||
connectionLastCheckedAt: "2026-06-24T10:00:00Z",
|
||||
insertedAt: "2026-06-24T09:00:00Z",
|
||||
updatedAt: "2026-06-24T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "config-bedrock",
|
||||
providerType: "bedrock",
|
||||
baseUrl: null,
|
||||
defaultModel: "anthropic.claude-4",
|
||||
businessContext: "AWS landing zone",
|
||||
connected: true,
|
||||
connectionLastCheckedAt: "2026-06-23T10:00:00Z",
|
||||
insertedAt: "2026-06-23T09:00:00Z",
|
||||
updatedAt: "2026-06-23T10:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
const modelsByProvider = {
|
||||
openai: [model("gpt-5.1")],
|
||||
bedrock: [model("anthropic.claude-4")],
|
||||
"openai-compatible": [model("llama-3.3")],
|
||||
};
|
||||
|
||||
describe("LighthouseV2ChatPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class ResizeObserver {
|
||||
observe = vi.fn();
|
||||
unobserve = vi.fn();
|
||||
disconnect = vi.fn();
|
||||
},
|
||||
);
|
||||
cancelRunMock.mockReset();
|
||||
createSessionMock.mockReset();
|
||||
getMessagesMock.mockReset();
|
||||
pushMock.mockReset();
|
||||
sendMessageMock.mockReset();
|
||||
|
||||
createSessionMock.mockResolvedValue({
|
||||
data: {
|
||||
id: "session-1",
|
||||
title: "Summarize findings",
|
||||
isArchived: false,
|
||||
insertedAt: "2026-06-24T10:00:00Z",
|
||||
updatedAt: "2026-06-24T10:00:00Z",
|
||||
activeTaskId: null,
|
||||
},
|
||||
});
|
||||
getMessagesMock.mockResolvedValue({ data: [] });
|
||||
sendMessageMock.mockResolvedValue({
|
||||
data: {
|
||||
task: {
|
||||
id: "task-1",
|
||||
name: "lighthouse-run",
|
||||
state: "executing",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// Then
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Lighthouse settings" }),
|
||||
).toHaveAttribute("href", "/lighthouse/config");
|
||||
});
|
||||
|
||||
it("sends messages with the connected default provider and model from configuration", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
// When
|
||||
await user.type(
|
||||
screen.getByRole("textbox", { name: "Message" }),
|
||||
["Summarize findings", "{Enter}"].join(""),
|
||||
);
|
||||
|
||||
// Then
|
||||
await waitFor(() =>
|
||||
expect(sendMessageMock).toHaveBeenCalledWith({
|
||||
sessionId: "session-1",
|
||||
text: "Summarize findings",
|
||||
provider: "openai",
|
||||
model: "gpt-5.1",
|
||||
}),
|
||||
);
|
||||
expect(createSessionMock).toHaveBeenCalledWith("Summarize findings");
|
||||
expect(pushMock).toHaveBeenCalledWith("/lighthouse?session=session-1");
|
||||
});
|
||||
});
|
||||
|
||||
function renderPage(
|
||||
props?: Partial<Parameters<typeof LighthouseV2ChatPage>[0]>,
|
||||
) {
|
||||
return render(
|
||||
<LighthouseV2ChatPage
|
||||
configurations={props?.configurations ?? configurations}
|
||||
modelsByProvider={props?.modelsByProvider ?? modelsByProvider}
|
||||
initialSessionId={props?.initialSessionId}
|
||||
initialMessages={props?.initialMessages ?? []}
|
||||
initialPrompt={props?.initialPrompt}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
function model(id: string): LighthouseV2SupportedModel {
|
||||
return {
|
||||
id,
|
||||
maxInputTokens: null,
|
||||
maxOutputTokens: null,
|
||||
supportsFunctionCalling: null,
|
||||
supportsVision: null,
|
||||
supportsReasoning: null,
|
||||
};
|
||||
}
|
||||
@@ -29,13 +29,6 @@ import {
|
||||
} from "@/components/ai-elements/conversation";
|
||||
import { LighthouseIcon } from "@/components/icons/Icons";
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/shadcn/select/select";
|
||||
import { Textarea } from "@/components/shadcn/textarea/textarea";
|
||||
import { useMountEffect } from "@/hooks/use-mount-effect";
|
||||
import {
|
||||
@@ -104,17 +97,13 @@ export function LighthouseV2ChatPage({
|
||||
const connectedConfigurations = configurations.filter(
|
||||
(configuration) => configuration.connected === true,
|
||||
);
|
||||
const initialProvider =
|
||||
connectedConfigurations[0]?.providerType ??
|
||||
configurations[0]?.providerType ??
|
||||
LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI;
|
||||
const [selectedProvider, setSelectedProvider] =
|
||||
useState<LighthouseV2ProviderType>(initialProvider);
|
||||
const [selectedModel, setSelectedModel] = useState(
|
||||
connectedConfigurations[0]?.defaultModel ??
|
||||
modelsByProvider[initialProvider]?.[0]?.id ??
|
||||
"",
|
||||
);
|
||||
const selectedConfiguration = connectedConfigurations[0] ?? configurations[0];
|
||||
const selectedProvider =
|
||||
selectedConfiguration?.providerType ?? LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI;
|
||||
const selectedModel =
|
||||
selectedConfiguration?.defaultModel ??
|
||||
modelsByProvider[selectedProvider]?.[0]?.id ??
|
||||
"";
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(
|
||||
initialSessionId ?? null,
|
||||
);
|
||||
@@ -129,25 +118,11 @@ export function LighthouseV2ChatPage({
|
||||
createInitialLighthouseV2StreamState(),
|
||||
);
|
||||
|
||||
const selectedConfiguration = configurations.find(
|
||||
(configuration) => configuration.providerType === selectedProvider,
|
||||
);
|
||||
const providerModels = modelsByProvider[selectedProvider] ?? [];
|
||||
const canSend =
|
||||
selectedConfiguration?.connected === true &&
|
||||
!streamState.activeTaskId &&
|
||||
!blockedByConflict;
|
||||
|
||||
const handleProviderChange = (provider: LighthouseV2ProviderType) => {
|
||||
const nextConfig = configurations.find(
|
||||
(configuration) => configuration.providerType === provider,
|
||||
);
|
||||
setSelectedProvider(provider);
|
||||
setSelectedModel(
|
||||
nextConfig?.defaultModel ?? modelsByProvider[provider]?.[0]?.id ?? "",
|
||||
);
|
||||
};
|
||||
|
||||
const refreshMessages = async (sessionId: string) => {
|
||||
const result = await getLighthouseV2Messages(sessionId);
|
||||
if ("data" in result) {
|
||||
@@ -316,7 +291,7 @@ export function LighthouseV2ChatPage({
|
||||
messages.length > 0 || Boolean(streamState.assistantText);
|
||||
|
||||
return (
|
||||
<section className="bg-background flex h-full min-h-0 flex-col">
|
||||
<section className="bg-bg-neutral-primary flex h-full min-h-0 flex-col">
|
||||
{hasConversation ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<Conversation className="min-h-0">
|
||||
@@ -330,7 +305,7 @@ export function LighthouseV2ChatPage({
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
<div className="bg-background px-4 pb-5 md:px-8">
|
||||
<div className="bg-bg-neutral-primary px-4 pb-5 md:px-8">
|
||||
<div className="mx-auto w-full max-w-4xl">
|
||||
<LighthouseV2Feedback
|
||||
feedback={feedback}
|
||||
@@ -346,18 +321,12 @@ export function LighthouseV2ChatPage({
|
||||
/>
|
||||
<LighthouseV2Composer
|
||||
canSend={canSend}
|
||||
configurations={configurations}
|
||||
input={input}
|
||||
isStreaming={Boolean(streamState.activeTaskId)}
|
||||
models={providerModels}
|
||||
selectedConfigurationConnected={
|
||||
selectedConfiguration?.connected === true
|
||||
}
|
||||
selectedModel={selectedModel}
|
||||
selectedProvider={selectedProvider}
|
||||
onInputChange={setInput}
|
||||
onProviderChange={handleProviderChange}
|
||||
onModelChange={setSelectedModel}
|
||||
onStop={handleStop}
|
||||
onSubmit={handleSubmit}
|
||||
onSubmitText={submitMessage}
|
||||
@@ -392,18 +361,12 @@ export function LighthouseV2ChatPage({
|
||||
/>
|
||||
<LighthouseV2Composer
|
||||
canSend={canSend}
|
||||
configurations={configurations}
|
||||
input={input}
|
||||
isStreaming={Boolean(streamState.activeTaskId)}
|
||||
models={providerModels}
|
||||
selectedConfigurationConnected={
|
||||
selectedConfiguration?.connected === true
|
||||
}
|
||||
selectedModel={selectedModel}
|
||||
selectedProvider={selectedProvider}
|
||||
onInputChange={setInput}
|
||||
onProviderChange={handleProviderChange}
|
||||
onModelChange={setSelectedModel}
|
||||
onStop={handleStop}
|
||||
onSubmit={handleSubmit}
|
||||
onSubmitText={submitMessage}
|
||||
@@ -471,16 +434,10 @@ function LighthouseV2Feedback({
|
||||
|
||||
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<HTMLFormElement>) => void;
|
||||
onSubmitText: (text: string) => Promise<void>;
|
||||
@@ -488,23 +445,17 @@ interface LighthouseV2ComposerProps {
|
||||
|
||||
function LighthouseV2Composer({
|
||||
canSend,
|
||||
configurations,
|
||||
input,
|
||||
isStreaming,
|
||||
models,
|
||||
selectedConfigurationConnected,
|
||||
selectedModel,
|
||||
selectedProvider,
|
||||
onInputChange,
|
||||
onModelChange,
|
||||
onProviderChange,
|
||||
onStop,
|
||||
onSubmit,
|
||||
onSubmitText,
|
||||
}: LighthouseV2ComposerProps) {
|
||||
return (
|
||||
<form
|
||||
className="border-border-neutral-secondary bg-bg-neutral-primary flex min-h-[150px] w-full flex-col rounded-[8px] border shadow-xs"
|
||||
className="border-border-neutral-secondary bg-bg-neutral-secondary flex min-h-[150px] w-full flex-col rounded-[8px] border shadow-xs"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Textarea
|
||||
@@ -527,47 +478,7 @@ function LighthouseV2Composer({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 px-3 pb-3">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<Select
|
||||
value={selectedProvider}
|
||||
onValueChange={(value) =>
|
||||
onProviderChange(value as LighthouseV2ProviderType)
|
||||
}
|
||||
>
|
||||
<SelectTrigger size="sm" iconSize="sm" className="h-8 w-[150px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{configurations.map((configuration) => (
|
||||
<SelectItem
|
||||
key={configuration.providerType}
|
||||
value={configuration.providerType}
|
||||
disabled={configuration.connected !== true}
|
||||
>
|
||||
{configuration.providerType}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedModel} onValueChange={onModelChange}>
|
||||
<SelectTrigger size="sm" iconSize="sm" className="h-8 w-[220px]">
|
||||
<SelectValue placeholder="Model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent width="wide">
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button type="button" variant="outline" size="icon-sm" asChild>
|
||||
<Link href="/lighthouse/config" aria-label="Lighthouse settings">
|
||||
<Settings className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-end px-3 pb-3">
|
||||
{isStreaming ? (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user