Skip to content

Commit 9d61fe9

Browse files
Merge pull request #1054 from microsoft/psl-TASfeature
feat: merge UI/UX changes from psl feature branch into mark branch
2 parents 7ae54e4 + 9070913 commit 9d61fe9

5 files changed

Lines changed: 443 additions & 45 deletions

File tree

src/App/src/components/content/PlanChat.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
8787
{renderAgentMessages(agentMessages, undefined, undefined, finalResultRef)}
8888

8989
{showProcessingPlanSpinner && renderPlanExecutionMessage()}
90-
{/* Streaming plan updates */}
91-
{showBufferingText && (
90+
{/* Streaming plan updates — hidden while an approval prompt is pending so
91+
the approval action is presented at the appropriate step instead of
92+
after the thinking process visibly completes. */}
93+
{showBufferingText && !showApprovalButtons && (
9294
<StreamingBufferMessage
9395
streamingMessageBuffer={streamingMessageBuffer}
9496
isStreaming={true}

src/App/src/components/content/streaming/StreamingAgentMessage.tsx

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Body1, Tag, makeStyles, tokens, Button } from "@fluentui/react-componen
77
import { TaskService } from "@/store";
88
import { PersonRegular, ArrowDownloadRegular } from "@fluentui/react-icons";
99
import { getAgentIcon, getAgentDisplayName } from '@/utils/agentIconUtils';
10+
import { formatJsonInText } from '@/utils/jsonFormatter';
1011

1112
interface StreamingAgentMessageProps {
1213
agentMessages: AgentMessageData[];
@@ -82,6 +83,9 @@ const useStyles = makeStyles({
8283
backgroundColor: 'var(--colorNeutralBackground2)',
8384
color: 'var(--colorNeutralForeground1)',
8485
maxWidth: '100%',
86+
width: '100%',
87+
boxSizing: 'border-box',
88+
overflowX: 'hidden',
8589
alignSelf: 'flex-start',
8690

8791
},
@@ -219,10 +223,10 @@ const renderAgentMessages = (
219223
/>
220224
),
221225
img: ({ node: _imgNode, ...props }) => (
222-
<div style={{ position: 'relative', display: 'inline-block', marginTop: '8px' }}>
226+
<div style={{ position: 'relative', display: 'block', width: '100%', maxWidth: '100%', marginTop: '8px', overflow: 'hidden' }}>
223227
<img
224228
{...props}
225-
style={{ maxWidth: '100%', borderRadius: '8px', display: 'block' }}
229+
style={{ display: 'block', width: '100%', maxWidth: '100%', height: 'auto', borderRadius: '8px' }}
226230
/>
227231
<Button
228232
appearance="subtle"
@@ -239,13 +243,27 @@ const renderAgentMessages = (
239243
color: 'white',
240244
borderRadius: '4px',
241245
}}
242-
onClick={() => {
246+
onClick={async () => {
243247
const url = props.src;
244-
if (url) {
248+
if (!url) return;
249+
const filename = `ad-image-${Date.now()}.png`;
250+
try {
251+
const response = await fetch(url, { mode: 'cors' });
252+
if (!response.ok) throw new Error(`Failed to fetch image (${response.status})`);
253+
const blob = await response.blob();
254+
const blobUrl = URL.createObjectURL(blob);
255+
const link = document.createElement('a');
256+
link.href = blobUrl;
257+
link.download = filename;
258+
document.body.appendChild(link);
259+
link.click();
260+
document.body.removeChild(link);
261+
URL.revokeObjectURL(blobUrl);
262+
} catch (err) {
263+
// Fallback: trigger direct download (works for same-origin or CORS-enabled URLs)
245264
const link = document.createElement('a');
246265
link.href = url;
247-
link.download = `ad-image-${Date.now()}.png`;
248-
link.target = '_blank';
266+
link.download = filename;
249267
document.body.appendChild(link);
250268
link.click();
251269
document.body.removeChild(link);
@@ -254,10 +272,42 @@ const renderAgentMessages = (
254272
title="Download image"
255273
/>
256274
</div>
275+
),
276+
p: ({ node: _pNode, ...props }) => (
277+
<p {...props} style={{ margin: '0 0 8px 0' }} />
278+
),
279+
h1: ({ node: _hNode, ...props }) => (
280+
<h1 {...props} style={{ fontSize: '20px', fontWeight: 600, margin: '16px 0 8px 0', lineHeight: '1.3' }} />
281+
),
282+
h2: ({ node: _hNode, ...props }) => (
283+
<h2 {...props} style={{ fontSize: '17px', fontWeight: 600, margin: '14px 0 8px 0', lineHeight: '1.3' }} />
284+
),
285+
h3: ({ node: _hNode, ...props }) => (
286+
<h3 {...props} style={{ fontSize: '15px', fontWeight: 600, margin: '12px 0 6px 0', lineHeight: '1.3' }} />
287+
),
288+
ul: ({ node: _ulNode, ...props }) => (
289+
<ul {...props} style={{ margin: '8px 0', paddingLeft: '24px' }} />
290+
),
291+
ol: ({ node: _olNode, ...props }) => (
292+
<ol {...props} style={{ margin: '8px 0', paddingLeft: '24px' }} />
293+
),
294+
li: ({ node: _liNode, ...props }) => (
295+
<li {...props} style={{ margin: '4px 0', lineHeight: '1.5' }} />
296+
),
297+
blockquote: ({ node: _bqNode, ...props }) => (
298+
<blockquote
299+
{...props}
300+
style={{
301+
margin: '8px 0',
302+
padding: '8px 12px',
303+
borderLeft: '3px solid var(--colorNeutralStroke1)',
304+
color: 'var(--colorNeutralForeground2)'
305+
}}
306+
/>
257307
)
258308
}}
259309
>
260-
{TaskService.cleanHRAgent(msg.content) || ""}
310+
{formatJsonInText(TaskService.cleanHRAgent(msg.content) || "")}
261311
</ReactMarkdown>
262312
</div>
263313
</div>

src/App/src/components/content/streaming/StreamingBufferMessage.tsx

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { CheckmarkCircle20Regular, ArrowTurnDownRightRegular } from '@fluentui/r
66
import ReactMarkdown from "react-markdown";
77
import remarkGfm from "remark-gfm";
88
import rehypePrism from "rehype-prism";
9-
9+
import { formatJsonInText } from "@/utils/jsonFormatter";
10+
1011
interface StreamingBufferMessageProps {
1112
streamingMessageBuffer: string;
1213
isStreaming?: boolean;
1314
}
14-
15+
1516
// Convert to a proper React component instead of a function
1617
const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
1718
streamingMessageBuffer,
@@ -21,7 +22,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
2122
const [shouldFade, setShouldFade] = useState<boolean>(false);
2223
const contentRef = useRef<HTMLDivElement>(null);
2324
const prevBufferLength = useRef<number>(0);
24-
25+
2526
// Trigger fade effect when new content is being streamed
2627
useEffect(() => {
2728
if (isStreaming && streamingMessageBuffer.length > prevBufferLength.current) {
@@ -32,16 +33,18 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
3233
}
3334
prevBufferLength.current = streamingMessageBuffer.length;
3435
}, [streamingMessageBuffer, isStreaming]);
35-
36+
3637
// Auto-scroll to bottom when streaming
3738
useEffect(() => {
3839
if (isStreaming && !isExpanded && contentRef.current) {
3940
contentRef.current.scrollTop = contentRef.current.scrollHeight;
4041
}
4142
}, [streamingMessageBuffer, isStreaming, isExpanded]);
42-
43+
4344
if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null;
44-
45+
46+
const formattedBuffer = formatJsonInText(streamingMessageBuffer);
47+
4548
return (
4649
<div style={{
4750
maxWidth: '800px',
@@ -90,7 +93,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
9093
AI Thinking Process
9194
</span>
9295
</div>
93-
96+
9497
<Button
9598
appearance="secondary"
9699
size="small"
@@ -106,7 +109,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
106109
{isExpanded ? 'Hide' : 'Details'}
107110
</Button>
108111
</div>
109-
112+
110113
{/* Content area - collapsed state */}
111114
{!isExpanded && (
112115
<div
@@ -132,7 +135,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
132135
pointerEvents: 'none',
133136
zIndex: 1
134137
}} />
135-
138+
136139
<div style={{
137140
display: 'flex',
138141
alignItems: 'flex-end',
@@ -175,20 +178,36 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
175178
onMouseLeave={(e) => {
176179
e.currentTarget.style.textDecoration = 'none';
177180
}}
178-
/>
179-
),
180-
p: ({ node, ...props }) => (
181-
<p {...props} style={{ margin: '0 0 8px 0' }} />
182-
)
183-
}}
181+
/>
182+
),
183+
184+
p: ({ node, ...props }) => (
185+
<p {...props} style={{ margin: '0 0 8px 0' }} />
186+
),
187+
188+
img: ({ node, ...props }) => (
189+
<img
190+
{...props}
191+
style={{
192+
maxWidth: '100%',
193+
width: '100%',
194+
height: 'auto',
195+
objectFit: 'contain', // resize, don't crop
196+
display: 'block',
197+
borderRadius: '8px',
198+
marginTop: '8px'
199+
}}
200+
/>
201+
)
202+
}}
184203
>
185-
{streamingMessageBuffer}
204+
{formattedBuffer}
186205
</ReactMarkdown>
187206
</div>
188207
</div>
189208
</div>
190209
)}
191-
210+
192211
{/* Content area - expanded state */}
193212
{isExpanded && (
194213
<div style={{
@@ -199,32 +218,47 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
199218
remarkPlugins={[remarkGfm]}
200219
rehypePlugins={[rehypePrism]}
201220
components={{
202-
a: ({ node, ...props }) => (
203-
<a
204-
{...props}
205-
style={{
206-
color: 'var(--colorNeutralBrandForeground1)',
207-
textDecoration: 'none'
208-
}}
209-
onMouseEnter={(e) => {
210-
e.currentTarget.style.textDecoration = 'underline';
211-
}}
212-
onMouseLeave={(e) => {
213-
e.currentTarget.style.textDecoration = 'none';
214-
}}
215-
/>
216-
)
217-
}}
221+
a: ({ node, ...props }) => (
222+
<a
223+
{...props}
224+
style={{
225+
color: 'var(--colorNeutralBrandForeground1)',
226+
textDecoration: 'none'
227+
}}
228+
onMouseEnter={(e) => {
229+
e.currentTarget.style.textDecoration = 'underline';
230+
}}
231+
onMouseLeave={(e) => {
232+
e.currentTarget.style.textDecoration = 'none';
233+
}}
234+
/>
235+
),
236+
237+
img: ({ node, ...props }) => (
238+
<img
239+
{...props}
240+
style={{
241+
maxWidth: '100%',
242+
width: '100%',
243+
height: 'auto',
244+
objectFit: 'contain', // no cropping
245+
display: 'block',
246+
borderRadius: '8px',
247+
marginTop: '8px'
248+
}}
249+
/>
250+
)
251+
}}
218252
>
219-
{streamingMessageBuffer}
253+
{formattedBuffer}
220254
</ReactMarkdown>
221255
</div>
222256
)}
223257
</div>
224258
</div>
225259
);
226260
};
227-
261+
228262
const MemoizedStreamingBufferMessage = React.memo(StreamingBufferMessage);
229263
MemoizedStreamingBufferMessage.displayName = 'StreamingBufferMessage';
230264
export default MemoizedStreamingBufferMessage;

src/App/src/components/content/streaming/StreamingPlanResponse.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ const useStyles = makeStyles({
6161
borderRadius: '8px',
6262
fontSize: '14px',
6363
lineHeight: '1.5',
64-
wordWrap: 'break-word'
64+
wordWrap: 'break-word',
65+
marginLeft: '48px',
66+
boxSizing: 'border-box'
6567
},
6668
factsSection: {
6769
backgroundColor: 'var(--colorNeutralBackground2)',

0 commit comments

Comments
 (0)