diff --git a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx
index 43fe4a1b4..54debd808 100644
--- a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx
+++ b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.stories.tsx
@@ -496,3 +496,249 @@ export const ParagraphToListTransition: Story = {
);
}
};
+
+// Test case for broken up inline code streaming
+const brokenInlineCodeTokens = [
+ {
+ token: 'This paragraph contains several inline code examples. Here is a complete ',
+ delayMs: 300
+ },
+ {
+ token: '`complete_code_block`',
+ delayMs: 200
+ },
+ {
+ token: ' HOLD ',
+ delayMs: 1000
+ },
+ {
+ token: " that arrives in one token. Now let's test broken inline code: ",
+ delayMs: 300
+ },
+ {
+ token: '`br',
+ delayMs: 400
+ },
+ {
+ token: 'oken',
+ delayMs: 2000
+ },
+ {
+ token: '_code',
+ delayMs: 250
+ },
+ {
+ token: '_block',
+ delayMs: 260
+ },
+ {
+ token: '`',
+ delayMs: 650
+ },
+ {
+ token: " where the backticks and content are split. Here's another example with ",
+ delayMs: 300
+ },
+ {
+ token: '`',
+ delayMs: 500
+ },
+ {
+ token: 'useState',
+ delayMs: 200
+ },
+ {
+ token: '`',
+ delayMs: 400
+ },
+ {
+ token: ' and ',
+ delayMs: 150
+ },
+ {
+ token: '`',
+ delayMs: 300
+ },
+ {
+ token: 'useEffect',
+ delayMs: 200
+ },
+ {
+ token: '`',
+ delayMs: 300
+ },
+ {
+ token: ' hooks. Sometimes the content itself is split like ',
+ delayMs: 300
+ },
+ {
+ token: '`',
+ delayMs: 200
+ },
+ {
+ token: 'customer',
+ delayMs: 150
+ },
+ {
+ token: '_all',
+ delayMs: 150
+ },
+ {
+ token: '_time',
+ delayMs: 150
+ },
+ {
+ token: '_clv',
+ delayMs: 150
+ },
+ {
+ token: '`',
+ delayMs: 200
+ },
+ {
+ token: ' and we need to wait for all parts to complete the inline code block.\n\n',
+ delayMs: 400
+ },
+ {
+ token: 'Another paragraph with more examples: ',
+ delayMs: 200
+ },
+ {
+ token: '`',
+ delayMs: 300
+ },
+ {
+ token: 'const',
+ delayMs: 150
+ },
+ {
+ token: ' result',
+ delayMs: 150
+ },
+ {
+ token: ' =',
+ delayMs: 100
+ },
+ {
+ token: ' await',
+ delayMs: 150
+ },
+ {
+ token: ' fetch',
+ delayMs: 150
+ },
+ {
+ token: '()',
+ delayMs: 100
+ },
+ {
+ token: '`',
+ delayMs: 300
+ },
+ {
+ token: ' shows how complex code can be streamed. Mixed with normal ',
+ delayMs: 300
+ },
+ {
+ token: '`simple`',
+ delayMs: 150
+ },
+ {
+ token: ' inline code that arrives complete.\n\n',
+ delayMs: 200
+ },
+ {
+ token: 'Final test case: ',
+ delayMs: 200
+ },
+ {
+ token: '`',
+ delayMs: 400
+ },
+ {
+ token: 'database',
+ delayMs: 200
+ },
+ {
+ token: '.query',
+ delayMs: 150
+ },
+ {
+ token: '("SELECT',
+ delayMs: 150
+ },
+ {
+ token: ' * FROM',
+ delayMs: 150
+ },
+ {
+ token: ' customers',
+ delayMs: 150
+ },
+ {
+ token: ' WHERE',
+ delayMs: 150
+ },
+ {
+ token: ' active',
+ delayMs: 150
+ },
+ {
+ token: ' = true")',
+ delayMs: 200
+ },
+ {
+ token: '`',
+ delayMs: 300
+ },
+ {
+ token: ' demonstrates SQL code streaming.',
+ delayMs: 200
+ },
+ {
+ token: 'HOLD',
+ delayMs: 50000
+ }
+];
+
+export const InlineCodeStreaming: Story = {
+ render: () => {
+ const { isStreamFinished, output } = useStreamTokenArray(brokenInlineCodeTokens);
+
+ return (
+
+
Testing Broken Inline Code Streaming
+
+ Watch how inline code renders when backticks and content are split across multiple stream
+ tokens
+
+
+
+
+
Raw Streaming Content
+
+
+ Stream Status: {isStreamFinished ? '✅ Complete' : '⏳ Streaming...'}
+
+
+ Current length: {output.length} characters
+
+
+
+
+ );
+ }
+};
diff --git a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/LLMAnimatedMarkdown.tsx b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/LLMAnimatedMarkdown.tsx
index f79023718..4a995255f 100644
--- a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/LLMAnimatedMarkdown.tsx
+++ b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/LLMAnimatedMarkdown.tsx
@@ -1,32 +1,51 @@
import type { BlockMatch } from '@llm-ui/react';
import AnimatedMarkdown from '../../typography/AnimatedMarkdown/AnimatedMarkdown';
-import React from 'react';
+import React, { useMemo } from 'react';
import { useAppMarkdownStreaming } from './AppMarkdownStreaming';
+import { findPartialInlineCode } from './inlineCodeHelpers';
type LLMAnimatedMarkdownProps = {
blockMatch: BlockMatch;
};
-export const LLMAnimatedMarkdown: React.FC = ({ blockMatch }) => {
- const markdown = blockMatch.output;
- const {
- animation,
- stripFormatting,
- isThrottleStreamingFinished,
- animationDuration,
- animationTimingFunction
- } = useAppMarkdownStreaming();
+export const LLMAnimatedMarkdown: React.FC = React.memo(
+ ({ blockMatch }) => {
+ const markdown = blockMatch.llmOutput;
+ const {
+ animation,
+ stripFormatting,
- return (
-
- );
-};
+ animationDuration,
+ isStreamFinished,
+ animationTimingFunction
+ } = useAppMarkdownStreaming();
+
+ const optimizedContent = useMemo(() => {
+ const partialMatch = findPartialInlineCode(markdown);
+
+ // If there's a partial inline code match, only show content up to the start of the partial match
+ // This naturally excludes the offending backtick without affecting complete inline code blocks
+ if (partialMatch) {
+ return markdown.slice(0, partialMatch.startIndex);
+ }
+
+ // No partial match, show all content as-is
+ return markdown;
+ }, [markdown]);
+
+ return (
+
+ );
+ }
+);
+
+LLMAnimatedMarkdown.displayName = 'LLMAnimatedMarkdown';
export default LLMAnimatedMarkdown;
diff --git a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/inlineCodeHelpers.test.ts b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/inlineCodeHelpers.test.ts
new file mode 100644
index 000000000..8ad863c68
--- /dev/null
+++ b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/inlineCodeHelpers.test.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect } from 'vitest';
+import { findPartialInlineCode } from './inlineCodeHelpers';
+
+describe('findPartialInlineCode', () => {
+ it('should find partial inline code (unclosed backtick)', () => {
+ // Test case: Text with an unclosed backtick at the end
+ const text = 'This is some text with `unclosed code';
+ const result = findPartialInlineCode(text);
+
+ expect(result).not.toBeNull();
+ expect(result).toEqual({
+ startIndex: 23, // Position of the backtick
+ endIndex: 37, // End of the partial code
+ outputRaw: '`unclosed code'
+ });
+ });
+
+ it('should return null for complete inline code', () => {
+ // Test case: Text with properly closed inline code
+ const text = 'This is some text with `complete code` here';
+ const result = findPartialInlineCode(text);
+
+ // Should return null because the code block is complete (properly closed)
+ expect(result).toBeNull();
+ });
+
+ it('should return null when no inline code is present', () => {
+ // Test case: Text with no backticks at all
+ const text = 'This is just plain text without any code blocks';
+ const result = findPartialInlineCode(text);
+
+ // Should return null because there are no backticks
+ expect(result).toBeNull();
+ });
+});
diff --git a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/inlineCodeHelpers.ts b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/inlineCodeHelpers.ts
new file mode 100644
index 000000000..b981522b0
--- /dev/null
+++ b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/inlineCodeHelpers.ts
@@ -0,0 +1,55 @@
+// Helper functions for inline code detection in streaming markdown
+
+import type { LLMOutputMatcher } from '@llm-ui/react';
+
+/**
+ * Finds complete inline code blocks (text between single backticks)
+ * @returns A function that finds complete inline code in the input
+ */
+export const findCompleteInlineCode = (): LLMOutputMatcher => {
+ return (input: string) => {
+ // Match inline code that is NOT part of a code block (```).
+ // Look for single backticks that are not preceded or followed by additional backticks
+ const regex = /(? {
return useLLMOutput({
llmOutput: content,
- fallbackBlock: {
- component: LLMAnimatedMarkdown,
- lookBack: markdownLookBack()
- },
- blocks: [
- {
- component: CodeComponentStreaming,
- findCompleteMatch: findCompleteCodeBlock(),
- findPartialMatch: findPartialCodeBlock(),
- lookBack: codeBlockLookBack()
- }
- ],
+ fallbackBlock,
+ blocks,
isStreamFinished,
throttle
});
diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/AnimatedMarkdown.tsx b/apps/web/src/components/ui/typography/AnimatedMarkdown/AnimatedMarkdown.tsx
index e11ce56ad..e41ddfdd8 100644
--- a/apps/web/src/components/ui/typography/AnimatedMarkdown/AnimatedMarkdown.tsx
+++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/AnimatedMarkdown.tsx
@@ -8,6 +8,7 @@ import { MarkdownAnimation } from './animation-helpers';
import styles from './AnimatedMarkdown.module.css';
import { cn } from '@/lib/classMerge';
import './animations.css';
+import { useMount } from '../../../../hooks';
export interface AnimatedMarkdownProps {
className?: string;
@@ -28,13 +29,8 @@ const AnimatedMarkdown: React.FC = ({
animationTimingFunction = 'ease-in-out',
isStreamFinished = true,
stripFormatting = false,
-
className
}) => {
- const optimizedContent = useMemo(() => {
- return content;
- }, [content]);
-
const { components } = useMarkdownComponents({
animation,
animationDuration,
@@ -50,7 +46,7 @@ const AnimatedMarkdown: React.FC = ({
// 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}>
- {optimizedContent}
+ {content}
);
diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/MarkdownComponent.tsx b/apps/web/src/components/ui/typography/AnimatedMarkdown/MarkdownComponent.tsx
index 9ce2bc7bf..ced49fe63 100644
--- a/apps/web/src/components/ui/typography/AnimatedMarkdown/MarkdownComponent.tsx
+++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/MarkdownComponent.tsx
@@ -5,6 +5,7 @@ import { cva } from 'class-variance-authority';
import { StreamingMessageCode } from '../../streaming/StreamingMessageCode';
import { cn } from '@/lib/classMerge';
import { useAnimationContext, AnimationProvider } from './AnimationContext';
+import { useMount } from '../../../../hooks';
type MarkdownComponentProps = {
children: React.ReactNode;
diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/index.ts b/apps/web/src/components/ui/typography/AnimatedMarkdown/index.ts
index fa3fa1a0b..c0930f466 100644
--- a/apps/web/src/components/ui/typography/AnimatedMarkdown/index.ts
+++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/index.ts
@@ -1,2 +1 @@
export { default as AnimatedMarkdown } from './AnimatedMarkdown';
-export { default as LLMAnimatedMarkdown } from '../AppMarkdownStreaming/LLMAnimatedMarkdown';
diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx b/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx
index 99a8a4852..c35aa07e9 100644
--- a/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx
+++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx
@@ -23,6 +23,7 @@ import {
BreakComponent,
CodeComponent
} from './MarkdownComponent';
+import { useMount, useWhyDidYouUpdate } from '../../../../hooks';
interface UseMarkdownComponentsProps {
stripFormatting?: boolean;
@@ -139,8 +140,6 @@ export const useMarkdownComponents = ({
),
- // Components WITHOUT animations
- // Components WITH animations (updated)
ul: ({ children, className, style }) => (
{children}