changes
This commit is contained in:
@@ -7,28 +7,6 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Streaming cursor that blinks at the end */
|
||||
.streamingCursor {
|
||||
display: inline-block;
|
||||
width: 0.5em;
|
||||
height: 1em;
|
||||
background-color: currentColor;
|
||||
margin-left: 2px;
|
||||
animation: blink 1s step-end infinite;
|
||||
vertical-align: text-bottom;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.preWrapper {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Anchor, Blockquote, Code, List, Text, Title } from '@mantine/core';
|
||||
import { StreamingText } from './StreamingText';
|
||||
import classes from './MarkdownMessage.module.css';
|
||||
|
||||
interface MarkdownMessageProps {
|
||||
@@ -8,6 +9,18 @@ interface MarkdownMessageProps {
|
||||
}
|
||||
|
||||
export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessageProps) {
|
||||
// While streaming, render plain text with fade-in animation
|
||||
// Once done, render full markdown
|
||||
if (isStreaming) {
|
||||
return (
|
||||
<div className={classes.markdown}>
|
||||
<Text size="sm" style={{ lineHeight: 1.6 }}>
|
||||
<StreamingText content={content} isStreaming={isStreaming} />
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.markdown}>
|
||||
<ReactMarkdown
|
||||
@@ -75,7 +88,6 @@ export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessag
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
{isStreaming && <span className={classes.streamingCursor} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.streamingText {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: textFadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes textFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import classes from './StreamingText.module.css';
|
||||
|
||||
interface StreamingTextProps {
|
||||
content: string;
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders text with a smooth fade-in animation for new chunks.
|
||||
* Splits content into "committed" (already visible) and "new" (fading in) portions.
|
||||
*/
|
||||
export function StreamingText({ content, isStreaming }: StreamingTextProps) {
|
||||
const [committedLength, setCommittedLength] = useState(0);
|
||||
const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// When content grows, schedule the new content to become "committed" after animation
|
||||
useEffect(() => {
|
||||
if (content.length > committedLength) {
|
||||
// Clear any pending timeout
|
||||
if (animationTimeoutRef.current) {
|
||||
clearTimeout(animationTimeoutRef.current);
|
||||
}
|
||||
|
||||
// After the fade-in animation completes, commit the new content
|
||||
animationTimeoutRef.current = setTimeout(() => {
|
||||
setCommittedLength(content.length);
|
||||
}, 300); // Match CSS animation duration
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationTimeoutRef.current) {
|
||||
clearTimeout(animationTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [content, committedLength]);
|
||||
|
||||
// When streaming stops, immediately commit all content
|
||||
useEffect(() => {
|
||||
if (!isStreaming) {
|
||||
setCommittedLength(content.length);
|
||||
}
|
||||
}, [isStreaming, content.length]);
|
||||
|
||||
// Reset when content is cleared (new message)
|
||||
useEffect(() => {
|
||||
if (content.length === 0) {
|
||||
setCommittedLength(0);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const committedText = content.slice(0, committedLength);
|
||||
const newText = content.slice(committedLength);
|
||||
|
||||
return (
|
||||
<span className={classes.streamingText}>
|
||||
<span>{committedText}</span>
|
||||
{newText && <span className={classes.fadeIn}>{newText}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user