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> = ({ const AnimatedMarkdown: React.FC<AnimatedMarkdownProps> = ({
content, content,
animation = 'fadeIn', animation = 'fadeIn',
animationDuration = 700, animationDuration = 300,
animationTimingFunction = 'ease-in-out', animationTimingFunction = 'ease-in-out',
isStreamFinished = true, isStreamFinished = true,
stripFormatting = false, stripFormatting = false,
className className
}) => { }) => {
const optimizedContent = useMemo(() => { const optimizedContent = useMemo(() => {
@ -48,11 +49,7 @@ const AnimatedMarkdown: React.FC<AnimatedMarkdownProps> = ({
components={components} components={components}
// remarkPlugins are used to extend or modify the Markdown parsing behavior. // remarkPlugins are used to extend or modify the Markdown parsing behavior.
// Here, remarkGfm enables GitHub Flavored Markdown features (like tables, strikethrough, task lists). // Here, remarkGfm enables GitHub Flavored Markdown features (like tables, strikethrough, task lists).
remarkPlugins={remarkPlugins} 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}
>
{optimizedContent} {optimizedContent}
</ReactMarkdown> </ReactMarkdown>
</div> </div>

View File

@ -5,6 +5,15 @@ import { cva } from 'class-variance-authority';
import { StreamingMessageCode } from '../../../streaming/StreamingMessageCode'; import { StreamingMessageCode } from '../../../streaming/StreamingMessageCode';
import { cn } from '@/lib/classMerge'; 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 = { type MarkdownComponentProps = {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
@ -26,8 +35,16 @@ export const ParagraphComponent: React.FC<MarkdownComponentProps> = ({
style, style,
...rest ...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 ( return (
<p style={style} className={className}> <p style={style} className={className} data-testid="paragraph-component">
{animateTokenizedText(children, rest)} {animateTokenizedText(children, rest)}
</p> </p>
); );
@ -218,7 +235,7 @@ export const UnorderedListComponent: React.FC<MarkdownComponentProps> = ({
...rest ...rest
}) => { }) => {
return ( 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} {children}
</ul> </ul>
); );
@ -231,7 +248,10 @@ export const OrderedListComponent: React.FC<MarkdownComponentProps & { start?: n
start start
}) => { }) => {
return ( 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} {children}
</ol> </ol>
); );
@ -248,13 +268,15 @@ export const ListItemComponent: React.FC<MarkdownComponentProps> = ({
style={style} style={style}
className={cn( className={cn(
className, 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, { {animateTokenizedText(children, rest)}
...rest,
doNotAnimateInitialText: true
})}
</li> </li>
); );
}; };

View File

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

View File

@ -10,7 +10,7 @@
@keyframes buster-blurIn { @keyframes buster-blurIn {
from { from {
opacity: 0; opacity: 0;
filter: blur(4px); filter: blur(3px);
} }
to { to {
opacity: 1; opacity: 1;
@ -71,7 +71,10 @@
} }
@keyframes buster-bounceIn { @keyframes buster-bounceIn {
0%, 40%, 80%, 100% { 0%,
40%,
80%,
100% {
transform: translateY(0); transform: translateY(0);
} }
20% { 20% {
@ -83,7 +86,8 @@
} }
@keyframes buster-elastic { @keyframes buster-elastic {
0%, 100% { 0%,
100% {
transform: scale(1); transform: scale(1);
} }
10% { 10% {

View File

@ -82,7 +82,7 @@ const actualTokenArray = [
{ {
token: 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. ", "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. ', token: 'The customer is identified by `customerid` which is a unique identifier. ',
@ -91,11 +91,11 @@ const actualTokenArray = [
{ {
token: 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. ', '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', 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', // token: '## PAUSE 400ms seconds\n\n',
@ -198,13 +198,7 @@ const StreamingDemo: React.FC<{ animation: MarkdownAnimation }> = ({ animation }
return ( return (
<div className="flex w-full space-y-4 space-x-4"> <div className="flex w-full space-y-4 space-x-4">
<div className="w-1/2"> <div className="w-1/2">
<AppMarkdownStreaming <AppMarkdownStreaming content={output} isStreamFinished={isStreamFinished} />
content={output}
isStreamFinished={isStreamFinished}
animation={animation}
animationDuration={700}
animationTimingFunction="ease-in-out"
/>
</div> </div>
<div className="flex w-1/2 flex-col space-y-2 rounded-md border border-gray-200 p-4"> <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> <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 <AppMarkdownStreaming
content={output} content={output}
isStreamFinished={isStreamFinished} isStreamFinished={isStreamFinished}
animation="fadeIn" animation="blurIn"
animationDuration={700} animationDuration={700}
animationTimingFunction="ease-in-out" animationTimingFunction="ease-in-out"
/> />

View File

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