mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): recover Lighthouse chat stream state
This commit is contained in:
@@ -442,13 +442,16 @@ describe("LighthouseV2ChatPage", () => {
|
||||
const recordCurrentUrl = () => {
|
||||
notifiedUrls.push(`${window.location.pathname}${window.location.search}`);
|
||||
};
|
||||
window.addEventListener(
|
||||
LIGHTHOUSE_V2_SESSIONS_CHANGED_EVENT,
|
||||
recordCurrentUrl,
|
||||
);
|
||||
renderPage();
|
||||
|
||||
try {
|
||||
// Register inside the try so a throw in renderPage() can't leak the
|
||||
// listener into later tests (the finally only runs if we entered the try).
|
||||
window.addEventListener(
|
||||
LIGHTHOUSE_V2_SESSIONS_CHANGED_EVENT,
|
||||
recordCurrentUrl,
|
||||
);
|
||||
renderPage();
|
||||
|
||||
// When
|
||||
await user.type(
|
||||
screen.getByRole("textbox", { name: "Message" }),
|
||||
|
||||
@@ -67,6 +67,7 @@ interface LighthouseV2ChatPageProps {
|
||||
initialActiveTaskId?: string | null;
|
||||
initialStreamUrl?: string;
|
||||
initialPrompt?: string;
|
||||
initialError?: string;
|
||||
}
|
||||
|
||||
export function LighthouseV2ChatPage({
|
||||
@@ -78,6 +79,7 @@ export function LighthouseV2ChatPage({
|
||||
initialActiveTaskId,
|
||||
initialStreamUrl,
|
||||
initialPrompt,
|
||||
initialError,
|
||||
}: LighthouseV2ChatPageProps) {
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const initialPromptSentRef = useRef(false);
|
||||
@@ -96,7 +98,7 @@ export function LighthouseV2ChatPage({
|
||||
);
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
const [input, setInput] = useState("");
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [feedback, setFeedback] = useState<string | null>(initialError ?? null);
|
||||
const [blockedByConflict, setBlockedByConflict] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [lastSubmittedText, setLastSubmittedText] = useState<string | null>(
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("event-reducer", () => {
|
||||
expect(next.activeTaskId).toBeNull();
|
||||
});
|
||||
|
||||
it("should mark disconnect as recoverable", () => {
|
||||
it("should clear the task gate on disconnect so retry can recover", () => {
|
||||
// Given
|
||||
const state = createInitialLighthouseV2StreamState("task-1");
|
||||
|
||||
@@ -141,6 +141,8 @@ describe("event-reducer", () => {
|
||||
|
||||
// Then
|
||||
expect(next.status).toBe("disconnected");
|
||||
expect(next.activeTaskId).toBe("task-1");
|
||||
// activeTaskId must be cleared: leaving it set keeps canSend false and
|
||||
// makes the Retry button a no-op after a dropped SSE connection.
|
||||
expect(next.activeTaskId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,11 @@ export type LighthouseV2StreamActivityItem =
|
||||
| LighthouseV2StreamTextActivityItem
|
||||
| LighthouseV2StreamToolCallActivityItem;
|
||||
|
||||
export interface LighthouseV2StreamError {
|
||||
code: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface LighthouseV2StreamState {
|
||||
status: LighthouseV2StreamStatus;
|
||||
activeTaskId: string | null;
|
||||
@@ -56,10 +61,7 @@ export interface LighthouseV2StreamState {
|
||||
toolCalls: LighthouseV2ToolCallState[];
|
||||
activityItems: LighthouseV2StreamActivityItem[];
|
||||
messageId?: string;
|
||||
error?: {
|
||||
code: string;
|
||||
detail: string;
|
||||
};
|
||||
error?: LighthouseV2StreamError;
|
||||
}
|
||||
|
||||
export function createInitialLighthouseV2StreamState(
|
||||
@@ -151,9 +153,12 @@ export function reduceLighthouseV2Event(
|
||||
},
|
||||
};
|
||||
case LIGHTHOUSE_V2_SSE_EVENT.DISCONNECT:
|
||||
// Clear the task gate so the UI can recover: keeping activeTaskId set
|
||||
// leaves canSend false and makes the Retry button a no-op.
|
||||
return {
|
||||
...state,
|
||||
status: LIGHTHOUSE_V2_STREAM_STATUS.DISCONNECTED,
|
||||
activeTaskId: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,34 +58,50 @@ export default async function AIChatbot({
|
||||
const result = await getLighthouseV2SupportedModels(
|
||||
configuration.providerType,
|
||||
);
|
||||
return [
|
||||
configuration.providerType,
|
||||
"data" in result ? result.data : [],
|
||||
] as const satisfies readonly [
|
||||
LighthouseV2ProviderType,
|
||||
LighthouseV2SupportedModel[],
|
||||
];
|
||||
return [configuration.providerType, result] as const;
|
||||
}),
|
||||
);
|
||||
const modelsByProvider = Object.fromEntries(modelsEntries) as Record<
|
||||
LighthouseV2ProviderType,
|
||||
LighthouseV2SupportedModel[]
|
||||
>;
|
||||
const modelsByProvider = Object.fromEntries(
|
||||
modelsEntries.map(([providerType, result]) => [
|
||||
providerType,
|
||||
"data" in result ? result.data : [],
|
||||
]),
|
||||
) as Record<LighthouseV2ProviderType, LighthouseV2SupportedModel[]>;
|
||||
// Surface (rather than silently swallow to []) providers whose models
|
||||
// failed to load, so an empty model list reads as a real backend failure.
|
||||
const failedModelProviders = modelsEntries
|
||||
.filter(([, result]) => !("data" in result))
|
||||
.map(([providerType]) => providerType);
|
||||
const modelsError =
|
||||
failedModelProviders.length > 0
|
||||
? `Could not load available models for: ${failedModelProviders.join(", ")}. Try again shortly.`
|
||||
: undefined;
|
||||
|
||||
const [initialMessages, activeSession] = activeSessionId
|
||||
? await Promise.all([
|
||||
getLighthouseV2Messages(activeSessionId),
|
||||
getLighthouseV2Session(activeSessionId),
|
||||
])
|
||||
: [{ data: [] }, undefined];
|
||||
// Treat the ?session= id as valid when its messages load (you can't fetch
|
||||
// messages for a non-existent session, so this is the authoritative
|
||||
// "session exists" check). A stale/deleted id fails here and is dropped so
|
||||
// the client starts fresh instead of sending against a dead session. The
|
||||
// session-metadata read stays best-effort — it only supplies activeTaskId,
|
||||
// so a flaky metadata read must NOT discard an otherwise-valid session.
|
||||
const sessionLoaded = Boolean(activeSessionId) && "data" in initialMessages;
|
||||
const validSessionId = sessionLoaded ? activeSessionId : undefined;
|
||||
const chatMessages =
|
||||
sessionLoaded && "data" in initialMessages ? initialMessages.data : [];
|
||||
const initialActiveTaskId =
|
||||
activeSession && "data" in activeSession
|
||||
sessionLoaded && activeSession && "data" in activeSession
|
||||
? (activeSession.data.activeTaskId ?? null)
|
||||
: null;
|
||||
const initialStreamUrl =
|
||||
activeSessionId && initialActiveTaskId
|
||||
? buildLighthouseV2StreamUrl(activeSessionId)
|
||||
validSessionId && initialActiveTaskId
|
||||
? buildLighthouseV2StreamUrl(validSessionId)
|
||||
: undefined;
|
||||
const chatRouteKey = activeSessionId ?? initialPrompt ?? "new";
|
||||
const chatRouteKey = validSessionId ?? initialPrompt ?? "new";
|
||||
|
||||
return (
|
||||
<ContentLayout title="Lighthouse AI" icon={<LighthouseIcon />}>
|
||||
@@ -96,13 +112,12 @@ export default async function AIChatbot({
|
||||
configurations={configurations}
|
||||
modelsByProvider={modelsByProvider}
|
||||
supportedProviders={supportedProviders}
|
||||
initialSessionId={activeSessionId}
|
||||
initialMessages={
|
||||
"data" in initialMessages ? initialMessages.data : []
|
||||
}
|
||||
initialSessionId={validSessionId}
|
||||
initialMessages={chatMessages}
|
||||
initialActiveTaskId={initialActiveTaskId}
|
||||
initialStreamUrl={initialStreamUrl}
|
||||
initialPrompt={initialPrompt}
|
||||
initialError={modelsError}
|
||||
/>
|
||||
</div>
|
||||
</ContentLayout>
|
||||
|
||||
Reference in New Issue
Block a user