story-kit: merge 163_story_remove_bubble_styling_from_streaming_chat_messages
This commit is contained in:
@@ -892,3 +892,164 @@ describe("Chat message queue (Story 155)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
capturedWsHandlers = null;
|
||||||
|
setupMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC1: streaming assistant message uses transparent background, no extra padding, no border-radius", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
// Send a message to put chat into loading state
|
||||||
|
const input = screen.getByPlaceholderText("Send a message...");
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(input, { target: { value: "Hello" } });
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate streaming tokens arriving
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onToken("Streaming response text");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the streaming message container (the inner div wrapping the Markdown)
|
||||||
|
const streamingText = await screen.findByText("Streaming response text");
|
||||||
|
// The markdown-body wrapper is the parent, and the styled div is its parent
|
||||||
|
const styledDiv = streamingText.closest(".markdown-body")
|
||||||
|
?.parentElement as HTMLElement;
|
||||||
|
|
||||||
|
expect(styledDiv).toBeTruthy();
|
||||||
|
const styleAttr = styledDiv.getAttribute("style") ?? "";
|
||||||
|
expect(styleAttr).toContain("background: transparent");
|
||||||
|
expect(styleAttr).toContain("padding: 0px");
|
||||||
|
expect(styleAttr).toContain("border-radius: 0px");
|
||||||
|
expect(styleAttr).toContain("max-width: 100%");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC1: streaming message wraps Markdown in markdown-body class", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText("Send a message...");
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(input, { target: { value: "Hello" } });
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onToken("Some markdown content");
|
||||||
|
});
|
||||||
|
|
||||||
|
const streamingText = await screen.findByText("Some markdown content");
|
||||||
|
const markdownBody = streamingText.closest(".markdown-body");
|
||||||
|
expect(markdownBody).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC2: no visual change when streaming ends and message transitions to completed", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
// Send a message to start streaming
|
||||||
|
const input = screen.getByPlaceholderText("Send a message...");
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(input, { target: { value: "Hello" } });
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate streaming tokens
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onToken("Final response");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture streaming message style attribute
|
||||||
|
const streamingText = await screen.findByText("Final response");
|
||||||
|
const streamingStyledDiv = streamingText.closest(".markdown-body")
|
||||||
|
?.parentElement as HTMLElement;
|
||||||
|
const streamingStyleAttr = streamingStyledDiv.getAttribute("style") ?? "";
|
||||||
|
|
||||||
|
// Transition: onUpdate completes the message
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onUpdate([
|
||||||
|
{ role: "user", content: "Hello" },
|
||||||
|
{ role: "assistant", content: "Final response" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the completed message — it should have the same styling
|
||||||
|
const completedText = await screen.findByText("Final response");
|
||||||
|
const completedMarkdownBody = completedText.closest(".markdown-body");
|
||||||
|
const completedStyledDiv =
|
||||||
|
completedMarkdownBody?.parentElement as HTMLElement;
|
||||||
|
|
||||||
|
expect(completedStyledDiv).toBeTruthy();
|
||||||
|
const completedStyleAttr = completedStyledDiv.getAttribute("style") ?? "";
|
||||||
|
|
||||||
|
// Both streaming and completed use transparent bg, 0 padding, 0 border-radius
|
||||||
|
expect(completedStyleAttr).toContain("background: transparent");
|
||||||
|
expect(completedStyleAttr).toContain("padding: 0px");
|
||||||
|
expect(completedStyleAttr).toContain("border-radius: 0px");
|
||||||
|
expect(streamingStyleAttr).toContain("background: transparent");
|
||||||
|
expect(streamingStyleAttr).toContain("padding: 0px");
|
||||||
|
expect(streamingStyleAttr).toContain("border-radius: 0px");
|
||||||
|
|
||||||
|
// Both have the markdown-body class wrapper
|
||||||
|
expect(streamingStyledDiv.querySelector(".markdown-body")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC3: completed assistant messages retain transparent background and no border-radius", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onUpdate([
|
||||||
|
{ role: "user", content: "Hi" },
|
||||||
|
{ role: "assistant", content: "Hello there!" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistantText = await screen.findByText("Hello there!");
|
||||||
|
const markdownBody = assistantText.closest(".markdown-body");
|
||||||
|
const styledDiv = markdownBody?.parentElement as HTMLElement;
|
||||||
|
|
||||||
|
expect(styledDiv).toBeTruthy();
|
||||||
|
const styleAttr = styledDiv.getAttribute("style") ?? "";
|
||||||
|
expect(styleAttr).toContain("background: transparent");
|
||||||
|
expect(styleAttr).toContain("padding: 0px");
|
||||||
|
expect(styleAttr).toContain("border-radius: 0px");
|
||||||
|
expect(styleAttr).toContain("max-width: 100%");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC3: completed user messages still have their bubble styling", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onUpdate([
|
||||||
|
{ role: "user", content: "I am a user message" },
|
||||||
|
{ role: "assistant", content: "I am a response" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// findByText returns the styled div itself for user messages (text is direct child)
|
||||||
|
const userStyledDiv = await screen.findByText("I am a user message");
|
||||||
|
const styleAttr = userStyledDiv.getAttribute("style") ?? "";
|
||||||
|
// User messages retain bubble: distinct background, padding, rounded corners
|
||||||
|
expect(styleAttr).toContain("padding: 10px 16px");
|
||||||
|
expect(styleAttr).toContain("border-radius: 20px");
|
||||||
|
expect(styleAttr).not.toContain("background: transparent");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -761,45 +761,49 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "85%",
|
maxWidth: "100%",
|
||||||
padding: "16px 20px",
|
padding: "0",
|
||||||
borderRadius: "12px",
|
borderRadius: "0",
|
||||||
background: "#262626",
|
background: "transparent",
|
||||||
color: "#fff",
|
color: "#ececec",
|
||||||
border: "1px solid #404040",
|
border: "none",
|
||||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
fontFamily: "inherit",
|
||||||
fontSize: "0.95rem",
|
fontSize: "1em",
|
||||||
fontWeight: 400,
|
fontWeight: "500",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "normal",
|
||||||
lineHeight: 1.6,
|
lineHeight: "1.6",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Markdown
|
<div className="markdown-body">
|
||||||
components={{
|
<Markdown
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
components={{
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
code: ({ className, children, ...props }: any) => {
|
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
code: ({ className, children, ...props }: any) => {
|
||||||
const isInline = !className;
|
const match = /language-(\w+)/.exec(
|
||||||
return !isInline && match ? (
|
className || "",
|
||||||
<SyntaxHighlighter
|
);
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: oneDark style types are incompatible
|
const isInline = !className;
|
||||||
style={oneDark as any}
|
return !isInline && match ? (
|
||||||
language={match[1]}
|
<SyntaxHighlighter
|
||||||
PreTag="div"
|
// biome-ignore lint/suspicious/noExplicitAny: oneDark style types are incompatible
|
||||||
>
|
style={oneDark as any}
|
||||||
{String(children).replace(/\n$/, "")}
|
language={match[1]}
|
||||||
</SyntaxHighlighter>
|
PreTag="div"
|
||||||
) : (
|
>
|
||||||
<code className={className} {...props}>
|
{String(children).replace(/\n$/, "")}
|
||||||
{children}
|
</SyntaxHighlighter>
|
||||||
</code>
|
) : (
|
||||||
);
|
<code className={className} {...props}>
|
||||||
},
|
{children}
|
||||||
}}
|
</code>
|
||||||
>
|
);
|
||||||
{streamingContent}
|
},
|
||||||
</Markdown>
|
}}
|
||||||
|
>
|
||||||
|
{streamingContent}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user