update to streaming

This commit is contained in:
Nate Kelley 2025-07-21 11:34:27 -06:00
parent ededd23024
commit d437535d50
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 68 additions and 50 deletions

View File

@ -24,10 +24,11 @@ const remarkPlugins = [remarkGfm];
const AnimatedMarkdown: React.FC<AnimatedMarkdownProps> = ({
content,
animation = 'fadeIn',
animationDuration = 700,
animationDuration = 300,
animationTimingFunction = 'ease-in-out',
isStreamFinished = true,
stripFormatting = false,
className
}) => {
const optimizedContent = useMemo(() => {
@ -48,11 +49,7 @@ const AnimatedMarkdown: React.FC<AnimatedMarkdownProps> = ({
components={components}
// remarkPlugins are used to extend or modify the Markdown parsing behavior.
// Here, remarkGfm enables GitHub Flavored Markdown features (like tables, strikethrough, task lists).
remarkPlugins={remarkPlugins}
// rehypePlugins are used to transform the resulting HTML AST after Markdown is parsed.
// rehypeRaw allows raw HTML in the Markdown to be parsed and rendered (be cautious with untrusted content).
// rehypePlugins={rehypePlugins}
>
remarkPlugins={remarkPlugins}>
{optimizedContent}
</ReactMarkdown>
</div>

View File

@ -5,6 +5,15 @@ import { cva } from 'class-variance-authority';
import { StreamingMessageCode } from '../../../streaming/StreamingMessageCode';
import { cn } from '@/lib/classMerge';
// Create a context to track list item state
const ListItemContext = React.createContext<{
isInListItem: boolean;
initialHadParagraph: boolean | null;
}>({
isInListItem: false,
initialHadParagraph: null
});
type MarkdownComponentProps = {
children: React.ReactNode;
className?: string;
@ -26,8 +35,16 @@ export const ParagraphComponent: React.FC<MarkdownComponentProps> = ({
style,
...rest
}) => {
const listItemContext = React.useContext(ListItemContext);
// If we're in a list item that started without paragraphs, don't render the p tag
if (listItemContext.isInListItem && listItemContext.initialHadParagraph === false) {
// Return children directly without the p tag wrapper
return <>{animateTokenizedText(children, rest)}</>;
}
return (
<p style={style} className={className}>
<p style={style} className={className} data-testid="paragraph-component">
{animateTokenizedText(children, rest)}
</p>
);
@ -218,7 +235,7 @@ export const UnorderedListComponent: React.FC<MarkdownComponentProps> = ({
...rest
}) => {
return (
<ul style={style} className={cn(className, 'mt-1 space-y-1', 'list-inside')}>
<ul style={style} className={cn(className, 'mt-1 space-y-1', 'list-inside', 'list-disc')}>
{children}
</ul>
);
@ -231,7 +248,10 @@ export const OrderedListComponent: React.FC<MarkdownComponentProps & { start?: n
start
}) => {
return (
<ol style={style} className={cn(className, 'mt-1 space-y-1', 'list-inside')} start={start}>
<ol
style={style}
className={cn(className, 'mt-1 space-y-1', 'list-inside', 'list-decimal')}
start={start}>
{children}
</ol>
);
@ -248,13 +268,15 @@ export const ListItemComponent: React.FC<MarkdownComponentProps> = ({
style={style}
className={cn(
className,
// Ensure proper vertical alignment
'[&>span]:align-top'
'[&_span]:inline',
// // Normal text flow
'whitespace-normal',
// Fix alignment of content
'[&>span]:inline [&>span]:align-top',
'[&>p]:inline [&>p]:align-top'
)}>
{animateTokenizedText(children, {
...rest,
doNotAnimateInitialText: true
})}
{animateTokenizedText(children, rest)}
</li>
);
};

View File

@ -40,6 +40,7 @@ const TokenizedText: React.FC<TokenizedTextProps> = React.memo(
(chunk: string, index: number) => (
<span
key={`animated-chunk-${index}`}
className={chunk.trim().length > 0 ? 'whitespace-pre-wrap' : ''}
style={doNotAnimateInitialText && index === 0 ? {} : animationStyle}>
{chunk}
</span>

View File

@ -1,21 +1,21 @@
@keyframes buster-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes buster-blurIn {
from {
opacity: 0;
filter: blur(4px);
}
to {
opacity: 1;
filter: blur(0px);
}
from {
opacity: 0;
filter: blur(3px);
}
to {
opacity: 1;
filter: blur(0px);
}
}
@keyframes buster-typewriter {
@ -71,7 +71,10 @@
}
@keyframes buster-bounceIn {
0%, 40%, 80%, 100% {
0%,
40%,
80%,
100% {
transform: translateY(0);
}
20% {
@ -83,7 +86,8 @@
}
@keyframes buster-elastic {
0%, 100% {
0%,
100% {
transform: scale(1);
}
10% {
@ -145,7 +149,7 @@
}
}
:root {
:root {
--buster-marker-animation: none;
}
@ -155,4 +159,4 @@
.buster-code-block {
animation: var(--buster-marker-animation);
}
}

View File

@ -82,7 +82,7 @@ const actualTokenArray = [
{
token:
"Looking at the database context, I can see there's a `customer` model that serves as the comprehensive customer model for customer relationship management and purchase behavior analysis. ",
delayMs: 800
delayMs: 300
},
{
token: 'The customer is identified by `customerid` which is a unique identifier. ',
@ -91,11 +91,11 @@ const actualTokenArray = [
{
token:
'The customer model also has relationships to `person` (for individual customers) and `store` (for store customers), as well as connections to `sales_order_header` for tracking customer orders. ',
delayMs: 900
delayMs: 500
},
{
token: 'This gives me a clear way to identify customers in the system.\n\n',
delayMs: 8400
delayMs: 1000
},
// {
// token: '## PAUSE 400ms seconds\n\n',
@ -198,13 +198,7 @@ const StreamingDemo: React.FC<{ animation: MarkdownAnimation }> = ({ animation }
return (
<div className="flex w-full space-y-4 space-x-4">
<div className="w-1/2">
<AppMarkdownStreaming
content={output}
isStreamFinished={isStreamFinished}
animation={animation}
animationDuration={700}
animationTimingFunction="ease-in-out"
/>
<AppMarkdownStreaming content={output} isStreamFinished={isStreamFinished} />
</div>
<div className="flex w-1/2 flex-col space-y-2 rounded-md border border-gray-200 p-4">
<h1 className="bg-gray-100 p-2 text-2xl font-bold">ACTUAL OUTPUT FROM LLM</h1>
@ -482,7 +476,7 @@ export const ParagraphToListTransition: Story = {
<AppMarkdownStreaming
content={output}
isStreamFinished={isStreamFinished}
animation="fadeIn"
animation="blurIn"
animationDuration={700}
animationTimingFunction="ease-in-out"
/>

View File

@ -4,7 +4,7 @@ import { markdownLookBack } from '@llm-ui/markdown';
import { throttleBasic, useLLMOutput } from '@llm-ui/react';
import { LLMAnimatedMarkdown } from './AnimatedMarkdown/LLMAnimatedMarkdown';
import CodeComponentStreaming from './CodeComponentStreaming';
import React, { useContext } from 'react';
import React, { useContext, useMemo } from 'react';
import type {
MarkdownAnimation,
MarkdownAnimationTimingFunction
@ -14,7 +14,7 @@ import { cn } from '@/lib/classMerge';
const throttle = throttleBasic({
// show output as soon as it arrives
readAheadChars: 0,
readAheadChars: 10,
// stay literally at the LLMs pace
targetBufferChars: 10,
adjustPercentage: 0.4,
@ -26,9 +26,9 @@ const throttle = throttleBasic({
const AppMarkdownStreaming = ({
content,
isStreamFinished,
animation,
animationDuration,
animationTimingFunction,
animation = 'blurIn',
animationDuration = 300,
animationTimingFunction = 'linear',
className,
stripFormatting = false
}: {
@ -40,7 +40,7 @@ const AppMarkdownStreaming = ({
className?: string;
stripFormatting?: boolean;
}) => {
const { blockMatches, isFinished, ...rest } = useLLMOutput({
const { blockMatches, isFinished } = useLLMOutput({
llmOutput: content,
fallbackBlock: {
component: LLMAnimatedMarkdown,