mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-20 08:32:25 +00:00
fix(webui): keep late reasoning attached above the answer
Some providers only surface structured `reasoning_content` after answer text has already streamed. The WebUI was treating those late `reasoning_delta` frames as a fresh assistant placeholder, so the Thinking bubble rendered below the already-visible answer. Attach late reasoning back to the active assistant turn instead. The bubble still renders above the message content, preserving the expected Thinking -> answer order even when the provider protocol delivers the reasoning post-hoc. Added a regression test for answer-first followed by reasoning_delta/reasoning_end. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
458b4ba235
commit
9829cf66d2
@ -21,17 +21,23 @@ interface StreamBuffer {
|
|||||||
/**
|
/**
|
||||||
* Append a reasoning chunk to the last open reasoning stream in ``prev``.
|
* Append a reasoning chunk to the last open reasoning stream in ``prev``.
|
||||||
*
|
*
|
||||||
* Lookup rule: find the most recent assistant turn that is either still
|
* Lookup rule: prefer the most recent assistant turn in the active UI tail.
|
||||||
* streaming reasoning (``reasoningStreaming``) or has no answer text yet.
|
* Most providers emit reasoning before answer text, but some only expose
|
||||||
* Anything else starts a fresh streaming placeholder so a new turn's
|
* ``reasoning_content`` after the answer stream completes. In that post-hoc
|
||||||
* reasoning never bleeds into the previous answer.
|
* case the reasoning still belongs to the same assistant turn and must render
|
||||||
|
* above the answer, not as a new row below it.
|
||||||
*/
|
*/
|
||||||
function attachReasoningChunk(prev: UIMessage[], chunk: string): UIMessage[] {
|
function attachReasoningChunk(prev: UIMessage[], chunk: string): UIMessage[] {
|
||||||
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
||||||
const candidate = prev[i];
|
const candidate = prev[i];
|
||||||
if (candidate.role !== "assistant" || candidate.kind === "trace") continue;
|
if (candidate.role !== "assistant" || candidate.kind === "trace") continue;
|
||||||
const hasAnswer = candidate.content.length > 0;
|
const hasAnswer = candidate.content.length > 0;
|
||||||
if (candidate.reasoningStreaming || (!hasAnswer && candidate.reasoning !== undefined)) {
|
if (
|
||||||
|
candidate.reasoningStreaming
|
||||||
|
|| candidate.reasoning !== undefined
|
||||||
|
|| hasAnswer
|
||||||
|
|| candidate.isStreaming
|
||||||
|
) {
|
||||||
const merged: UIMessage = {
|
const merged: UIMessage = {
|
||||||
...candidate,
|
...candidate,
|
||||||
reasoning: (candidate.reasoning ?? "") + chunk,
|
reasoning: (candidate.reasoning ?? "") + chunk,
|
||||||
|
|||||||
@ -209,6 +209,35 @@ describe("useNanobotStream", () => {
|
|||||||
expect(result.current.messages[0].reasoningStreaming).toBe(false);
|
expect(result.current.messages[0].reasoningStreaming).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("attaches post-hoc reasoning to the same assistant turn above the answer", () => {
|
||||||
|
const fake = fakeClient();
|
||||||
|
const { result } = renderHook(() => useNanobotStream("chat-r5", EMPTY_MESSAGES), {
|
||||||
|
wrapper: wrap(fake.client),
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fake.emit("chat-r5", {
|
||||||
|
event: "delta",
|
||||||
|
chat_id: "chat-r5",
|
||||||
|
text: "hi~",
|
||||||
|
});
|
||||||
|
fake.emit("chat-r5", { event: "stream_end", chat_id: "chat-r5" });
|
||||||
|
fake.emit("chat-r5", {
|
||||||
|
event: "reasoning_delta",
|
||||||
|
chat_id: "chat-r5",
|
||||||
|
text: "This reasoning arrived after the answer stream.",
|
||||||
|
});
|
||||||
|
fake.emit("chat-r5", { event: "reasoning_end", chat_id: "chat-r5" });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.messages).toHaveLength(1);
|
||||||
|
expect(result.current.messages[0].content).toBe("hi~");
|
||||||
|
expect(result.current.messages[0].reasoning).toBe(
|
||||||
|
"This reasoning arrived after the answer stream.",
|
||||||
|
);
|
||||||
|
expect(result.current.messages[0].reasoningStreaming).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("attaches assistant media_urls to complete messages", () => {
|
it("attaches assistant media_urls to complete messages", () => {
|
||||||
const fake = fakeClient();
|
const fake = fakeClient();
|
||||||
const { result } = renderHook(() => useNanobotStream("chat-m", EMPTY_MESSAGES), {
|
const { result } = renderHook(() => useNanobotStream("chat-m", EMPTY_MESSAGES), {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user