fix(webui): show copy action on every assistant message
This commit is contained in:
@@ -29,7 +29,7 @@ export function buildDisplayUnits(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assistantCopyFlags(units: DisplayUnit[]): boolean[] {
|
export function assistantForkFlags(units: DisplayUnit[]): boolean[] {
|
||||||
const flags = new Array<boolean>(units.length).fill(true);
|
const flags = new Array<boolean>(units.length).fill(true);
|
||||||
let hasLaterUnitBeforeUser = false;
|
let hasLaterUnitBeforeUser = false;
|
||||||
for (let i = units.length - 1; i >= 0; i -= 1) {
|
for (let i = units.length - 1; i >= 0; i -= 1) {
|
||||||
@@ -63,7 +63,7 @@ export function ThreadMessages({
|
|||||||
() => unitIndexAfterMessageCount(units, forkBoundaryMessageCount),
|
() => unitIndexAfterMessageCount(units, forkBoundaryMessageCount),
|
||||||
[forkBoundaryMessageCount, units],
|
[forkBoundaryMessageCount, units],
|
||||||
);
|
);
|
||||||
const copyFlags = useMemo(() => assistantCopyFlags(units), [units]);
|
const forkFlags = useMemo(() => assistantForkFlags(units), [units]);
|
||||||
const liveActivityClusterIndices = useMemo(
|
const liveActivityClusterIndices = useMemo(
|
||||||
() => isStreaming ? currentActivityClusterIndices(units) : new Set<number>(),
|
() => isStreaming ? currentActivityClusterIndices(units) : new Set<number>(),
|
||||||
[isStreaming, units],
|
[isStreaming, units],
|
||||||
@@ -90,7 +90,7 @@ export function ThreadMessages({
|
|||||||
? unit.message.id
|
? unit.message.id
|
||||||
: undefined;
|
: undefined;
|
||||||
const forkIndex =
|
const forkIndex =
|
||||||
unit.type === "message" && unit.message.role === "assistant" && copyFlags[index]
|
unit.type === "message" && unit.message.role === "assistant" && forkFlags[index]
|
||||||
? nextUserIndex
|
? nextUserIndex
|
||||||
: undefined;
|
: undefined;
|
||||||
if (unit.type === "message" && unit.message.role === "user") nextUserIndex += 1;
|
if (unit.type === "message" && unit.message.role === "user") nextUserIndex += 1;
|
||||||
@@ -112,11 +112,6 @@ export function ThreadMessages({
|
|||||||
) : (
|
) : (
|
||||||
<MessageBubble
|
<MessageBubble
|
||||||
message={unit.message}
|
message={unit.message}
|
||||||
showCopyAction={
|
|
||||||
unit.message.role === "assistant"
|
|
||||||
? copyFlags[index]
|
|
||||||
: true
|
|
||||||
}
|
|
||||||
cliApps={cliApps}
|
cliApps={cliApps}
|
||||||
mcpPresets={mcpPresets}
|
mcpPresets={mcpPresets}
|
||||||
slashCommands={slashCommands}
|
slashCommands={slashCommands}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react";
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
assistantCopyFlags,
|
assistantForkFlags,
|
||||||
buildDisplayUnits,
|
buildDisplayUnits,
|
||||||
ThreadMessages,
|
ThreadMessages,
|
||||||
unitKeysForDisplay,
|
unitKeysForDisplay,
|
||||||
@@ -747,7 +747,7 @@ describe("ThreadMessages", () => {
|
|||||||
expect(screen.queryByText("Worked for 0s")).not.toBeInTheDocument();
|
expect(screen.queryByText("Worked for 0s")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows copy only on the last assistant slice before the next user turn", () => {
|
it("shows copy on every assistant slice while keeping fork on the last slice", () => {
|
||||||
const messages: UIMessage[] = [
|
const messages: UIMessage[] = [
|
||||||
{
|
{
|
||||||
id: "early",
|
id: "early",
|
||||||
@@ -771,19 +771,26 @@ describe("ThreadMessages", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
render(<ThreadMessages messages={messages} isStreaming={false} />);
|
render(
|
||||||
|
<ThreadMessages
|
||||||
|
messages={messages}
|
||||||
|
isStreaming={false}
|
||||||
|
onForkFromMessage={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(1);
|
expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(2);
|
||||||
|
expect(screen.getAllByRole("button", { name: "Fork" })).toHaveLength(1);
|
||||||
expect(screen.getByText("final reply")).toBeInTheDocument();
|
expect(screen.getByText("final reply")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows copy only on the second assistant when two text slices appear before user", () => {
|
it("shows copy on adjacent assistant text slices", () => {
|
||||||
const messages: UIMessage[] = [
|
const messages: UIMessage[] = [
|
||||||
{ id: "a1", role: "assistant", content: "part one", createdAt: 1 },
|
{ id: "a1", role: "assistant", content: "part one", createdAt: 1 },
|
||||||
{ id: "a2", role: "assistant", content: "part two", createdAt: 2 },
|
{ id: "a2", role: "assistant", content: "part two", createdAt: 2 },
|
||||||
];
|
];
|
||||||
render(<ThreadMessages messages={messages} isStreaming={false} />);
|
render(<ThreadMessages messages={messages} isStreaming={false} />);
|
||||||
expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(1);
|
expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses turn ids as activity grouping boundaries when available", () => {
|
it("uses turn ids as activity grouping boundaries when available", () => {
|
||||||
@@ -810,7 +817,7 @@ describe("ThreadMessages", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("computes final assistant copy flags with user-boundary semantics", () => {
|
it("computes final assistant fork flags with user-boundary semantics", () => {
|
||||||
const units = buildDisplayUnits([
|
const units = buildDisplayUnits([
|
||||||
{ id: "u1", role: "user", content: "one", createdAt: 1 },
|
{ id: "u1", role: "user", content: "one", createdAt: 1 },
|
||||||
{ id: "a1", role: "assistant", content: "draft", createdAt: 2 },
|
{ id: "a1", role: "assistant", content: "draft", createdAt: 2 },
|
||||||
@@ -827,7 +834,7 @@ describe("ThreadMessages", () => {
|
|||||||
{ id: "a3", role: "assistant", content: "next", createdAt: 6 },
|
{ id: "a3", role: "assistant", content: "next", createdAt: 6 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const flags = assistantCopyFlags(units);
|
const flags = assistantForkFlags(units);
|
||||||
const assistantFlags = units
|
const assistantFlags = units
|
||||||
.map((unit, index) =>
|
.map((unit, index) =>
|
||||||
unit.type === "message" && unit.message.role === "assistant"
|
unit.type === "message" && unit.message.role === "assistant"
|
||||||
|
|||||||
Reference in New Issue
Block a user