npm package: @primoia/vocall-angular
Description: Angular SDK for Vocall - WebSocket UI automation platform
Author: Primoia
Latest version: 1.0.1

# Vocall SDK for Angular

Integrate voice and chat-driven UI automation into your Angular applications using the Vocall AAP protocol.

---

## Features

- Bidirectional WebSocket communication with the Vocall engine
- Reactive state management via RxJS `BehaviorSubject` observables
- Automatic NgZone integration for seamless change detection
- Screen-scoped field and action registration
- 14 built-in UI actions (fill, navigate, click, highlight, and more)
- Typewriter-style animated field filling
- Automatic reconnection with exponential backoff
- Persistent visitor identification via `localStorage`
- Streaming chat token support for real-time responses
- Compatible with Angular 18 and 19 (standalone components)

## Installation

```bash
npm install @vocall/angular-sdk
```

### Peer Dependencies

The SDK requires the following peer dependencies:

```json
{
  "@angular/common": "^18.0.0 || ^19.0.0",
  "@angular/core": "^18.0.0 || ^19.0.0",
  "rxjs": "^7.8.0"
}
```

## Quick Start

Both `VocallService` and `FieldRegistryService` are provided in root, so no additional module configuration is needed.

### 1. Connect to the Vocall server

```typescript
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import {
  VocallService,
  ManifestMessage,
  FieldType,
  VocallStatus,
} from '@vocall/angular-sdk';

@Component({
  selector: 'app-my-form',
  standalone: true,
  template: `
    <form>
      <label for="name">Name</label>
      <input #nameInput id="name" type="text" />

      <label for="email">Email</label>
      <input #emailInput id="email" type="email" />

      <button type="button" (click)="onSubmit()">Submit</button>
    </form>

    <p>Status: {{ vocall.status$ | async }}</p>
  `,
})
export class MyFormComponent implements OnInit, OnDestroy {
  @ViewChild('nameInput', { static: true }) nameInput!: ElementRef<HTMLInputElement>;
  @ViewChild('emailInput', { static: true }) emailInput!: ElementRef<HTMLInputElement>;

  constructor(public vocall: VocallService) {}

  ngOnInit(): void {
    const manifest: ManifestMessage = {
      type: 'manifest',
      app: 'my-app',
      screens: {
        form: {
          id: 'form',
          label: 'Contact Form',
          fields: [
            { id: 'name', type: FieldType.Text, label: 'Name', required: true },
            { id: 'email', type: FieldType.Email, label: 'Email', required: true },
          ],
          actions: [
            { id: 'submit', label: 'Submit' },
          ],
        },
      },
    };

    // Register fields
    this.vocall.registerField('form', 'name', {
      element: this.nameInput.nativeElement,
      setValue: (v) => (this.nameInput.nativeElement.value = v),
      getValue: () => this.nameInput.nativeElement.value,
    });

    this.vocall.registerField('form', 'email', {
      element: this.emailInput.nativeElement,
      setValue: (v) => (this.emailInput.nativeElement.value = v),
      getValue: () => this.emailInput.nativeElement.value,
    });

    // Register actions
    this.vocall.registerAction('form', 'submit', () => this.onSubmit());

    // Set callbacks
    this.vocall.onNavigate = (screenId) => console.log('Navigate to:', screenId);
    this.vocall.onToast = (message, level) => console.log(`[${level}] ${message}`);

    // Connect
    this.vocall.connect('ws://localhost:12900/connect', manifest);
  }

  onSubmit(): void {
    console.log('Form submitted');
  }

  ngOnDestroy(): void {
    this.vocall.unregisterScreen('form');
    this.vocall.disconnect();
  }
}
```

## API Reference

### VocallService

The primary injectable service. Provided in root -- no module imports required.

#### Observables

| Observable | Type | Description |
|---|---|---|
| `status$` | `Observable<VocallStatus>` | Current engine status (disconnected, idle, thinking, executing, etc.) |
| `connected$` | `Observable<boolean>` | Whether the WebSocket connection is active |
| `messages$` | `Observable<ChatMessage[]>` | Full chat message history (user, agent, system) |
| `sessionId$` | `Observable<string \| null>` | Current session ID assigned by the server |
| `rawMessage$` | `Observable<Record<string, unknown>>` | Every raw incoming server message |

#### Synchronous Getters

| Getter | Type | Description |
|---|---|---|
| `status` | `VocallStatus` | Current status value |
| `connected` | `boolean` | Current connection state |
| `messages` | `ChatMessage[]` | Current message array |
| `sessionId` | `string \| null` | Current session ID |
| `client` | `VocallClient \| null` | Underlying client instance |

#### Methods

```typescript
// Connection
connect(serverUrl: string, manifest: ManifestMessage, options?: { token?: string; visitorId?: string }): void
disconnect(): void

// Messaging
sendText(text: string): void
sendConfirm(seq: number, confirmed: boolean): void
sendResult(seq: number, results: ActionResult[], state?: StateMessage): void
sendState(state: StateMessage): void
clearMessages(): void

// Field & action registration
registerField(screenId: string, fieldId: string, entry: FieldEntry): void
unregisterField(screenId: string, fieldId: string): void
unregisterScreen(screenId: string): void
registerAction(screenId: string, actionId: string, callback: ActionCallback): void
unregisterAction(screenId: string, actionId: string): void
```

#### Callbacks

Set these on the service to handle server-initiated UI actions:

```typescript
// Navigation between screens
vocall.onNavigate = (screenId: string) => { ... };

// Toast/snackbar notifications
vocall.onToast = (message: string, level: string, duration?: number) => { ... };

// Confirmation dialogs
vocall.onConfirm = (seq: number, message: string) => {
  const confirmed = window.confirm(message);
  vocall.sendConfirm(seq, confirmed);
};

// Modal open/close
vocall.onOpenModal = (modalId: string, query?: string) => { ... };
vocall.onCloseModal = () => { ... };
```

### FieldRegistryService

Manages field and action registrations across Angular components. Provided in root.

```typescript
// Field registration
registerField(screenId: string, fieldId: string, entry: FieldEntry): void
unregisterField(screenId: string, fieldId: string): void
unregisterScreen(screenId: string): void

// Action registration
registerAction(screenId: string, actionId: string, callback: ActionCallback): void
unregisterAction(screenId: string, actionId: string): void

// Lookups
getField(screenId: string, fieldId: string): FieldEntry | undefined
findField(fieldId: string): FieldEntry | undefined
getAction(screenId: string, actionId: string): ActionCallback | undefined
findAction(actionId: string): ActionCallback | undefined
```

### FieldEntry

Each registered field must provide:

```typescript
interface FieldEntry {
  element: HTMLElement;                 // The DOM element
  setValue: (value: string) => void;    // Programmatic value setter
  getValue: () => string;              // Current value getter
}
```

### VocallStatus

```typescript
enum VocallStatus {
  Disconnected = 'disconnected',
  Idle         = 'idle',
  Listening    = 'listening',
  Recording    = 'recording',
  Thinking     = 'thinking',
  Speaking     = 'speaking',
  Executing    = 'executing',
}
```

## Manifest Structure

The manifest describes your application's screens, fields, and actions. It is sent to the server immediately after connection.

```typescript
const manifest: ManifestMessage = {
  type: 'manifest',
  app: 'my-application',
  version: '1.0.0',
  currentScreen: 'dashboard',
  user: {
    name: 'John Doe',
    email: 'john@example.com',
    org: 'Acme Corp',
    role: 'admin',
  },
  persona: {
    name: 'Assistant',
    role: 'helper',
    instructions: 'Help the user fill out forms.',
  },
  screens: {
    dashboard: {
      id: 'dashboard',
      label: 'Dashboard',
      route: '/dashboard',
      fields: [
        {
          id: 'search',
          type: FieldType.Text,
          label: 'Search',
          placeholder: 'Search...',
        },
      ],
      actions: [
        { id: 'refresh', label: 'Refresh' },
        { id: 'export', label: 'Export', requiresConfirmation: true },
      ],
      modals: [
        { id: 'details', label: 'Details', searchable: true },
      ],
    },
    form: {
      id: 'form',
      label: 'Registration Form',
      route: '/register',
      fields: [
        { id: 'full_name', type: FieldType.Text, label: 'Full Name', required: true },
        { id: 'birth_date', type: FieldType.Date, label: 'Birth Date' },
        { id: 'country', type: FieldType.Select, label: 'Country', options: [
          { value: 'us', label: 'United States' },
          { value: 'br', label: 'Brazil' },
        ]},
        { id: 'notes', type: FieldType.Textarea, label: 'Notes', maxLength: 500 },
        { id: 'agree', type: FieldType.Checkbox, label: 'I agree to terms' },
      ],
      actions: [
        { id: 'submit', label: 'Submit' },
        { id: 'cancel', label: 'Cancel', destructive: true },
      ],
    },
  },
};
```

### FieldDescriptor Properties

| Property | Type | Description |
|---|---|---|
| `id` | `string` | Unique field identifier within the screen |
| `type` | `FieldType` | Field type (see below) |
| `label` | `string` | Human-readable label |
| `required` | `boolean` | Whether the field is required |
| `mask` | `string` | Input mask pattern (for masked fields) |
| `placeholder` | `string` | Placeholder text |
| `options` | `SelectOption[]` | Options for select/autocomplete fields |
| `source` | `string` | Data source identifier |
| `min` | `number` | Minimum value |
| `max` | `number` | Maximum value |
| `maxLength` | `number` | Maximum character length |
| `readOnly` | `boolean` | Whether the field is read-only |

## Field Types

The `FieldType` enum defines all supported field types:

| Type | Value | Description |
|---|---|---|
| `Text` | `text` | Standard text input |
| `Number` | `number` | Numeric input |
| `Currency` | `currency` | Currency/monetary input |
| `Date` | `date` | Date picker |
| `Datetime` | `datetime` | Date and time picker |
| `Email` | `email` | Email address input |
| `Phone` | `phone` | Phone number input |
| `Masked` | `masked` | Input with mask pattern |
| `Select` | `select` | Dropdown select |
| `Autocomplete` | `autocomplete` | Autocomplete/typeahead input |
| `Checkbox` | `checkbox` | Checkbox toggle |
| `Radio` | `radio` | Radio button group |
| `Textarea` | `textarea` | Multi-line text area |
| `File` | `file` | File upload input |
| `Hidden` | `hidden` | Hidden field |

## UI Actions

The Vocall engine can send 14 UI actions to the client. These are executed automatically by the SDK when a `command` message is received.

| Action | Target | Description |
|---|---|---|
| `navigate` | screen | Navigate to a different screen |
| `fill` | field | Fill a field with a value (supports typewriter animation) |
| `clear` | field | Clear a field's value |
| `select` | field | Set a select/dropdown value |
| `click` | action | Trigger a registered action callback |
| `highlight` | field | Scroll to and visually highlight a field |
| `focus` | field | Focus a field |
| `scroll_to` | field | Scroll a field into view |
| `show_toast` | -- | Display a toast notification |
| `ask_confirm` | -- | Prompt the user for confirmation |
| `open_modal` | modal | Open a modal dialog |
| `close_modal` | -- | Close the current modal |
| `enable` | field | Enable a disabled field |
| `disable` | field | Disable a field |

Actions are categorized internally as **sequential** (`navigate`, `click`, `open_modal`, `close_modal`, `ask_confirm`, `show_toast`) or **parallel** (all others). Sequential actions run in order; parallel actions execute concurrently for performance.

## Demo

A demo application is available in the `demo/` directory of this repository. It provides a working example of field registration, action handling, and WebSocket communication with the Vocall engine.

## Testing

The SDK includes a comprehensive test suite with ~180 tests covering protocol types, field registry, WebSocket client, and the Angular service layer.

### Running Tests

```bash
npm test                # Run all tests
npm test -- --coverage  # Run with coverage report
```

### Test Structure

| File | Tests | What it covers |
|---|---|---|
| `src/lib/protocol/types.spec.ts` | 29 | Protocol type enums, message interfaces, field types |
| `src/lib/services/field-registry.service.spec.ts` | 29 | Field and action registration, lookups, screen management |
| `src/lib/client/vocall-client.spec.ts` | 79 | WebSocket connection, reconnection, command execution, streaming |
| `src/lib/services/vocall.service.spec.ts` | 43 | Angular service wrapper, RxJS observables, NgZone integration |

### Test Configuration

- **Framework:** Jest with ts-jest
- **Environment:** jsdom
- **Config:** [jest.config.ts](jest.config.ts) | [tsconfig.spec.json](tsconfig.spec.json)

## Development

### Building the SDK

```bash
npm install
npm run build
```

### Running the Demo

The demo application is in the `demo/` directory:

```bash
cd demo
npm install
npm start          # http://localhost:4200
```

The demo connects to a Vocall server at `ws://localhost:12900/connect`. See the [workspace README](https://github.com/primoia/primoia-vocall-workspace#quick-start) for local server setup.

### Docker

When running from the workspace, the demo is available as a Docker container:

```bash
# From the workspace root
make serve-all     # Starts engine + all demos
# Angular demo at http://localhost:21002
```

## Related

This SDK is part of the [Primoia Vocall Workspace](https://github.com/primoia/primoia-vocall-workspace).

| Resource | Link |
|---|---|
| Workspace (setup, Docker) | [primoia-vocall-workspace](https://github.com/primoia/primoia-vocall-workspace#readme) |
| Next.js SDK | [vocall-sdk-nextjs](https://github.com/primoia/vocall-sdk-nextjs) |
| Vue SDK | [vocall-sdk-vue](https://github.com/primoia/vocall-sdk-vue) |
| Kotlin SDK | [vocall-sdk-kotlin](https://github.com/primoia/vocall-sdk-kotlin) |
| Swift SDK | [vocall-sdk-swift](https://github.com/primoia/vocall-sdk-swift) |
| Flutter SDK | [jarvis-sdk-flutter](https://github.com/primoia/jarvis-sdk-flutter) |

## License

Proprietary. All rights reserved.