From d437535d50dd7d34fd6330ad56cefbde77b42c41 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 21 Jul 2025 11:34:27 -0600 Subject: [PATCH] update to streaming --- .../AnimatedMarkdown/AnimatedMarkdown.tsx | 9 ++--- .../AnimatedMarkdown/MarkdownComponent.tsx | 40 ++++++++++++++----- .../AnimatedMarkdown/TokenizedText.tsx | 1 + .../AnimatedMarkdown/animations.css | 40 ++++++++++--------- .../AppMarkdownStreaming.stories.tsx | 16 +++----- .../AppMarkdownStreaming.tsx | 12 +++--- 6 files changed, 68 insertions(+), 50 deletions(-) diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimatedMarkdown.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimatedMarkdown.tsx index e903f1b0c..e11ce56ad 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimatedMarkdown.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimatedMarkdown.tsx @@ -24,10 +24,11 @@ const remarkPlugins = [remarkGfm]; const AnimatedMarkdown: React.FC = ({ 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 = ({ 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} diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx index 3d6203239..a9413fade 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx @@ -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 = ({ 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 ( -

+

{animateTokenizedText(children, rest)}

); @@ -218,7 +235,7 @@ export const UnorderedListComponent: React.FC = ({ ...rest }) => { return ( -
    +
      {children}
    ); @@ -231,7 +248,10 @@ export const OrderedListComponent: React.FC { return ( -
      +
        {children}
      ); @@ -248,13 +268,15 @@ export const ListItemComponent: React.FC = ({ 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)} ); }; diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx index 93f600c22..2f78c47fe 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx @@ -40,6 +40,7 @@ const TokenizedText: React.FC = React.memo( (chunk: string, index: number) => ( 0 ? 'whitespace-pre-wrap' : ''} style={doNotAnimateInitialText && index === 0 ? {} : animationStyle}> {chunk} diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animations.css b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animations.css index f3763dc9d..0d5a2a0c7 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animations.css +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animations.css @@ -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); -} \ No newline at end of file +} diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx index 20d408ed6..2c632ba83 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx @@ -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 (
      - +

      ACTUAL OUTPUT FROM LLM

      @@ -482,7 +476,7 @@ export const ParagraphToListTransition: Story = { diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.tsx index 64c6e3fe7..061af6c6e 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AppMarkdownStreaming.tsx @@ -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 LLM’s 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,