Skip to content
10 changes: 9 additions & 1 deletion packages/react-headless/src/store/createChatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export const createChatStore = (config: StoreConfig) => {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}

const messageCountBefore = get().messages.length;

await processStreamedMessage({
response,
createMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })),
Expand All @@ -255,9 +257,15 @@ export const createChatStore = (config: StoreConfig) => {
deleteMessage: (id) => set((s) => ({ messages: s.messages.filter((m) => m.id !== id) })),
adapter: streamProtocol,
});

if (get().messages.length === messageCountBefore) {
throw new Error("Failed to get a response. Please check your connection and try again.");
}
} catch (e) {
if (!abortController.signal.aborted) {
set({ threadError: e instanceof Error ? e : new Error(String(e)) });
const error = e instanceof Error ? e : new Error(String(e));
console.error("[OpenUI] processMessage failed:", error);
set({ threadError: error });
}
} finally {
set({ _abortController: null, isRunning: false });
Expand Down
7 changes: 4 additions & 3 deletions packages/react-headless/src/stream/processStreamedMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ export const processStreamedMessage = async ({
}
break;

case EventType.RUN_ERROR:
console.error("Stream error:", (event as any).error);
break;
case EventType.RUN_ERROR: {
const msg = (event as any).message || (event as any).error || "Stream error";
throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg));
}
}

if (isFirst) {
Expand Down
18 changes: 18 additions & 0 deletions packages/react-ui/src/components/Shell/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import clsx from "clsx";
import React, { memo, useEffect, useRef } from "react";
import { useLayoutContext } from "../../context/LayoutContext";
import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom";
import { Callout } from "../Callout";
import { MarkDownRenderer } from "../MarkDownRenderer";
import { MessageLoading as MessageLoadingComponent } from "../MessageLoading";
import type { AssistantMessageComponent, UserMessageComponent } from "../OpenUIChat/types";
Expand Down Expand Up @@ -320,6 +321,21 @@ export const MessageLoading = () => {
);
};

export const ThreadError = () => {
const threadError = useThread((s) => s.threadError);
if (!threadError) return null;

return (
<div className="openui-shell-thread-error">
<Callout
variant="danger"
title="Something went wrong"
description={threadError.message || "An unexpected error occurred. Please try again."}
/>
</div>
);
};

export const Messages = ({
className,
loader,
Expand All @@ -333,6 +349,7 @@ export const Messages = ({
}) => {
const messages = useThread((s) => s.messages);
const isRunning = useThread((s) => s.isRunning);
const threadError = useThread((s) => s.threadError);

return (
<div className={clsx("openui-shell-thread-messages", className)}>
Expand All @@ -349,6 +366,7 @@ export const Messages = ({
);
})}
{isRunning && <div>{loader}</div>}
{!isRunning && threadError && <ThreadError />}
</div>
);
};
Expand Down
9 changes: 9 additions & 0 deletions packages/react-ui/src/components/Shell/thread.scss
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ $center-align-spacing: calc(32px + cssUtils.$space-s);
}
}

.openui-shell-thread-error {
padding: 0 $center-align-spacing;
min-height: auto !important;

.openui-shell-container--mobile & {
padding: 0;
}
}

.openui-shell-thread-message-loading {
display: flex;
padding: 0 $center-align-spacing;
Expand Down
Loading