Skip to content

Commit a1761cb

Browse files
committed
Improve streaming experience
1 parent 02aea0f commit a1761cb

File tree

5 files changed

+47
-57
lines changed

5 files changed

+47
-57
lines changed

src/components/AIView.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ const EmptyState = styled.div`
9494
}
9595
`;
9696

97+
const GlobalStreamingIndicator = styled.div`
98+
font-size: 10px;
99+
color: var(--color-assistant-border);
100+
font-style: italic;
101+
padding: 8px 0;
102+
animation: pulse 1.5s ease-in-out infinite;
103+
104+
@keyframes pulse {
105+
0%,
106+
100% {
107+
opacity: 0.6;
108+
}
109+
50% {
110+
opacity: 1;
111+
}
112+
}
113+
`;
114+
97115
const EditBarrier = styled.div`
98116
margin: 20px 0;
99117
padding: 12px 15px;
@@ -506,6 +524,9 @@ const AIViewInner: React.FC<AIViewProps> = ({ workspaceId, projectName, branch,
506524
})}
507525
</>
508526
)}
527+
{canInterrupt && (
528+
<GlobalStreamingIndicator>streaming... hit Esc to cancel</GlobalStreamingIndicator>
529+
)}
509530
{errorMessage && (
510531
<ErrorMessage
511532
title={errorMessage.title}

src/components/ChatInput.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,7 @@ const InputControls = styled.div`
3030
const InputField = styled.textarea<{ isEditing?: boolean; canInterrupt?: boolean }>`
3131
flex: 1;
3232
background: ${(props) => (props.isEditing ? "var(--color-editing-mode-alpha)" : "#1e1e1e")};
33-
border: 1px solid
34-
${(props) =>
35-
props.isEditing
36-
? "var(--color-editing-mode)"
37-
: props.canInterrupt
38-
? "var(--color-interrupted)"
39-
: "#3e3e42"};
33+
border: 1px solid ${(props) => (props.isEditing ? "var(--color-editing-mode)" : "#3e3e42")};
4034
color: #d4d4d4;
4135
padding: 8px 12px;
4236
border-radius: 4px;
@@ -50,12 +44,7 @@ const InputField = styled.textarea<{ isEditing?: boolean; canInterrupt?: boolean
5044
5145
&:focus {
5246
outline: none;
53-
border-color: ${(props) =>
54-
props.isEditing
55-
? "var(--color-editing-mode)"
56-
: props.canInterrupt
57-
? "var(--color-interrupted)"
58-
: "#569cd6"};
47+
border-color: ${(props) => (props.isEditing ? "var(--color-editing-mode)" : "#569cd6")};
5948
}
6049
6150
&::placeholder {

src/components/Messages/AssistantMessage.tsx

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,6 @@ const RawContent = styled.pre`
1919
border-radius: 3px;
2020
`;
2121

22-
const StreamingIndicator = styled.span`
23-
font-size: 10px;
24-
color: var(--color-assistant-border);
25-
font-style: italic;
26-
margin-right: 8px;
27-
animation: pulse 1.5s ease-in-out infinite;
28-
29-
@keyframes pulse {
30-
0%,
31-
100% {
32-
opacity: 0.6;
33-
}
34-
50% {
35-
opacity: 1;
36-
}
37-
}
38-
`;
39-
4022
const WaitingMessage = styled.div`
4123
font-family: var(--font-primary);
4224
font-size: 13px;
@@ -94,11 +76,6 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({ message, cla
9476
},
9577
];
9678

97-
// Streaming indicator in right label
98-
const rightLabel = isStreaming ? (
99-
<StreamingIndicator>streaming...</StreamingIndicator>
100-
) : undefined;
101-
10279
// Render appropriate content based on state
10380
const renderContent = () => {
10481
// Empty streaming state
@@ -141,7 +118,6 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({ message, cla
141118
borderColor="var(--color-assistant-border)"
142119
message={message}
143120
buttons={buttons}
144-
rightLabel={rightLabel}
145121
className={className}
146122
>
147123
{renderContent()}

src/components/Messages/ReasoningMessage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ export const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message, cla
131131
<HeaderLeft>
132132
<ThinkingIcon>💭</ThinkingIcon>
133133
<span>Thinking</span>
134-
{isStreaming && <StreamingIndicator>streaming...</StreamingIndicator>}
135134
</HeaderLeft>
136135
{!isStreaming && <Caret isExpanded={isExpanded}></Caret>}
137136
</ReasoningHeader>

src/components/Messages/TypewriterMarkdown.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,31 @@ import styled from "@emotion/styled";
33
import { markdownStyles } from "../Messages/MarkdownStyles";
44
import { MarkdownCore } from "./MarkdownCore";
55

6-
const MarkdownContainer = styled.div`
6+
const MarkdownContainer = styled.div<{ isStreaming: boolean }>`
77
${markdownStyles}
8-
`;
9-
10-
const CursorSpan = styled.span<{ show: boolean }>`
11-
display: ${(props) => (props.show ? "inline" : "none")};
12-
animation: blink 1s step-end infinite;
138
14-
@keyframes blink {
15-
0%,
16-
50% {
17-
opacity: 1;
9+
/* Target the last text node's parent when streaming and add blinking cursor */
10+
${(props) =>
11+
props.isStreaming &&
12+
`
13+
p:last-child::after,
14+
li:last-child::after,
15+
div:last-child::after,
16+
span:last-child::after {
17+
content: "▊";
18+
margin-left: 0.15em;
19+
animation: blink 1s step-end infinite;
1820
}
19-
51%,
20-
100% {
21-
opacity: 0;
21+
22+
@keyframes blink {
23+
0%, 50% {
24+
opacity: 1;
25+
}
26+
51%, 100% {
27+
opacity: 0;
28+
}
2229
}
23-
}
30+
`}
2431
`;
2532

2633
interface TypewriterMarkdownProps {
@@ -39,13 +46,11 @@ export const TypewriterMarkdown = React.memo<TypewriterMarkdownProps>(function T
3946
const content = deltas.join("");
4047

4148
// Show cursor only when streaming (not complete)
42-
const showCursor = !isComplete && content.length > 0;
49+
const isStreaming = !isComplete && content.length > 0;
4350

4451
return (
45-
<MarkdownContainer className={className}>
46-
<MarkdownCore content={content}>
47-
<CursorSpan show={showCursor}></CursorSpan>
48-
</MarkdownCore>
52+
<MarkdownContainer className={className} isStreaming={isStreaming}>
53+
<MarkdownCore content={content} />
4954
</MarkdownContainer>
5055
);
5156
});

0 commit comments

Comments
 (0)