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:
Fusion
2026-04-17 19:23:40 -07:00
committed by gsxdsm
parent 695d0fd858
commit f6cc5aeaf0
4 changed files with 90 additions and 9 deletions

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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: [