Integration

Plain signals — no AI library

End-to-end streaming chat using only Angular signals, fetch, and ReadableStream. No Hashbrown, no Vercel AI SDK, no NgRx. Use this as a reference when wiring ShadNG to a custom backend.

What this shows

ShadNG components consume signal-driven inputs for state, value, and attachments. The library doesn't import any AI framework — you bring your own state. This example uses a backend that returns server-sent events (SSE), streamed chunk by chunk into a signal.

Component

chat.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  signal,
} from '@angular/core';
import {
  PromptInput,
  PromptInputState,
  PromptInputSubmit,
  PromptInputSubmitEvent,
  PromptInputTextarea,
  PromptInputToolbar,
} from '@shadng/prompt-input';

@Component({
  selector: 'app-chat',
  imports: [PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputSubmit],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col gap-4">
      <pre class="whitespace-pre-wrap rounded-md bg-muted p-4 text-sm">{{ response() }}</pre>

      <prompt-input
        [(value)]="prompt"
        [state]="state()"
        (submitted)="onSubmit($event)"
        (canceled)="onCancel()"
        (retried)="onSubmit({ value: prompt(), attachments: [], preventDefault: noop })"
      >
        <prompt-input-textarea placeholder="Ask anything…" />
        <prompt-input-toolbar>
          <prompt-input-submit />
        </prompt-input-toolbar>
      </prompt-input>
    </div>
  `,
})
export class ChatComponent {
  prompt = signal('');
  state = signal<PromptInputState>('ready');
  response = signal('');

  private controller: AbortController | null = null;
  protected readonly noop = () => undefined;

  async onSubmit({ value }: PromptInputSubmitEvent) {
    this.state.set('submitted');
    this.response.set('');
    this.controller = new AbortController();

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ prompt: value }),
        signal: this.controller.signal,
      });

      if (!res.ok || !res.body) {
        throw new Error('Network error');
      }

      this.state.set('streaming');

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

      while (true) {
        const { value: chunk, done } = await reader.read();
        if (done) break;
        this.response.update((prev) => prev + decoder.decode(chunk));
      }

      this.state.set('ready');
      this.prompt.set('');
    } catch (err) {
      if ((err as Error).name === 'AbortError') {
        this.state.set('ready');
        return;
      }
      this.state.set('error');
    }
  }

  onCancel() {
    this.controller?.abort();
  }
}

Backend (any framework)

The server endpoint streams chunks of plain text:

POST /api/chat
// Express example — same pattern works on any framework.
app.post('/api/chat', async (req, res) => {
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.setHeader('Transfer-Encoding', 'chunked');

  const stream = await yourLlm.stream(req.body.prompt);

  for await (const chunk of stream) {
    res.write(chunk);
  }
  res.end();
});

What's happening

  1. State machine. The container's state input drives the submit button visual and the aria-live announcements. We move through ready → submitted → streaming → ready on success, or error on failure.
  2. Submit handler. On (submitted) we POST to the backend and read the response body via ReadableStream. Each chunk appended to the response() signal — your message renderer reads from there.
  3. Cancel. On (canceled) (Esc or Stop button click during streaming) we abort the underlying AbortController.
  4. Retry. On (retried) (Submit click in error state) we re-fire the same request.

Adapting to Hashbrown or Vercel AI SDK

Replace the manual fetch + ReadableStream with the library's helpers, but keep the same state signal driving the container. Hashbrown's chatResource() exposes a status() signal that maps 1:1 to our four states. Vercel AI SDK's Angular adapter is similar.

Dedicated integration docs for Hashbrown and Vercel AI SDK are planned for v0.1.1.

MIT © Kalvnerv0.1.0 · pre-release