feat(FN-2009): show waiting and thinking states during chat streaming
- Render streaming assistant bubbles in ChatView and QuickChatFAB as soon as streaming starts - Show "Connecting…" before text arrives and switch to "Thinking…" when thinking output is present - Render streaming thinking content in a collapsible details block in QuickChatFAB to match ChatView behavior - Add muted italic waiting-state styles for chat and quick chat message content - Add ChatView streaming state tests for waiting and thinking pre-text scenarios
This commit is contained in:
@@ -992,7 +992,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
<div className="chat-message-time">{formatRelativeTime(message.createdAt)}</div>
|
||||
</div>
|
||||
))}
|
||||
{isStreaming && streamingText && (
|
||||
{isStreaming && (
|
||||
<div className="chat-message chat-message--assistant chat-message--streaming">
|
||||
<div className="chat-message-avatar">
|
||||
<Bot size={14} />
|
||||
@@ -1002,7 +1002,13 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
return modelTag ? <span className="chat-model-tag">{modelTag}</span> : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="chat-message-content">{renderMessageContent(streamingText)}</div>
|
||||
{streamingText ? (
|
||||
<div className="chat-message-content">{renderMessageContent(streamingText)}</div>
|
||||
) : (
|
||||
<div className="chat-message-content chat-message-content--waiting">
|
||||
{streamingThinking ? "Thinking…" : "Connecting…"}
|
||||
</div>
|
||||
)}
|
||||
{streamingThinking && (
|
||||
<details className="chat-message-thinking">
|
||||
<summary>Thinking</summary>
|
||||
|
||||
@@ -865,7 +865,7 @@ export function QuickChatFAB({ projectId, addToast, showFAB = true, open, onOpen
|
||||
<div className="quick-chat-panel-messages" ref={messagesRef} data-testid="quick-chat-messages">
|
||||
{sessionsLoading || messagesLoading ? (
|
||||
<div className="quick-chat-panel-empty">Loading conversation…</div>
|
||||
) : messages.length === 0 && !streamingText && !streamingThinking ? (
|
||||
) : messages.length === 0 && !streamingText && !streamingThinking && !isStreaming ? (
|
||||
<div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -882,18 +882,23 @@ export function QuickChatFAB({ projectId, addToast, showFAB = true, open, onOpen
|
||||
);
|
||||
})}
|
||||
{/* Streaming message bubble */}
|
||||
{(streamingText || streamingThinking) && (
|
||||
{isStreaming && (
|
||||
<div
|
||||
className="quick-chat-panel-message quick-chat-panel-message--received quick-chat-panel-message--streaming"
|
||||
data-testid="quick-chat-streaming-message"
|
||||
>
|
||||
{streamingThinking && (
|
||||
<p className="quick-chat-panel-thinking" data-testid="quick-chat-streaming-thinking">
|
||||
{streamingThinking}
|
||||
{streamingText ? (
|
||||
<p data-testid="quick-chat-streaming-text">{renderMessageContent(streamingText)}</p>
|
||||
) : (
|
||||
<p className="quick-chat-panel-waiting" data-testid="quick-chat-waiting">
|
||||
{streamingThinking ? "Thinking…" : "Connecting…"}
|
||||
</p>
|
||||
)}
|
||||
{streamingText && (
|
||||
<p data-testid="quick-chat-streaming-text">{renderMessageContent(streamingText)}</p>
|
||||
{streamingThinking && (
|
||||
<details className="chat-message-thinking" data-testid="quick-chat-streaming-thinking">
|
||||
<summary>Thinking</summary>
|
||||
<pre className="chat-message-thinking-content">{streamingThinking}</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -810,6 +810,64 @@ describe("ChatView", () => {
|
||||
expect(details).toHaveProperty("open", false);
|
||||
});
|
||||
|
||||
describe("streaming states", () => {
|
||||
it("shows waiting indicator when streaming starts before text arrives", () => {
|
||||
setupMockChat({
|
||||
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "user", content: "Hello", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
isStreaming: true,
|
||||
streamingText: "",
|
||||
streamingThinking: "",
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
// Streaming message should show with "Connecting..." text
|
||||
const streamingMessage = document.querySelector(".chat-message--streaming");
|
||||
expect(streamingMessage).toBeInTheDocument();
|
||||
expect(streamingMessage?.textContent).toContain("Connecting");
|
||||
|
||||
// Waiting class should be present
|
||||
const waitingContent = streamingMessage?.querySelector(".chat-message-content--waiting");
|
||||
expect(waitingContent).toBeInTheDocument();
|
||||
|
||||
// Typing indicator dots should be rendered
|
||||
const typingIndicator = streamingMessage?.querySelector(".chat-typing-indicator");
|
||||
expect(typingIndicator).toBeInTheDocument();
|
||||
expect(typingIndicator?.querySelectorAll("span").length).toBe(3);
|
||||
});
|
||||
|
||||
it("shows thinking indicator when streaming thinking arrives before text", () => {
|
||||
setupMockChat({
|
||||
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "user", content: "Hello", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
isStreaming: true,
|
||||
streamingText: "",
|
||||
streamingThinking: "analyzing the request...",
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
// Streaming message should show with "Thinking..." text
|
||||
const streamingMessage = document.querySelector(".chat-message--streaming");
|
||||
expect(streamingMessage).toBeInTheDocument();
|
||||
expect(streamingMessage?.textContent).toContain("Thinking");
|
||||
|
||||
// Thinking details should be rendered
|
||||
const thinkingDetails = streamingMessage?.querySelector("details.chat-message-thinking");
|
||||
expect(thinkingDetails).toBeInTheDocument();
|
||||
expect(thinkingDetails?.querySelector(".chat-message-thinking-content")?.textContent).toContain("analyzing the request");
|
||||
|
||||
// Typing indicator dots should be rendered
|
||||
const typingIndicator = streamingMessage?.querySelector(".chat-typing-indicator");
|
||||
expect(typingIndicator).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters sessions by search query", async () => {
|
||||
setupMockChat({
|
||||
sessions: [
|
||||
|
||||
Reference in New Issue
Block a user