Skip to content

dina-kar/ngx-streamdown

Repository files navigation

ngx-streamdown

Angular port of streamdown - A streaming markdown renderer optimized for AI-powered applications.

npm version

Overview

ngx-streamdown brings the power of streaming markdown rendering to Angular applications. Built on top of ngx-markdown, it handles incomplete Markdown syntax gracefully during real-time streaming from AI models, providing seamless formatting even with partial or unterminated Markdown blocks.

Features

  • 🚀 Angular-native - Built with Angular signals and standalone components
  • 🔄 Streaming-optimized - Handles incomplete Markdown gracefully using remend
  • 🎨 Progressive rendering - Parses markdown into blocks for better performance
  • 📊 GitHub Flavored Markdown - Full GFM support via ngx-markdown
  • 🔢 Math rendering - LaTeX equations via KaTeX
  • 🎯 TypeScript - Full type safety with TypeScript
  • Performance optimized - Debounced rendering and change detection
  • 🛡️ OnPush strategy - Optimized change detection for better performance

Installation

npm install ngx-streamdown ngx-markdown

Peer Dependencies

  • @angular/common ^17.0.0 || ^18.0.0
  • @angular/core ^17.0.0 || ^18.0.0
  • ngx-markdown ^17.0.0 || ^18.0.0
  • rxjs ^7.8.0
  • remend (automatically installed)

Setup

1. Import Required Modules

For standalone components (Angular 14+):

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideMarkdown } from 'ngx-markdown';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideHttpClient(),
    provideMarkdown(),
  ],
};

For NgModule-based apps:

// app.module.ts
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { MarkdownModule } from 'ngx-markdown';

@NgModule({
  imports: [
    HttpClientModule,
    MarkdownModule.forRoot(),
  ],
})
export class AppModule {}

2. Add Styles (Optional)

Add to your angular.json or import in your global styles:

/* styles.css */
@import 'katex/dist/katex.css'; /* For math support */

Usage

Basic Example

import { Component } from '@angular/core';
import { StreamingMarkdownComponent } from 'ngx-streamdown';

@Component({
  selector: 'app-chat',
  standalone: true,
  imports: [StreamingMarkdownComponent],
  template: `
    <ngx-streamdown
      [content]="markdown"
      [mode]="'streaming'"
      [isAnimating]="isStreaming">
    </ngx-streamdown>
  `
})
export class ChatComponent {
  markdown = '# Hello **World**!';
  isStreaming = false;
}

Streaming Example

import { Component } from '@angular/core';
import { StreamingMarkdownComponent } from 'ngx-streamdown';

@Component({
  selector: 'app-ai-chat',
  standalone: true,
  imports: [StreamingMarkdownComponent],
  template: `
    <div class="chat-container">
      <ngx-streamdown
        [content]="streamingContent"
        [mode]="'streaming'"
        [parseIncompleteMarkdown]="true"
        [isAnimating]="isStreaming"
        [showCaret]="true"
        [caret]="'block'"
        [enableKatex]="true">
      </ngx-streamdown>
    </div>
  `
})
export class AIChatComponent {
  streamingContent = '';
  isStreaming = false;

  async streamFromAI() {
    this.isStreaming = true;
    
    // Simulate streaming from an AI API
    const fullResponse = "# AI Response\n\nThis is **streaming** from an AI!";
    
    for (let i = 0; i < fullResponse.length; i++) {
      this.streamingContent = fullResponse.substring(0, i + 1);
      await new Promise(resolve => setTimeout(resolve, 50));
    }
    
    this.isStreaming = false;
  }
}

With Real AI Streaming

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { StreamingMarkdownComponent } from 'ngx-streamdown';

@Component({
  selector: 'app-ai-chat',
  standalone: true,
  imports: [StreamingMarkdownComponent],
  template: `
    <ngx-streamdown
      [content]="response"
      [mode]="'streaming'"
      [isAnimating]="isStreaming">
    </ngx-streamdown>
  `
})
export class AIChatComponent {
  response = '';
  isStreaming = false;

  constructor(private http: HttpClient) {}

  async streamResponse(prompt: string) {
    this.isStreaming = true;
    this.response = '';

    const response = await fetch('/api/ai/stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt }),
    });

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    if (!reader) return;

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value);
      this.response += chunk;
    }

    this.isStreaming = false;
  }
}

Component API

Inputs

Input Type Default Description
content string '' The markdown content to render
mode 'static' | 'streaming' 'streaming' Rendering mode
parseIncompleteMarkdown boolean true Apply remend to handle incomplete syntax
className string '' Additional CSS classes
enableKatex boolean true Enable KaTeX math rendering
enableMermaid boolean false Enable Mermaid diagrams
isAnimating boolean false Whether content is currently streaming
showCaret boolean true Show cursor caret when streaming
caret 'block' | 'bar' | 'underscore' 'block' Caret style
debounceTime number 16 Debounce time in ms (~60fps)
remendOptions RemendOptions undefined Options for remend parser
enableBlockParsing boolean true Parse into blocks for progressive rendering

Service API

StreamingMarkdownService

Injectable service for processing markdown:

import { StreamingMarkdownService } from 'ngx-streamdown';

@Component({
  // ...
})
export class MyComponent {
  constructor(private streamingService: StreamingMarkdownService) {}

  processMarkdown(text: string) {
    // Process with remend
    const processed = this.streamingService.processMarkdown(text, {
      mode: 'streaming',
      parseIncompleteMarkdown: true,
    });

    // Parse into blocks
    const blocks = this.streamingService.parseIntoBlocks(processed);

    // Check for incomplete syntax
    const hasIncomplete = this.streamingService.hasIncompleteSyntax(text);
  }
}

Methods

  • processMarkdown(markdown: string, config?: StreamdownConfig): string - Process markdown with optional remend
  • parseIntoBlocks(markdown: string): string[] - Parse markdown into renderable blocks
  • hasIncompleteSyntax(markdown: string): boolean - Check if markdown has incomplete syntax
  • updateMarkdown(markdown: string, config?: StreamdownConfig): void - Update the markdown observable
  • reset(): void - Reset the markdown content

Styling

The component includes default styles, but you can customize them using CSS variables:

ngx-streamdown {
  --font-family: 'Inter', system-ui, sans-serif;
  --font-size: 1rem;
  --line-height: 1.6;
  --text-color: #1f2937;
  --link-color: #2563eb;
  --code-bg: rgba(175, 184, 193, 0.2);
  --border-color: #e5e7eb;
  --table-header-bg: #f9fafb;
}

Or use custom classes:

<ngx-streamdown
  [content]="markdown"
  className="my-custom-markdown">
</ngx-streamdown>
.my-custom-markdown {
  font-family: 'Georgia', serif;
  font-size: 1.125rem;
}

.my-custom-markdown h1 {
  color: #2563eb;
}

Advanced Usage

Custom Remend Options

<ngx-streamdown
  [content]="markdown"
  [remendOptions]="{
    bold: true,
    italic: true,
    inlineCode: true,
    links: false,
    images: false
  }">
</ngx-streamdown>

Disable Block Parsing

For small content or when you want single-block rendering:

<ngx-streamdown
  [content]="markdown"
  [enableBlockParsing]="false">
</ngx-streamdown>

Custom Debounce Time

Adjust rendering frequency:

<ngx-streamdown
  [content]="markdown"
  [debounceTime]="50"> <!-- ~20fps -->
</ngx-streamdown>

Comparison with React Version

Feature React (streamdown) Angular (ngx-streamdown)
Streaming Support
Remend Integration
Block Parsing
KaTeX Support ✅ (via ngx-markdown)
Mermaid Support ✅ (via ngx-markdown)
Code Highlighting Shiki Prism (ngx-markdown)
Framework React Angular 17+

Performance Tips

  1. Use OnPush strategy - The component already uses OnPush change detection
  2. Adjust debounceTime - Lower values = smoother but more CPU intensive
  3. Disable block parsing for short content
  4. Use trackBy - Built-in for efficient ngFor rendering
  5. Virtual scrolling - For very long conversations, wrap in a virtual scroll container

Examples

See the examples directory for complete working examples:

  • streaming-example.component.ts - Basic streaming demonstration

Demo App

A full Angular demo app lives in demo-app. It showcases streaming controls, presets, and theming.

# Build the library first
npm install
npm run build

# Run the demo
cd demo-app
npm install
npm start

Development

# Install dependencies
npm install

# Build the library
npm run build

# Run tests
npm test

# Lint
npm run lint

Related Projects

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors