跪拜 Guibai
← Back to the summary

How to Build a Typewriter Effect for LLM Streaming with Vue 3 and ReadableStream

📝Abstract

In AI applications and intelligent Agent development, streaming output has become the standard interaction for conversational interfaces. Instead of waiting for the large model to generate the entire response before rendering it all at once, you can use the browser's native ReadableStream with the DeepSeek large model API to easily create a typewriter animation that displays content as it is generated. This article explains the underlying principles and engineering approach layer by layer, with practical Vue3 <script setup> code. You don't need to copy the entire code; once you grasp the core logic, you can migrate it to any frontend project.

🤖 1. In the Age of Agents, How Should We Adjust Our Development Mindset?

Today's large model Agents are continuously iterating, with capabilities increasingly approaching the autonomous completion of complex tasks, marking an important direction towards AGI. A new model of human-machine collaboration is taking shape: delegate the standardized, repetitive work that AI excels at to Agents, while humans focus on decision-making, review, and core business control.

When it comes to frontend development, there are two highly practical approaches:

  1. Refuse to build project scaffolding from scratch Don't manually write index.html, App.vue, and other basic files every time you start a new project. Instead, go directly to Github to pull a mature template and iterate on your business logic on top of it.
  2. Reasonably divide the work boundaries between humans and AI Hand off project initialization, boilerplate code, and common base component construction to the Agent; developers should lead and control complex interaction logic, edge-case exception handling, and core business rules.

⚡ 2. Vite Hot Reload: An Efficiency Booster for Debugging Streaming Projects

This demo is developed based on Vite + Vue3. Vite's built-in Hot Reload is a powerful tool for local development.

🌊 3. The Underlying Principle of Streaming Output: Binary ReadableStream

Traditional One-Shot Request Mode

Frontend initiates a request → The large model server computes the entire answer → Returns a complete JSON → The page renders it all at once. When the AI outputs long text, the user sees nothing but "Loading..." for a long time, resulting in a poor interactive experience.

Stream Mode

The large model continuously sends incremental text chunks to the frontend as it generates tokens. The frontend receives the chunked data and immediately appends it for rendering, which is the familiar typewriter effect.

At the browser level, the streaming response returned by the LLM is essentially a binary data stream, ReadableStream, which can be understood using a water pipe analogy:

  1. response.body: The pipe (ReadableStream) returned by the server;
  2. .getReader(): Obtains a reader to continuously pull data from the pipe;
  3. reader.read(): Each call pulls out a chunk of Uint8Array binary data (unsigned integers ranging from 0 to 255);
  4. TextDecoder: Binary bytes cannot be directly converted into readable text; a decoder is needed to perform the translation.

📡 4. LLM Streaming Communication Specification: SSE-like Chunk Format

Mainstream large model streaming interfaces, such as those from DeepSeek and OpenAI, uniformly follow data chunking rules similar to SSE:

Example raw stream snippet:

plaintext

data: {"choices":[{"delta":{"content":"Once"}}]}
data: {"choices":[{"delta":{"content":" upon"}}]}
data: [DONE]

Standard frontend processing flow:

  1. Split the decoded string by newline characters;
  2. Filter and select the data lines starting with data: ;
  3. Filter out the termination marker [DONE];
  4. Parse the chunk as JSON and extract the delta.content incremental text;
  5. Continuously concatenate the text and update the page view.

⚠️ Two common pitfalls for beginners:

  1. The native JS string method is startsWith. It's very easy to mistakenly write startWith, which throws a "function does not exist" error;
  2. A single read of a binary chunk might truncate a JSON string. You must set up a buffer to save the incomplete fragment for parsing in the next loop iteration.

💻 5. Core Vue3 Practical Approach (Key Point! Don't Just Copy the Code Blindly)

Using Vue3's <script setup> Composition API offers clear advantages over Vue2's Options API: logic related to the same business concern is grouped together, making the code more cohesive and easier to maintain.

Basic Reactive Variables

js

import { ref } from 'vue';
// User's question
const question = ref('Tell a story about a Chinese dragon');
// AI output content
const content = ref('');
// Toggle: whether to enable streaming output
const stream = ref(true);

Core Request Logic

js

const update = async () => {
  if (!question.value) return;
  content.value = 'Thinking...';

  const endpoint = 'https://api.deepseek.com/chat/completions';
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
    },
    body: JSON.stringify({
      model: 'deepseek-v4-flash',
      messages: [{ role: 'user', content: question.value }],
      stream: stream.value
    })
  });

  if(stream.value){
    // ========== 🎯 Core Streaming Logic ==========
    content.value = '';
    const reader = response.body?.getReader();
    const decoder = new TextDecoder();
    let done = false;
    let buffer = ''; // Cache for incomplete JSON strings

    while (!done) {
      const { value, done: doneReading } = await reader.read();
      done = doneReading;
      if(!value) continue;
      
      // Decode binary and concatenate with buffer
      const chunk = buffer + decoder.decode(value);
      buffer = '';
      const lines = chunk.split('\n');
      
      for(const line of lines){
        const trimLine = line.trim();
        if(!trimLine.startsWith('data: ')) continue;
        const dataStr = trimLine.replace('data: ','');
        if(dataStr === '[DONE]') continue;
        
        try{
          const res = JSON.parse(dataStr);
          const text = res.choices[0].delta.content;
          if(text) content.value += text;
        }catch{
          // JSON is incomplete, store in buffer for next round of parsing
          buffer = line;
        }
      }
    }
  }else{
    // Non-streaming: receive the complete result at once
    const data = await response.json();
    content.value = data.choices[0].message.content;
  }
}

✅ Three core blocks you need to master; the rest of the code can be trimmed or modified as needed:

  1. The while loop continuously calls reader.read() to pull binary chunks;
  2. The buffer for fault tolerance, solving the problem of truncated JSON;
  3. Continuously appending incremental text, leveraging Vue's reactivity to automatically update the page.

Minimal template reference:

vue

<template>
  <div class="container">
    <div>
      <label>Input:</label>
      <input v-model="question"/>
      <button @click="update">Submit</button>
    </div>
    <div>
      <label>Streaming</label>
      <input type="checkbox" v-model="stream"/>
    </div>
    <div>{{ content }}</div>
  </div>
</template>

🚨 6. Three Risks You Must Address Before Going Live

  1. CORS Cross-Origin Restrictions Calling the DeepSeek public API directly from the browser frontend will trigger a cross-origin error. For local development, you can use a Vite proxy; in a production environment, you must add a backend relay layer.
  2. API Key Leakage Keys inside a frontend project cannot be kept secret; a user can simply open the browser's developer tools to grab them. Online projects must not make direct frontend requests to large model APIs.
  3. Network Exception Handling You need fallback handling for disconnections, timeouts, and chunk parsing failures. The buffer cannot be casually removed; it is key to the stable operation of streaming.

🧠 7. Expansion: The Value of Streaming Output in Agent Development

Streaming output is not just about implementing a nice typewriter animation. In intelligent Agent scenarios, the AI completes thinking, tool calling, and result summarization step by step. Relying on streaming push, we can display the Agent's complete chain of thought and tool call logs in real time, allowing users to clearly see how the AI breaks down tasks and executes actions.

Mastering browser data stream processing is a fundamental skill for developing AI conversation and intelligent Agent frontend interfaces.