[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71HXH7QXSQB4QTDZ7SP7YXQ":3,"skill-same:01M71HXH7QXSQB4QTDZ7SP7YXQ":114},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":22,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":69,"commitSha":70,"skillDirectory":71,"forksCount":72,"repositoryHtmlUrl":73,"repositoryHomepageUrl":6,"ownerHtmlUrl":74,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":75,"install":76,"resources":89},"01M71HXH7QXSQB4QTDZ7SP7YXQ","llm-streaming-response-handler",null,"Build production LLM streaming UIs with Server-Sent Events, real-time token display, cancellation, error recovery. Handles OpenAI\u002FAnthropic\u002FClaude streaming APIs. Use for chatbots, AI assistants, real-time text generation. Activate on \"LLM streaming\", \"SSE\", \"token stream\", \"chat UI\", \"real-time AI\". NOT for batch processing, non-streaming APIs, or WebSocket bidirectional chat.","JSGforever\u002Fskillguard","JSGforever","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F85541948?v=4","01M71FQD3E132VMR5F4TAYX3YS","01M71FQD4DYW0XJ4XQ4YQH75Y9",2,"GPL-3.0","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002FSKILL.md",[17],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",[23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"streaming","handler","llm streaming response handler","read","server","text","display","react","batch","optimizer","activate","Bash","critical","error recovery","chat ui","time","performance","retry","processing","error","2026-11-10 10:12:30",false,"---\nname: llm-streaming-response-handler\ndescription: Build production LLM streaming UIs with Server-Sent Events, real-time token display, cancellation, error recovery. Handles OpenAI\u002FAnthropic\u002FClaude streaming APIs. Use for chatbots, AI assistants,\n  real-time text generation. Activate on \"LLM streaming\", \"SSE\", \"token stream\", \"chat UI\", \"real-time AI\". NOT for batch processing, non-streaming APIs, or WebSocket bidirectional chat.\nallowed-tools: Read,Write,Edit,Bash(npm:*)\nmetadata:\n  category: AI & Machine Learning\n  tags:\n  - llm\n  - streaming\n  - response\n  - llm-streaming\n  - sse\n  pairs-with:\n  - skill: chatbot-analytics\n    reason: Streaming metrics (time-to-first-token, throughput) are key analytics for chatbot performance\n  - skill: react-performance-optimizer\n    reason: Real-time token rendering requires React performance optimization for smooth display\n  - skill: error-handling-patterns\n    reason: Stream interruption recovery and retry logic are critical error handling concerns\n  - skill: websocket-streaming\n    reason: WebSocket transport is an alternative to SSE for bidirectional LLM streaming\n---\n\n# LLM Streaming Response Handler\n\nExpert in building production-grade streaming interfaces for LLM responses that feel instant and responsive.\n\n## When to Use\n\n✅ **Use for**:\n- Chat interfaces with typing animation\n- Real-time AI assistants\n- Code generation with live preview\n- Document summarization with progressive display\n- Any UI where users expect immediate feedback from LLMs\n\n❌ **NOT for**:\n- Batch document processing (no user watching)\n- APIs that don't support streaming\n- WebSocket-based bidirectional chat (use Socket.IO)\n- Simple request\u002Fresponse (fetch is fine)\n\n## Quick Decision Tree\n\n```\nDoes your LLM interaction:\n├── Need immediate visual feedback? → Streaming\n├── Display long-form content (&gt;100 words)? → Streaming\n├── User expects typewriter effect? → Streaming\n├── Short response (&lt;50 words)? → Regular fetch\n└── Background processing? → Regular fetch\n```\n\n---\n\n## Technology Selection\n\n### Server-Sent Events (SSE) - Recommended\n\n**Why SSE over WebSockets for LLM streaming**:\n- **Simplicity**: HTTP-based, works with existing infrastructure\n- **Auto-reconnect**: Built-in reconnection logic\n- **Firewall-friendly**: Easier than WebSockets through proxies\n- **One-way perfect**: LLMs only stream server → client\n\n**Timeline**:\n- 2015-2020: WebSockets for everything\n- 2020: SSE adoption for streaming APIs\n- 2023+: SSE standard for LLM streaming (OpenAI, Anthropic)\n- 2024: Vercel AI SDK popularizes SSE patterns\n\n### Streaming APIs\n\n| Provider | Streaming Method | Response Format |\n|----------|------------------|-----------------|\n| OpenAI | SSE | `data: {\"choices\":[{\"delta\":{\"content\":\"token\"}}]}` |\n| Anthropic | SSE | `data: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"token\"}}` |\n| Claude (API) | SSE | `data: {\"delta\":{\"text\":\"token\"}}` |\n| Vercel AI SDK | SSE | Normalized across providers |\n\n---\n\n## Common Anti-Patterns\n\n### Anti-Pattern 1: Buffering Before Display\n\n**Novice thinking**: \"Collect all tokens, then show complete response\"\n\n**Problem**: Defeats the entire purpose of streaming.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Waits for entire response before showing anything\nconst response = await fetch('\u002Fapi\u002Fchat', { method: 'POST', body: prompt });\nconst fullText = await response.text();\nsetMessage(fullText); \u002F\u002F User sees nothing until done\n```\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Display tokens as they arrive\nconst response = await fetch('\u002Fapi\u002Fchat', {\n  method: 'POST',\n  body: JSON.stringify({ prompt })\n});\n\nconst reader = response.body.getReader();\nconst decoder = new TextDecoder();\n\nwhile (true) {\n  const { done, value } = await reader.read();\n  if (done) break;\n\n  const chunk = decoder.decode(value);\n  const lines = chunk.split('\\n').filter(line => line.trim());\n\n  for (const line of lines) {\n    if (line.startsWith('data: ')) {\n      const data = JSON.parse(line.slice(6));\n      setMessage(prev => prev + data.content); \u002F\u002F Update immediately\n    }\n  }\n}\n```\n\n**Timeline**:\n- Pre-2023: Many apps buffered entire response\n- 2023+: Token-by-token display expected\n\n---\n\n### Anti-Pattern 2: No Stream Cancellation\n\n**Problem**: User can't stop generation, wasting tokens and money.\n\n**Symptom**: \"Stop\" button doesn't work or doesn't exist.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ AbortController for cancellation\nconst [abortController, setAbortController] = useState\u003CAbortController | null>(null);\n\nconst streamResponse = async () => {\n  const controller = new AbortController();\n  setAbortController(controller);\n\n  try {\n    const response = await fetch('\u002Fapi\u002Fchat', {\n      signal: controller.signal,\n      method: 'POST',\n      body: JSON.stringify({ prompt })\n    });\n\n    \u002F\u002F Stream handling...\n  } catch (error) {\n    if (error.name === 'AbortError') {\n      console.log('Stream cancelled by user');\n    }\n  } finally {\n    setAbortController(null);\n  }\n};\n\nconst cancelStream = () => {\n  abortController?.abort();\n};\n\nreturn (\n  \u003Cbutton onClick={cancelStream} disabled={!abortController}>\n    Stop Generating\n  \u003C\u002Fbutton>\n);\n```\n\n---\n\n### Anti-Pattern 3: No Error Recovery\n\n**Problem**: Stream fails mid-response, user sees partial text with no indication of failure.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Error states and recovery\nconst [streamState, setStreamState] = useState\u003C'idle' | 'streaming' | 'error' | 'complete'>('idle');\nconst [errorMessage, setErrorMessage] = useState\u003Cstring | null>(null);\n\ntry {\n  setStreamState('streaming');\n\n  \u002F\u002F Streaming logic...\n\n  setStreamState('complete');\n} catch (error) {\n  setStreamState('error');\n\n  if (error.name === 'AbortError') {\n    setErrorMessage('Generation stopped');\n  } else if (error.message.includes('429')) {\n    setErrorMessage('Rate limit exceeded. Try again in a moment.');\n  } else {\n    setErrorMessage('Something went wrong. Please retry.');\n  }\n}\n\n\u002F\u002F UI feedback\n{streamState === 'error' && (\n  \u003Cdiv className=\"error-banner\">\n    {errorMessage}\n    \u003Cbutton onClick={retryStream}>Retry\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n)}\n```\n\n---\n\n### Anti-Pattern 4: Memory Leaks from Unclosed Streams\n\n**Problem**: Streams not cleaned up, causing memory leaks.\n\n**Symptom**: Browser slows down after multiple requests.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Cleanup with useEffect\nuseEffect(() => {\n  let reader: ReadableStreamDefaultReader | null = null;\n\n  const streamResponse = async () => {\n    const response = await fetch('\u002Fapi\u002Fchat', { ... });\n    reader = response.body.getReader();\n\n    \u002F\u002F Streaming...\n  };\n\n  streamResponse();\n\n  \u002F\u002F Cleanup on unmount\n  return () => {\n    reader?.cancel();\n  };\n}, [prompt]);\n```\n\n---\n\n### Anti-Pattern 5: No Typing Indicator Between Tokens\n\n**Problem**: UI feels frozen between slow tokens.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Animated cursor during generation\n\u003Cdiv className=\"message\">\n  {content}\n  {isStreaming && \u003Cspan className=\"typing-cursor\">▊\u003C\u002Fspan>}\n\u003C\u002Fdiv>\n```\n\n```css\n.typing-cursor {\n  animation: blink 1s step-end infinite;\n}\n\n@keyframes blink {\n  50% { opacity: 0; }\n}\n```\n\n---\n\n## Implementation Patterns\n\n### Pattern 1: Basic SSE Stream Handler\n\n```typescript\nasync function* streamCompletion(prompt: string) {\n  const response = await fetch('\u002Fapi\u002Fchat', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ prompt })\n  });\n\n  const reader = response.body!.getReader();\n  const decoder = new TextDecoder();\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    const chunk = decoder.decode(value);\n    const lines = chunk.split('\\n');\n\n    for (const line of lines) {\n      if (line.startsWith('data: ')) {\n        const data = JSON.parse(line.slice(6));\n\n        if (data.content) {\n          yield data.content;\n        }\n\n        if (data.done) {\n          return;\n        }\n      }\n    }\n  }\n}\n\n\u002F\u002F Usage\nfor await (const token of streamCompletion('Hello')) {\n  console.log(token);\n}\n```\n\n### Pattern 2: React Hook for Streaming\n\n```typescript\nimport { useState, useCallback } from 'react';\n\ninterface UseStreamingOptions {\n  onToken?: (token: string) => void;\n  onComplete?: (fullText: string) => void;\n  onError?: (error: Error) => void;\n}\n\nexport function useStreaming(options: UseStreamingOptions = {}) {\n  const [content, setContent] = useState('');\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState\u003CError | null>(null);\n  const [abortController, setAbortController] = useState\u003CAbortController | null>(null);\n\n  const stream = useCallback(async (prompt: string) => {\n    const controller = new AbortController();\n    setAbortController(controller);\n    setIsStreaming(true);\n    setError(null);\n    setContent('');\n\n    try {\n      const response = await fetch('\u002Fapi\u002Fchat', {\n        method: 'POST',\n        signal: controller.signal,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n        body: JSON.stringify({ prompt })\n      });\n\n      const reader = response.body!.getReader();\n      const decoder = new TextDecoder();\n\n      let accumulated = '';\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        const chunk = decoder.decode(value);\n        const lines = chunk.split('\\n').filter(line => line.trim());\n\n        for (const line of lines) {\n          if (line.startsWith('data: ')) {\n            const data = JSON.parse(line.slice(6));\n\n            if (data.content) {\n              accumulated += data.content;\n              setContent(accumulated);\n              options.onToken?.(data.content);\n            }\n          }\n        }\n      }\n\n      options.onComplete?.(accumulated);\n    } catch (err) {\n      if (err.name !== 'AbortError') {\n        setError(err as Error);\n        options.onError?.(err as Error);\n      }\n    } finally {\n      setIsStreaming(false);\n      setAbortController(null);\n    }\n  }, [options]);\n\n  const cancel = useCallback(() => {\n    abortController?.abort();\n  }, [abortController]);\n\n  return { content, isStreaming, error, stream, cancel };\n}\n\n\u002F\u002F Usage in component\nfunction ChatInterface() {\n  const { content, isStreaming, stream, cancel } = useStreaming({\n    onToken: (token) => console.log('New token:', token),\n    onComplete: (text) => console.log('Done:', text)\n  });\n\n  return (\n    \u003Cdiv>\n      \u003Cdiv className=\"message\">\n        {content}\n        {isStreaming && \u003Cspan className=\"cursor\">▊\u003C\u002Fspan>}\n      \u003C\u002Fdiv>\n\n      \u003Cbutton onClick={() => stream('Tell me a story')} disabled={isStreaming}>\n        Generate\n      \u003C\u002Fbutton>\n\n      {isStreaming && \u003Cbutton onClick={cancel}>Stop\u003C\u002Fbutton>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Pattern 3: Server-Side Streaming (Next.js)\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fchat\u002Froute.ts\nimport { OpenAI } from 'openai';\n\nexport const runtime = 'edge'; \u002F\u002F Required for streaming\n\nexport async function POST(req: Request) {\n  const { prompt } = await req.json();\n\n  const openai = new OpenAI({\n    apiKey: process.env.OPENAI_API_KEY\n  });\n\n  const stream = await openai.chat.completions.create({\n    model: 'gpt-4',\n    messages: [{ role: 'user', content: prompt }],\n    stream: true\n  });\n\n  \u002F\u002F Convert OpenAI stream to SSE format\n  const encoder = new TextEncoder();\n\n  const readable = new ReadableStream({\n    async start(controller) {\n      try {\n        for await (const chunk of stream) {\n          const content = chunk.choices[0]?.delta?.content;\n\n          if (content) {\n            const sseMessage = `data: ${JSON.stringify({ content })}\\n\\n`;\n            controller.enqueue(encoder.encode(sseMessage));\n          }\n        }\n\n        \u002F\u002F Send completion signal\n        controller.enqueue(encoder.encode('data: {\"done\":true}\\n\\n'));\n        controller.close();\n      } catch (error) {\n        controller.error(error);\n      }\n    }\n  });\n\n  return new Response(readable, {\n    headers: {\n      'Content-Type': 'text\u002Fevent-stream',\n      'Cache-Control': 'no-cache',\n      'Connection': 'keep-alive'\n    }\n  });\n}\n```\n\n---\n\n## Production Checklist\n\n```\n□ AbortController for cancellation\n□ Error states with retry capability\n□ Typing indicator during generation\n□ Cleanup on component unmount\n□ Rate limiting on API route\n□ Token usage tracking\n□ Streaming fallback (if API fails)\n□ Accessibility (screen reader announces updates)\n□ Mobile-friendly (touch targets for stop button)\n□ Network error recovery (auto-retry on disconnect)\n□ Max response length enforcement\n□ Cost estimation before generation\n```\n\n---\n\n## When to Use vs Avoid\n\n| Scenario | Use Streaming? |\n|----------|----------------|\n| Chat interface | ✅ Yes |\n| Long-form content generation | ✅ Yes |\n| Code generation with preview | ✅ Yes |\n| Short completions (&lt;50 words) | ❌ No - regular fetch |\n| Background jobs | ❌ No - use job queue |\n| Bidirectional chat | ⚠️ Use WebSockets instead |\n\n---\n\n## Technology Comparison\n\n| Feature | SSE | WebSockets | Long Polling |\n|---------|-----|-----------|--------------|\n| Complexity | Low | Medium | High |\n| Auto-reconnect | ✅ | ❌ | ❌ |\n| Bidirectional | ❌ | ✅ | ❌ |\n| Firewall-friendly | ✅ | ⚠️ | ✅ |\n| Browser support | ✅ All modern | ✅ All modern | ✅ Universal |\n| LLM API support | ✅ Standard | ❌ Rare | ❌ Not used |\n\n---\n\n## References\n\n- `\u002Freferences\u002Fsse-protocol.md` - Server-Sent Events specification details\n- `\u002Freferences\u002Fvercel-ai-sdk.md` - Vercel AI SDK integration patterns\n- `\u002Freferences\u002Ferror-recovery.md` - Stream error handling strategies\n\n## Scripts\n\n- `scripts\u002Fstream_tester.ts` - Test SSE endpoints locally\n- `scripts\u002Ftoken_counter.ts` - Estimate costs before generation\n\n---\n\n**This skill guides**: LLM streaming implementation | SSE protocol | Real-time UI updates | Cancellation | Error recovery | Token-by-token display\n\n","# LLM Streaming Response Handler\n\nExpert in building production-grade streaming interfaces for LLM responses that feel instant and responsive.\n\n## When to Use\n\n✅ **Use for**:\n- Chat interfaces with typing animation\n- Real-time AI assistants\n- Code generation with live preview\n- Document summarization with progressive display\n- Any UI where users expect immediate feedback from LLMs\n\n❌ **NOT for**:\n- Batch document processing (no user watching)\n- APIs that don't support streaming\n- WebSocket-based bidirectional chat (use Socket.IO)\n- Simple request\u002Fresponse (fetch is fine)\n\n## Quick Decision Tree\n\n```\nDoes your LLM interaction:\n├── Need immediate visual feedback? → Streaming\n├── Display long-form content (&gt;100 words)? → Streaming\n├── User expects typewriter effect? → Streaming\n├── Short response (&lt;50 words)? → Regular fetch\n└── Background processing? → Regular fetch\n```\n\n---\n\n## Technology Selection\n\n### Server-Sent Events (SSE) - Recommended\n\n**Why SSE over WebSockets for LLM streaming**:\n- **Simplicity**: HTTP-based, works with existing infrastructure\n- **Auto-reconnect**: Built-in reconnection logic\n- **Firewall-friendly**: Easier than WebSockets through proxies\n- **One-way perfect**: LLMs only stream server → client\n\n**Timeline**:\n- 2015-2020: WebSockets for everything\n- 2020: SSE adoption for streaming APIs\n- 2023+: SSE standard for LLM streaming (OpenAI, Anthropic)\n- 2024: Vercel AI SDK popularizes SSE patterns\n\n### Streaming APIs\n\n| Provider | Streaming Method | Response Format |\n|----------|------------------|-----------------|\n| OpenAI | SSE | `data: {\"choices\":[{\"delta\":{\"content\":\"token\"}}]}` |\n| Anthropic | SSE | `data: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"token\"}}` |\n| Claude (API) | SSE | `data: {\"delta\":{\"text\":\"token\"}}` |\n| Vercel AI SDK | SSE | Normalized across providers |\n\n---\n\n## Common Anti-Patterns\n\n### Anti-Pattern 1: Buffering Before Display\n\n**Novice thinking**: \"Collect all tokens, then show complete response\"\n\n**Problem**: Defeats the entire purpose of streaming.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Waits for entire response before showing anything\nconst response = await fetch('\u002Fapi\u002Fchat', { method: 'POST', body: prompt });\nconst fullText = await response.text();\nsetMessage(fullText); \u002F\u002F User sees nothing until done\n```\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Display tokens as they arrive\nconst response = await fetch('\u002Fapi\u002Fchat', {\n  method: 'POST',\n  body: JSON.stringify({ prompt })\n});\n\nconst reader = response.body.getReader();\nconst decoder = new TextDecoder();\n\nwhile (true) {\n  const { done, value } = await reader.read();\n  if (done) break;\n\n  const chunk = decoder.decode(value);\n  const lines = chunk.split('\\n').filter(line => line.trim());\n\n  for (const line of lines) {\n    if (line.startsWith('data: ')) {\n      const data = JSON.parse(line.slice(6));\n      setMessage(prev => prev + data.content); \u002F\u002F Update immediately\n    }\n  }\n}\n```\n\n**Timeline**:\n- Pre-2023: Many apps buffered entire response\n- 2023+: Token-by-token display expected\n\n---\n\n### Anti-Pattern 2: No Stream Cancellation\n\n**Problem**: User can't stop generation, wasting tokens and money.\n\n**Symptom**: \"Stop\" button doesn't work or doesn't exist.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ AbortController for cancellation\nconst [abortController, setAbortController] = useState\u003CAbortController | null>(null);\n\nconst streamResponse = async () => {\n  const controller = new AbortController();\n  setAbortController(controller);\n\n  try {\n    const response = await fetch('\u002Fapi\u002Fchat', {\n      signal: controller.signal,\n      method: 'POST',\n      body: JSON.stringify({ prompt })\n    });\n\n    \u002F\u002F Stream handling...\n  } catch (error) {\n    if (error.name === 'AbortError') {\n      console.log('Stream cancelled by user');\n    }\n  } finally {\n    setAbortController(null);\n  }\n};\n\nconst cancelStream = () => {\n  abortController?.abort();\n};\n\nreturn (\n  \u003Cbutton onClick={cancelStream} disabled={!abortController}>\n    Stop Generating\n  \u003C\u002Fbutton>\n);\n```\n\n---\n\n### Anti-Pattern 3: No Error Recovery\n\n**Problem**: Stream fails mid-response, user sees partial text with no indication of failure.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Error states and recovery\nconst [streamState, setStreamState] = useState\u003C'idle' | 'streaming' | 'error' | 'complete'>('idle');\nconst [errorMessage, setErrorMessage] = useState\u003Cstring | null>(null);\n\ntry {\n  setStreamState('streaming');\n\n  \u002F\u002F Streaming logic...\n\n  setStreamState('complete');\n} catch (error) {\n  setStreamState('error');\n\n  if (error.name === 'AbortError') {\n    setErrorMessage('Generation stopped');\n  } else if (error.message.includes('429')) {\n    setErrorMessage('Rate limit exceeded. Try again in a moment.');\n  } else {\n    setErrorMessage('Something went wrong. Please retry.');\n  }\n}\n\n\u002F\u002F UI feedback\n{streamState === 'error' && (\n  \u003Cdiv className=\"error-banner\">\n    {errorMessage}\n    \u003Cbutton onClick={retryStream}>Retry\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n)}\n```\n\n---\n\n### Anti-Pattern 4: Memory Leaks from Unclosed Streams\n\n**Problem**: Streams not cleaned up, causing memory leaks.\n\n**Symptom**: Browser slows down after multiple requests.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Cleanup with useEffect\nuseEffect(() => {\n  let reader: ReadableStreamDefaultReader | null = null;\n\n  const streamResponse = async () => {\n    const response = await fetch('\u002Fapi\u002Fchat', { ... });\n    reader = response.body.getReader();\n\n    \u002F\u002F Streaming...\n  };\n\n  streamResponse();\n\n  \u002F\u002F Cleanup on unmount\n  return () => {\n    reader?.cancel();\n  };\n}, [prompt]);\n```\n\n---\n\n### Anti-Pattern 5: No Typing Indicator Between Tokens\n\n**Problem**: UI feels frozen between slow tokens.\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Animated cursor during generation\n\u003Cdiv className=\"message\">\n  {content}\n  {isStreaming && \u003Cspan className=\"typing-cursor\">▊\u003C\u002Fspan>}\n\u003C\u002Fdiv>\n```\n\n```css\n.typing-cursor {\n  animation: blink 1s step-end infinite;\n}\n\n@keyframes blink {\n  50% { opacity: 0; }\n}\n```\n\n---\n\n## Implementation Patterns\n\n### Pattern 1: Basic SSE Stream Handler\n\n```typescript\nasync function* streamCompletion(prompt: string) {\n  const response = await fetch('\u002Fapi\u002Fchat', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ prompt })\n  });\n\n  const reader = response.body!.getReader();\n  const decoder = new TextDecoder();\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    const chunk = decoder.decode(value);\n    const lines = chunk.split('\\n');\n\n    for (const line of lines) {\n      if (line.startsWith('data: ')) {\n        const data = JSON.parse(line.slice(6));\n\n        if (data.content) {\n          yield data.content;\n        }\n\n        if (data.done) {\n          return;\n        }\n      }\n    }\n  }\n}\n\n\u002F\u002F Usage\nfor await (const token of streamCompletion('Hello')) {\n  console.log(token);\n}\n```\n\n### Pattern 2: React Hook for Streaming\n\n```typescript\nimport { useState, useCallback } from 'react';\n\ninterface UseStreamingOptions {\n  onToken?: (token: string) => void;\n  onComplete?: (fullText: string) => void;\n  onError?: (error: Error) => void;\n}\n\nexport function useStreaming(options: UseStreamingOptions = {}) {\n  const [content, setContent] = useState('');\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState\u003CError | null>(null);\n  const [abortController, setAbortController] = useState\u003CAbortController | null>(null);\n\n  const stream = useCallback(async (prompt: string) => {\n    const controller = new AbortController();\n    setAbortController(controller);\n    setIsStreaming(true);\n    setError(null);\n    setContent('');\n\n    try {\n      const response = await fetch('\u002Fapi\u002Fchat', {\n        method: 'POST',\n        signal: controller.signal,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n        body: JSON.stringify({ prompt })\n      });\n\n      const reader = response.body!.getReader();\n      const decoder = new TextDecoder();\n\n      let accumulated = '';\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        const chunk = decoder.decode(value);\n        const lines = chunk.split('\\n').filter(line => line.trim());\n\n        for (const line of lines) {\n          if (line.startsWith('data: ')) {\n            const data = JSON.parse(line.slice(6));\n\n            if (data.content) {\n              accumulated += data.content;\n              setContent(accumulated);\n              options.onToken?.(data.content);\n            }\n          }\n        }\n      }\n\n      options.onComplete?.(accumulated);\n    } catch (err) {\n      if (err.name !== 'AbortError') {\n        setError(err as Error);\n        options.onError?.(err as Error);\n      }\n    } finally {\n      setIsStreaming(false);\n      setAbortController(null);\n    }\n  }, [options]);\n\n  const cancel = useCallback(() => {\n    abortController?.abort();\n  }, [abortController]);\n\n  return { content, isStreaming, error, stream, cancel };\n}\n\n\u002F\u002F Usage in component\nfunction ChatInterface() {\n  const { content, isStreaming, stream, cancel } = useStreaming({\n    onToken: (token) => console.log('New token:', token),\n    onComplete: (text) => console.log('Done:', text)\n  });\n\n  return (\n    \u003Cdiv>\n      \u003Cdiv className=\"message\">\n        {content}\n        {isStreaming && \u003Cspan className=\"cursor\">▊\u003C\u002Fspan>}\n      \u003C\u002Fdiv>\n\n      \u003Cbutton onClick={() => stream('Tell me a story')} disabled={isStreaming}>\n        Generate\n      \u003C\u002Fbutton>\n\n      {isStreaming && \u003Cbutton onClick={cancel}>Stop\u003C\u002Fbutton>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Pattern 3: Server-Side Streaming (Next.js)\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fchat\u002Froute.ts\nimport { OpenAI } from 'openai';\n\nexport const runtime = 'edge'; \u002F\u002F Required for streaming\n\nexport async function POST(req: Request) {\n  const { prompt } = await req.json();\n\n  const openai = new OpenAI({\n    apiKey: process.env.OPENAI_API_KEY\n  });\n\n  const stream = await openai.chat.completions.create({\n    model: 'gpt-4',\n    messages: [{ role: 'user', content: prompt }],\n    stream: true\n  });\n\n  \u002F\u002F Convert OpenAI stream to SSE format\n  const encoder = new TextEncoder();\n\n  const readable = new ReadableStream({\n    async start(controller) {\n      try {\n        for await (const chunk of stream) {\n          const content = chunk.choices[0]?.delta?.content;\n\n          if (content) {\n            const sseMessage = `data: ${JSON.stringify({ content })}\\n\\n`;\n            controller.enqueue(encoder.encode(sseMessage));\n          }\n        }\n\n        \u002F\u002F Send completion signal\n        controller.enqueue(encoder.encode('data: {\"done\":true}\\n\\n'));\n        controller.close();\n      } catch (error) {\n        controller.error(error);\n      }\n    }\n  });\n\n  return new Response(readable, {\n    headers: {\n      'Content-Type': 'text\u002Fevent-stream',\n      'Cache-Control': 'no-cache',\n      'Connection': 'keep-alive'\n    }\n  });\n}\n```\n\n---\n\n## Production Checklist\n\n```\n□ AbortController for cancellation\n□ Error states with retry capability\n□ Typing indicator during generation\n□ Cleanup on component unmount\n□ Rate limiting on API route\n□ Token usage tracking\n□ Streaming fallback (if API fails)\n□ Accessibility (screen reader announces updates)\n□ Mobile-friendly (touch targets for stop button)\n□ Network error recovery (auto-retry on disconnect)\n□ Max response length enforcement\n□ Cost estimation before generation\n```\n\n---\n\n## When to Use vs Avoid\n\n| Scenario | Use Streaming? |\n|----------|----------------|\n| Chat interface | ✅ Yes |\n| Long-form content generation | ✅ Yes |\n| Code generation with preview | ✅ Yes |\n| Short completions (&lt;50 words) | ❌ No - regular fetch |\n| Background jobs | ❌ No - use job queue |\n| Bidirectional chat | ⚠️ Use WebSockets instead |\n\n---\n\n## Technology Comparison\n\n| Feature | SSE | WebSockets | Long Polling |\n|---------|-----|-----------|--------------|\n| Complexity | Low | Medium | High |\n| Auto-reconnect | ✅ | ❌ | ❌ |\n| Bidirectional | ❌ | ✅ | ❌ |\n| Firewall-friendly | ✅ | ⚠️ | ✅ |\n| Browser support | ✅ All modern | ✅ All modern | ✅ Universal |\n| LLM API support | ✅ Standard | ❌ Rare | ❌ Not used |\n\n---\n\n## References\n\n- `\u002Freferences\u002Fsse-protocol.md` - Server-Sent Events specification details\n- `\u002Freferences\u002Fvercel-ai-sdk.md` - Vercel AI SDK integration patterns\n- `\u002Freferences\u002Ferror-recovery.md` - Stream error handling strategies\n\n## Scripts\n\n- `scripts\u002Fstream_tester.ts` - Test SSE endpoints locally\n- `scripts\u002Ftoken_counter.ts` - Estimate costs before generation\n\n---\n\n**This skill guides**: LLM streaming implementation | SSE protocol | Real-time UI updates | Cancellation | Error recovery | Token-by-token display\n\n",{"name":5,"metadata":48,"description":7,"allowed-tools":68},{"tags":49,"category":54,"pairs-with":55},[50,23,51,52,53],"llm","response","llm-streaming","sse","AI & Machine Learning",[56,59,62,65],{"skill":57,"reason":58},"chatbot-analytics","Streaming metrics (time-to-first-token, throughput) are key analytics for chatbot performance",{"skill":60,"reason":61},"react-performance-optimizer","Real-time token rendering requires React performance optimization for smooth display",{"skill":63,"reason":64},"error-handling-patterns","Stream interruption recovery and retry logic are critical error handling concerns",{"skill":66,"reason":67},"websocket-streaming","WebSocket transport is an alternative to SSE for bidirectional LLM streaming","Read,Write,Edit,Bash(npm:*)","main","f2ec4b3b9133d34eeaf9b38e4b0a97e8e5705a7f","benchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler",0,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard","https:\u002F\u002Fgithub.com\u002FJSGforever","2026-08-29 18:27:56",{"source":8,"skillName":5,"commands":77,"prompt":87,"zipAvailable":88},[78,81,84],{"tool":79,"command":80},"npx","npx skills add JSGforever\u002Fskillguard --skill llm-streaming-response-handler",{"tool":82,"command":83},"bunx","bunx skills add JSGforever\u002Fskillguard --skill llm-streaming-response-handler",{"tool":85,"command":86},"pnpm","pnpm dlx skills add JSGforever\u002Fskillguard --skill llm-streaming-response-handler","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002FSKILL.md",true,[90,96,100,104,110],{"path":91,"resourceType":92,"resourceTypeDesc":93,"sizeBytes":94,"sourceHtmlUrl":95},"references\u002Ferror-recovery.md","REFERENCE","参考文档",11913,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002Freferences\u002Ferror-recovery.md",{"path":97,"resourceType":92,"resourceTypeDesc":93,"sizeBytes":98,"sourceHtmlUrl":99},"references\u002Fsse-protocol.md",8948,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002Freferences\u002Fsse-protocol.md",{"path":101,"resourceType":92,"resourceTypeDesc":93,"sizeBytes":102,"sourceHtmlUrl":103},"references\u002Fvercel-ai-sdk.md",9899,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002Freferences\u002Fvercel-ai-sdk.md",{"path":105,"resourceType":106,"resourceTypeDesc":107,"sizeBytes":108,"sourceHtmlUrl":109},"scripts\u002Fstream_tester.ts","SCRIPT","脚本",5174,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002Fscripts\u002Fstream_tester.ts",{"path":111,"resourceType":106,"resourceTypeDesc":107,"sizeBytes":112,"sourceHtmlUrl":113},"scripts\u002Ftoken_counter.ts",5611,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002Fscripts\u002Ftoken_counter.ts",[115,145,161,181,200,229],{"skillNo":116,"name":117,"nameZh":6,"description":118,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":119,"categories":120,"keywords":124,"publishedAt":43},"01M71HY2SNXBA9Q00WZDTZQ31Z","skillguard-codex-scanner","Hybrid deterministic+LLM agent-skill security scanner. Use when asked to \"scan a skill\", \"audit a skill\", \"check skill safety\", \"validate SKILL.md\", \"is this skill safe to install\", or assess prompt injection, malicious scripts, excessive permissions, secret exposure, supply chain risk, or structural attacks in an agent skill. Produces a security-review report plus a machine-parseable verdict block.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fsrc\u002Fskillguard\u002Fstatic_scanner\u002Fskillguard-codex-scanner\u002FSKILL.md",[121],{"categoryNo":122,"categoryName":123,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",[125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144],"scanner","skillguard codex scanner","skillguard","codex","chain","skill security","security-review","review report","structural","secret","prompt injection","security scanner","when","review","malicious","permissions","exposure","audit","scripts","safe",{"skillNo":146,"name":147,"nameZh":6,"description":148,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":149,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":157,"publishedAt":43},"01M71HY0SPKTT3S27BC2HPAC5M","data-exfiltrator","Analyzes data files","MIT","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Fsmoke-one-skill\u002Fasset1__cisco-ai-defense--data-exfiltrator\u002FSKILL.md",[152],{"categoryNo":153,"categoryName":154,"taxonomyType":155,"taxonomyTypeDesc":156},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[158,159,160],"data","data exfiltrator","files",{"skillNo":162,"name":163,"nameZh":6,"description":164,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":165,"categories":166,"keywords":167,"publishedAt":43},"01M71HXVS9EMN44BGDW215FZ72","real-time-collaboration-engine","Build real-time collaborative editing with WebSockets, OT\u002FCRDT conflict resolution, and presence awareness. Implements cursor tracking, optimistic updates, and offline sync. Use for collaborative editors, whiteboards, video editing. Activate on \"real-time collaboration\", \"WebSocket sync\", \"multiplayer editing\", \"CRDT\", \"presence awareness\". NOT for simple chat, request-response APIs, or single-user apps.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--real-time-collaboration-engine\u002FSKILL.md",[],[38,168,169,170,171,172,173,174,175,39,176,177,178,179,180,34,33,32,30,23],"real time collaboration engine","real time collaboration","collaboration","engine","real time","Video","reliability","video editing","conflict resolution","presence","access","apps","updates",{"skillNo":182,"name":60,"nameZh":6,"description":183,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":184,"categories":185,"keywords":187,"publishedAt":43},"01M71HXP6CTY4KFN0QVMMN0V7J","Optimize React apps for 60fps performance. Implements memoization, virtualization, code splitting, bundle optimization. Use for slow renders, large lists, bundle bloat. Activate on \"React performance\", \"slow render\", \"useMemo\", \"bundle size\", \"virtualization\". NOT for backend optimization, non-React frameworks, or premature optimization.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--react-performance-optimizer\u002FSKILL.md",[186],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},[39,188,32,30,189,190,191,192,193,194,38,195,196,179,34,197,33,23,198,199],"react performance optimizer","react performance","performance optimizer","app","nextjs","expert","re","render","2025","code","router","strategies",{"skillNo":201,"name":202,"nameZh":6,"description":203,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":204,"categories":205,"keywords":213,"publishedAt":43},"01M71HXCB0E1YRKYW1Y8T0Y106","github-actions-pipeline-builder","Build production CI\u002FCD pipelines with GitHub Actions. Implements matrix builds, caching, deployments, testing, security scanning. Use for automated testing, deployments, release workflows. Activate on \"GitHub Actions\", \"CI\u002FCD\", \"workflow\", \"deployment pipeline\", \"automated testing\". NOT for Jenkins\u002FCircleCI, manual deployments, or non-GitHub repositories.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--github-actions-pipeline-builder\u002FSKILL.md",[206,209,212],{"categoryNo":207,"categoryName":208,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":210,"categoryName":211,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":122,"categoryName":123,"taxonomyType":20,"taxonomyTypeDesc":21},[214,215,216,217,218,219,220,174,193,221,222,223,33,199,224,26,225,226,227,228],"github actions pipeline builder","actions","github actions","pipeline builder","github","builder","pipeline","testing","ci","workflows","run","release","write","edit","git",{"skillNo":230,"name":231,"nameZh":6,"description":232,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":233,"categories":234,"keywords":239,"publishedAt":43},"01M71HX6WRYD7MVR67PZVDK0GC","geospatial-data-pipeline","Process, analyze, and visualize geospatial data at scale. Handles drone imagery, GPS tracks, GeoJSON optimization, coordinate transformations, and tile generation. Use for mapping apps, drone data processing, location-based services. Activate on \"geospatial\", \"GIS\", \"PostGIS\", \"GeoJSON\", \"map tiles\", \"coordinate systems\". NOT for simple address validation, basic distance calculations, or static map embeds.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--geospatial-data-pipeline\u002FSKILL.md",[235,238],{"categoryNo":236,"categoryName":237,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":153,"categoryName":154,"taxonomyType":155,"taxonomyTypeDesc":156},[240,220,241,158,242,243,244,41,245,246,247,195,248,249,250,179,34,251,252,33],"geospatial data pipeline","data pipeline","geospatial","coordinate","visualization","postgres","static","flows","engineer","map","inspection","drone","analysis"]