diff --git a/frontend/src/features/chat/ChatPage.a11y.test.tsx b/frontend/src/features/chat/ChatPage.a11y.test.tsx index cb83259..4f23b6f 100644 --- a/frontend/src/features/chat/ChatPage.a11y.test.tsx +++ b/frontend/src/features/chat/ChatPage.a11y.test.tsx @@ -338,6 +338,18 @@ describe("ChatPage response announcements", () => { await waitFor(() => expect(announcer()).toHaveTextContent(/^Response complete$/)); }); + it("says a response was cut short when the context limit ended it, and shows why under the answer", async () => { + const user = userEvent.setup(); + const { api, finishWith } = harness(); + renderChat(api); + await sendPrompt(user); + + await finishWith("generation.completed", { assistant_message_id: "assistant-1" }, savedAnswer({ eval_count: 900, stop_reason: "length" })); + + await waitFor(() => expect(announcer()).toHaveTextContent(/^Response cut short by the context limit$/)); + expect(await screen.findByText(/This answer was cut short by the context limit/)).toBeVisible(); + }); + it("says a response stopped after the person stopped it and the answer was kept", async () => { const user = userEvent.setup(); const { api, finishWith } = harness(); diff --git a/frontend/src/features/chat/MessageCard.test.tsx b/frontend/src/features/chat/MessageCard.test.tsx index c831981..5c06fc0 100644 --- a/frontend/src/features/chat/MessageCard.test.tsx +++ b/frontend/src/features/chat/MessageCard.test.tsx @@ -92,4 +92,34 @@ describe("MessageCard", () => { expect(screen.queryByRole("region", { name: "Cortex suggests remembering" })).toBeNull(); expect(screen.getByText("Noted.")).toBeInTheDocument(); }); + + it("notes under an answer the context limit cut short, and says what helps", () => { + renderCard({ + id: "assistant-1", + role: "assistant", + content: "The three steps are: first, open the", + stats: { eval_count: 512, tokens_per_second: 24.5, stop_reason: "length" }, + }); + + const note = screen.getByRole("note"); + expect(note).toHaveTextContent("This answer was cut short by the context limit."); + expect(note).toHaveTextContent("Raising the Context window gives answers more room."); + // The partial answer and its usage are still shown as they were. + expect(screen.getByText("The three steps are: first, open the")).toBeInTheDocument(); + expect(screen.getByText(/512 tok/)).toBeInTheDocument(); + }); + + it("still says why when the limit left only reasoning and no answer", () => { + renderCard({ id: "assistant-1", role: "assistant", content: "", thoughts: "Let me think about this carefully.", stats: { stop_reason: "length" } }); + + expect(screen.getByRole("note")).toHaveTextContent("cut short by the context limit"); + }); + + it("adds no note to an answer that finished on its own, one with no reason recorded, or a person's message", () => { + renderCard({ id: "assistant-1", role: "assistant", content: "Done.", stats: { eval_count: 3, tokens_per_second: 30, stop_reason: "stop" } }); + renderCard({ id: "assistant-2", role: "assistant", content: "Older answer.", stats: { eval_count: 3, tokens_per_second: 30 } }); + renderCard({ id: "user-1", role: "user", content: "Question?", stats: { stop_reason: "length" } }); + + expect(screen.queryByText(/cut short by the context limit/)).toBeNull(); + }); }); diff --git a/frontend/src/features/chat/MessageCard.tsx b/frontend/src/features/chat/MessageCard.tsx index 48dd3c7..8398a83 100644 --- a/frontend/src/features/chat/MessageCard.tsx +++ b/frontend/src/features/chat/MessageCard.tsx @@ -74,6 +74,10 @@ export const MessageCard = memo(function MessageCard({ message, index, isFinalAs
{message.role === "user" ?

{message.content}

: }
)} {message.role === "assistant" && untranslated &&

Couldn't translate this answer; showing the original.

} + {/* The runtime reports why the model stopped. "length" means it ran + into the context limit, and the answer ends wherever that fell: + without this it reads as though it finished. */} + {message.role === "assistant" && message.stats?.stop_reason === "length" &&

This answer was cut short by the context limit. Raising the Context window gives answers more room.

} {message.sources && message.sources.length > 0 &&
Sources{message.sources.length} {message.sources.length === 1 ? "item" : "items"}
typeof source === "string" ? source : JSON.stringify(source)).join("\n\n")} />
} {message.role === "assistant" && message.id && onSaveMemory && proposedMemories && proposedMemories.length > 0 && ( diff --git a/frontend/src/features/chat/ResponseAnnouncer.tsx b/frontend/src/features/chat/ResponseAnnouncer.tsx index fa147b1..14f1f46 100644 --- a/frontend/src/features/chat/ResponseAnnouncer.tsx +++ b/frontend/src/features/chat/ResponseAnnouncer.tsx @@ -4,12 +4,14 @@ import { useChatStore } from "../../stores/useChatStore"; /** * What to tell someone who cannot see the answer arrive, once it is saved: - * whether it finished or was stopped, and how long it was when that is known. + * whether it finished, was stopped, or was cut off by the context limit, and + * how long it was when that is known. */ function describeFinishedResponse(chat: ChatResponse | null): string { const lastAssistant = [...(chat?.messages ?? [])].reverse().find((message) => message.role === "assistant"); const stats = lastAssistant?.stats; if (stats?.stopped) return "Response stopped"; + if (stats?.stop_reason === "length") return "Response cut short by the context limit"; const tokens = stats?.eval_count; if (typeof tokens === "number" && tokens > 0) { return `Response complete, ${tokens.toLocaleString()} ${tokens === 1 ? "token" : "tokens"}`;