Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/App/src/components/content/PlanChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
{renderAgentMessages(agentMessages, undefined, undefined, finalResultRef)}

{showProcessingPlanSpinner && renderPlanExecutionMessage()}
{/* Streaming plan updates */}
{showBufferingText && (
{/* Streaming plan updates — hidden while an approval prompt is pending so
the approval action is presented at the appropriate step instead of
after the thinking process visibly completes. */}
{showBufferingText && !showApprovalButtons && (
<StreamingBufferMessage
streamingMessageBuffer={streamingMessageBuffer}
isStreaming={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Body1, Tag, makeStyles, tokens, Button } from "@fluentui/react-componen
import { TaskService } from "@/store";
import { PersonRegular, ArrowDownloadRegular } from "@fluentui/react-icons";
import { getAgentIcon, getAgentDisplayName } from '@/utils/agentIconUtils';
import { formatJsonInText } from '@/utils/jsonFormatter';

interface StreamingAgentMessageProps {
agentMessages: AgentMessageData[];
Expand Down Expand Up @@ -82,6 +83,9 @@ const useStyles = makeStyles({
backgroundColor: 'var(--colorNeutralBackground2)',
color: 'var(--colorNeutralForeground1)',
maxWidth: '100%',
width: '100%',
boxSizing: 'border-box',
overflowX: 'hidden',
alignSelf: 'flex-start',

},
Expand Down Expand Up @@ -219,10 +223,10 @@ const renderAgentMessages = (
/>
),
img: ({ node: _imgNode, ...props }) => (
<div style={{ position: 'relative', display: 'inline-block', marginTop: '8px' }}>
<div style={{ position: 'relative', display: 'block', width: '100%', maxWidth: '100%', marginTop: '8px', overflow: 'hidden' }}>
<img
{...props}
style={{ maxWidth: '100%', borderRadius: '8px', display: 'block' }}
style={{ display: 'block', width: '100%', maxWidth: '100%', height: 'auto', borderRadius: '8px' }}
/>
<Button
appearance="subtle"
Expand All @@ -239,13 +243,27 @@ const renderAgentMessages = (
color: 'white',
borderRadius: '4px',
}}
onClick={() => {
onClick={async () => {
const url = props.src;
if (url) {
if (!url) return;
const filename = `ad-image-${Date.now()}.png`;
try {
const response = await fetch(url, { mode: 'cors' });
if (!response.ok) throw new Error(`Failed to fetch image (${response.status})`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
} catch (err) {
// Fallback: trigger direct download (works for same-origin or CORS-enabled URLs)
const link = document.createElement('a');
link.href = url;
link.download = `ad-image-${Date.now()}.png`;
link.target = '_blank';
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
Expand All @@ -254,10 +272,42 @@ const renderAgentMessages = (
title="Download image"
/>
</div>
),
p: ({ node: _pNode, ...props }) => (
<p {...props} style={{ margin: '0 0 8px 0' }} />
),
h1: ({ node: _hNode, ...props }) => (
<h1 {...props} style={{ fontSize: '20px', fontWeight: 600, margin: '16px 0 8px 0', lineHeight: '1.3' }} />
),
h2: ({ node: _hNode, ...props }) => (
<h2 {...props} style={{ fontSize: '17px', fontWeight: 600, margin: '14px 0 8px 0', lineHeight: '1.3' }} />
),
h3: ({ node: _hNode, ...props }) => (
<h3 {...props} style={{ fontSize: '15px', fontWeight: 600, margin: '12px 0 6px 0', lineHeight: '1.3' }} />
),
ul: ({ node: _ulNode, ...props }) => (
<ul {...props} style={{ margin: '8px 0', paddingLeft: '24px' }} />
),
ol: ({ node: _olNode, ...props }) => (
<ol {...props} style={{ margin: '8px 0', paddingLeft: '24px' }} />
),
li: ({ node: _liNode, ...props }) => (
<li {...props} style={{ margin: '4px 0', lineHeight: '1.5' }} />
),
blockquote: ({ node: _bqNode, ...props }) => (
<blockquote
{...props}
style={{
margin: '8px 0',
padding: '8px 12px',
borderLeft: '3px solid var(--colorNeutralStroke1)',
color: 'var(--colorNeutralForeground2)'
}}
/>
)
}}
>
{TaskService.cleanHRAgent(msg.content) || ""}
{formatJsonInText(TaskService.cleanHRAgent(msg.content) || "")}
</ReactMarkdown>
</div>
</div>
Expand Down
104 changes: 69 additions & 35 deletions src/App/src/components/content/streaming/StreamingBufferMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { CheckmarkCircle20Regular, ArrowTurnDownRightRegular } from '@fluentui/r
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypePrism from "rehype-prism";

import { formatJsonInText } from "@/utils/jsonFormatter";

interface StreamingBufferMessageProps {
streamingMessageBuffer: string;
isStreaming?: boolean;
}

// Convert to a proper React component instead of a function
const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
streamingMessageBuffer,
Expand All @@ -21,7 +22,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
const [shouldFade, setShouldFade] = useState<boolean>(false);
const contentRef = useRef<HTMLDivElement>(null);
const prevBufferLength = useRef<number>(0);

// Trigger fade effect when new content is being streamed
useEffect(() => {
if (isStreaming && streamingMessageBuffer.length > prevBufferLength.current) {
Expand All @@ -32,16 +33,18 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
}
prevBufferLength.current = streamingMessageBuffer.length;
}, [streamingMessageBuffer, isStreaming]);

// Auto-scroll to bottom when streaming
useEffect(() => {
if (isStreaming && !isExpanded && contentRef.current) {
contentRef.current.scrollTop = contentRef.current.scrollHeight;
}
}, [streamingMessageBuffer, isStreaming, isExpanded]);

if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null;


const formattedBuffer = formatJsonInText(streamingMessageBuffer);

return (
<div style={{
maxWidth: '800px',
Expand Down Expand Up @@ -90,7 +93,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
AI Thinking Process
</span>
</div>

<Button
appearance="secondary"
size="small"
Expand All @@ -106,7 +109,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
{isExpanded ? 'Hide' : 'Details'}
</Button>
</div>

{/* Content area - collapsed state */}
{!isExpanded && (
<div
Expand All @@ -132,7 +135,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
pointerEvents: 'none',
zIndex: 1
}} />

<div style={{
display: 'flex',
alignItems: 'flex-end',
Expand Down Expand Up @@ -175,20 +178,36 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = 'none';
}}
/>
),
p: ({ node, ...props }) => (
<p {...props} style={{ margin: '0 0 8px 0' }} />
)
}}
/>
),

p: ({ node, ...props }) => (
<p {...props} style={{ margin: '0 0 8px 0' }} />
),

img: ({ node, ...props }) => (
<img
{...props}
style={{
maxWidth: '100%',
width: '100%',
height: 'auto',
objectFit: 'contain', // resize, don't crop
display: 'block',
borderRadius: '8px',
marginTop: '8px'
}}
/>
)
}}
>
{streamingMessageBuffer}
{formattedBuffer}
</ReactMarkdown>
</div>
</div>
</div>
)}

{/* Content area - expanded state */}
{isExpanded && (
<div style={{
Expand All @@ -199,32 +218,47 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypePrism]}
components={{
a: ({ node, ...props }) => (
<a
{...props}
style={{
color: 'var(--colorNeutralBrandForeground1)',
textDecoration: 'none'
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = 'underline';
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = 'none';
}}
/>
)
}}
a: ({ node, ...props }) => (
<a
{...props}
style={{
color: 'var(--colorNeutralBrandForeground1)',
textDecoration: 'none'
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = 'underline';
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = 'none';
}}
/>
),

img: ({ node, ...props }) => (
<img
{...props}
style={{
maxWidth: '100%',
width: '100%',
height: 'auto',
objectFit: 'contain', // no cropping
display: 'block',
borderRadius: '8px',
marginTop: '8px'
}}
/>
)
}}
>
{streamingMessageBuffer}
{formattedBuffer}
</ReactMarkdown>
</div>
)}
</div>
</div>
);
};

const MemoizedStreamingBufferMessage = React.memo(StreamingBufferMessage);
MemoizedStreamingBufferMessage.displayName = 'StreamingBufferMessage';
export default MemoizedStreamingBufferMessage;
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ const useStyles = makeStyles({
borderRadius: '8px',
fontSize: '14px',
lineHeight: '1.5',
wordWrap: 'break-word'
wordWrap: 'break-word',
marginLeft: '48px',
boxSizing: 'border-box'
},
factsSection: {
backgroundColor: 'var(--colorNeutralBackground2)',
Expand Down
Loading
Loading