voxblog/apps/admin/STREAMING_FIX.md
Ender 197fd69ce3 fix: resolve streaming content accumulation issue
- Fixed stale closure bug in streaming content by using useRef to properly accumulate content instead of relying on state closure
- Added auto-scrolling functionality to keep latest content visible during generation
- Implemented smooth scrolling behavior and improved HTML styling for better readability
- Added content buffer reset when starting new generation
- Enhanced documentation with detailed explanation of the closure problem and solution
2025-10-25 21:39:40 +02:00

4.1 KiB

Streaming Content Accumulation Fix

Problem

The streaming content was showing individual tokens instead of the accumulated HTML article because of a stale closure issue.

What Was Happening

// ❌ WRONG - Stale closure
onContent: (data) => {
  onSetStreamingContent(streamingContent + data.delta);
  // streamingContent is captured from the initial render
  // It never updates in this callback!
}

Result: Each delta was added to the INITIAL empty string, not the accumulated content.

Delta 1: "" + "TypeScript" = "TypeScript"
Delta 2: "" + " is" = " is"  // Lost "TypeScript"!
Delta 3: "" + " a" = " a"    // Lost everything!

Solution

Use a ref to track the accumulated content outside the closure:

// ✅ CORRECT - Ref accumulation
const contentBufferRef = useRef<string>('');

onContent: (data) => {
  contentBufferRef.current += data.delta;
  onSetStreamingContent(contentBufferRef.current);
}

Result: Each delta is properly accumulated.

Delta 1: "" + "TypeScript" = "TypeScript"
Delta 2: "TypeScript" + " is" = "TypeScript is"
Delta 3: "TypeScript is" + " a" = "TypeScript is a"

Additional Improvements

1. Auto-Scroll

const streamingBoxRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  if (isGenerating && streamingContent && streamingBoxRef.current) {
    streamingBoxRef.current.scrollTop = streamingBoxRef.current.scrollHeight;
  }
}, [streamingContent, isGenerating]);

Automatically scrolls to show new content as it arrives.

2. Smooth Scrolling

sx={{
  scrollBehavior: 'smooth',
  // ...
}}

Smooth animation when auto-scrolling.

3. Better HTML Styling

'& h2, & h3': { mt: 2, mb: 1 },
'& p': { mb: 1 },
'& ul, & ol': { pl: 3, mb: 1 },
'& li': { mb: 0.5 },

Proper spacing for HTML elements.

Technical Explanation

Closure Problem

In JavaScript, when you create a callback function, it "closes over" the variables from its surrounding scope. Those variables are captured at the time the function is created.

const [content, setContent] = useState('');

// This callback is created once when component mounts
const callback = () => {
  console.log(content); // Always logs the INITIAL value!
};

Why Refs Work

Refs are mutable objects that persist across renders:

const ref = useRef('');

// This always reads the CURRENT value
const callback = () => {
  console.log(ref.current); // Gets the latest value!
};

Before vs After

Before (Broken)

User sees:
"TypeScript"
" is"
" a"
"powerful"
...individual tokens, no accumulation

After (Fixed)

User sees:
"TypeScript"
"TypeScript is"
"TypeScript is a"
"TypeScript is a powerful"
...proper accumulation, rendered as HTML

Files Changed

✅ apps/admin/src/components/steps/StepGenerate.tsx
   - Added contentBufferRef for accumulation
   - Added streamingBoxRef for auto-scroll
   - Added useEffect for auto-scroll
   - Fixed onContent callback to use ref
   - Added smooth scroll behavior

Testing

  1. Start generation
  2. Watch the "Live Generation" box
  3. Should see:
    • Proper HTML rendering (headings, paragraphs, lists)
    • Content accumulating (not individual tokens)
    • Auto-scroll to bottom
    • Smooth animations
    • Pulsing blue border

Common React Pitfalls

This is a classic React pitfall that catches many developers:

Don't Do This

const [value, setValue] = useState(0);

setInterval(() => {
  setValue(value + 1); // Always adds to initial value!
}, 1000);

Do This Instead

const [value, setValue] = useState(0);

setInterval(() => {
  setValue(prev => prev + 1); // Functional update
}, 1000);

// OR use a ref
const valueRef = useRef(0);
setInterval(() => {
  valueRef.current += 1;
  setValue(valueRef.current);
}, 1000);

Conclusion

The fix ensures that streaming content properly accumulates and displays as formatted HTML, providing a smooth, professional user experience with auto-scrolling and proper rendering.

Status: Fixed and working!