npm package: @pillar-ai/angular
Description: Angular SDK for Pillar product copilot — AI assistant for SaaS and web apps
Author: Pillar Team
Latest version: 0.1.15

# @pillar-ai/angular

Angular SDK for the [Pillar](https://trypillar.com) open-source AI copilot — embed a product assistant in your Angular app that executes tasks, not just answers questions. [GitHub](https://github.com/pillarhq/pillar) · [Docs](https://trypillar.com/docs)

[![npm version](https://img.shields.io/npm/v/@pillar-ai/angular)](https://www.npmjs.com/package/@pillar-ai/angular)
[![npm downloads](https://img.shields.io/npm/dm/@pillar-ai/angular)](https://www.npmjs.com/package/@pillar-ai/angular)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/)

## What is Pillar?

Pillar is a product copilot for SaaS and web applications. Users say what they want, and Pillar uses your UI to make it happen — navigating pages, pre-filling forms, and calling your APIs.

For example, a user could ask:

> "Close the Walmart deal as won in Salesforce and notify implementation"

> "Create a P1 bug in Linear for the checkout crash and add it to this sprint"

> "How do I set up a webhook in Stripe?"

Pillar understands the intent, builds a multi-step plan, and executes it client-side with the user's session.

## Documentation

**[View Full Documentation](https://trypillar.com/docs)** | [Angular Quickstart](https://trypillar.com/docs/get-started/quickstart?framework=angular) | [API Reference](https://trypillar.com/docs/reference)

## Requirements

- Angular 17.0.0 or higher
- `@pillar-ai/sdk` (installed automatically as a dependency)

## Installation

```bash
npm install @pillar-ai/angular
```

## Quick Start

### 1. Initialize Pillar in your app config

```typescript
// app.config.ts
import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { PillarService } from '@pillar-ai/angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => {
      const pillar = inject(PillarService);
      return pillar.init({ productKey: 'your-product-key' });
    }),
  ],
};
```

### 2. Use Pillar in your components

```typescript
// help-button.component.ts
import { Component } from '@angular/core';
import { injectPillar } from '@pillar-ai/angular';

@Component({
  selector: 'app-help-button',
  standalone: true,
  template: `
    <button (click)="toggle()">
      {{ isPanelOpen() ? 'Close Help' : 'Get Help' }}
    </button>
  `,
})
export class HelpButtonComponent {
  private pillar = injectPillar();
  isPanelOpen = this.pillar.isPanelOpen;
  toggle = this.pillar.toggle;
}
```

## API Reference

### PillarService

Injectable service that manages the Pillar SDK lifecycle.

```typescript
import { PillarService } from '@pillar-ai/angular';

@Component({...})
export class MyComponent {
  constructor(private pillarService: PillarService) {}
}
```

#### Methods

| Method | Description |
|--------|-------------|
| `init(config)` | Initialize the SDK with your product key |
| `open(options?)` | Open the help panel |
| `close()` | Close the help panel |
| `toggle()` | Toggle the help panel |
| `openArticle(slug)` | Open a specific article |
| `openCategory(slug)` | Navigate to a category |
| `search(query)` | Open search with a query |
| `navigate(view, params?)` | Navigate to a specific view |
| `setTheme(theme)` | Update the panel theme |
| `setTextSelectionEnabled(enabled)` | Toggle text selection popover |
| `on(event, callback)` | Subscribe to SDK events |
| `onTask(taskName, handler)` | Register a task handler |

#### Signals

| Signal | Type | Description |
|--------|------|-------------|
| `state` | `WritableSignal<PillarState>` | Current SDK state |
| `isReady` | `Signal<boolean>` | Whether SDK is ready |
| `isPanelOpen` | `WritableSignal<boolean>` | Whether panel is open |

### injectPillar()

Angular injection function for accessing the Pillar SDK with full functionality.

```typescript
import { injectPillar } from '@pillar-ai/angular';

@Component({...})
export class MyComponent {
  private pillar = injectPillar();

  handleHelp() {
    this.pillar.open({ view: 'chat' });
  }
}
```

### injectHelpPanel()

Simplified injection function focused on panel controls.

```typescript
import { injectHelpPanel } from '@pillar-ai/angular';

@Component({...})
export class HelpMenuComponent {
  private panel = injectHelpPanel();

  isOpen = this.panel.isOpen;
  toggle = this.panel.toggle;
  openChat = this.panel.openChat;
}
```

### PillarPanelComponent

Standalone component for custom panel placement.

```typescript
import { PillarPanelComponent } from '@pillar-ai/angular';

@Component({
  selector: 'app-layout',
  standalone: true,
  imports: [PillarPanelComponent],
  template: `
    <div class="layout">
      <pillar-panel class="help-panel" />
      <main>
        <router-outlet />
      </main>
    </div>
  `,
})
export class LayoutComponent {}
```

**Important**: When using `PillarPanelComponent`, set `panel.container: 'manual'` in your config:

```typescript
pillar.init({
  productKey: 'your-product-key',
  config: { panel: { container: 'manual' } }
});
```

## Advanced Usage

### Type-Safe Task Handlers

Define your actions and get full TypeScript support:

```typescript
// lib/pillar/actions.ts
import { defineActions } from '@pillar-ai/sdk';

export const actions = defineActions({
  invite_member: {
    description: 'Invite a team member',
    data: {
      email: { type: 'string', description: 'Email address' },
      role: { type: 'string', description: 'Member role' },
    },
  },
});
```

```typescript
// my.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { injectPillar } from '@pillar-ai/angular';
import { actions } from './lib/pillar/actions';

@Component({...})
export class MyComponent implements OnInit, OnDestroy {
  private pillar = injectPillar<typeof actions>();
  private unsubscribe?: () => void;

  ngOnInit() {
    // TypeScript knows the exact shape of data
    this.unsubscribe = this.pillar.onTask('invite_member', (data) => {
      console.log(data.email); // ✓ Typed as string
      console.log(data.role);  // ✓ Typed as string
    });
  }

  ngOnDestroy() {
    this.unsubscribe?.();
  }
}
```

### Inline UI with Render Components

For `inline_ui` tools, use the `render` prop to display custom Angular components in the chat. The AI agent provides data directly to the render component — no `execute` function needed:

```typescript
// invite-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-invite-card',
  standalone: true,
  imports: [FormsModule],
  template: `
    <div class="invite-card">
      <h3>Invite Team Members</h3>
      <input [(ngModel)]="email" placeholder="Email address" />
      <button (click)="confirm()">Send Invite</button>
      <button (click)="cancel()">Cancel</button>
    </div>
  `,
})
export class InviteCardComponent {
  @Input() data: any;
  @Output() onConfirm = new EventEmitter<Record<string, unknown>>();
  @Output() onCancel = new EventEmitter<void>();

  email = '';

  confirm() {
    this.onConfirm.emit({ email: this.email });
  }

  cancel() {
    this.onCancel.emit();
  }
}
```

```typescript
// invite-tools.component.ts
import { Component } from '@angular/core';
import { injectPillarTool } from '@pillar-ai/angular';
import { InviteCardComponent } from './invite-card.component';

@Component({
  selector: 'app-invite-tools',
  standalone: true,
  template: '',
})
export class InviteToolsComponent {
  constructor() {
    injectPillarTool({
      name: 'invite_member',
      description: 'Invite a new team member via email',
      type: 'inline_ui',
      inputSchema: {
        type: 'object',
        properties: {
          suggestedEmail: { type: 'string', description: 'Pre-fill email if mentioned' },
        },
      },
      render: InviteCardComponent,
    });
  }
}
```

The render component receives:
- `data` (via `@Input()`) — Data provided by the AI agent (extracted from the conversation via `inputSchema`)
- `onConfirm` (via `@Output()`) — emit when the user confirms/completes the action
- `onCancel` (via `@Output()`) — emit when the user cancels

> **Note:** The `cards` config on `pillar.init()` is deprecated. Use the `render` prop on `injectPillarTool()` instead for better co-location and type safety.

### Theme Synchronization

Sync Pillar's theme with your app's dark mode:

```typescript
@Component({...})
export class ThemeToggleComponent {
  private pillar = injectPillar();

  toggleDarkMode(isDark: boolean) {
    this.pillar.setTheme({ mode: isDark ? 'dark' : 'light' });
  }
}
```

### Subscribe to Events

```typescript
@Component({...})
export class AnalyticsComponent implements OnInit, OnDestroy {
  private pillar = injectPillar();
  private unsubscribes: (() => void)[] = [];

  ngOnInit() {
    this.unsubscribes.push(
      this.pillar.on('panel:open', () => {
        analytics.track('help_panel_opened');
      }),
      this.pillar.on('task:execute', (task) => {
        analytics.track('task_executed', { name: task.name });
      })
    );
  }

  ngOnDestroy() {
    this.unsubscribes.forEach(unsub => unsub());
  }
}
```

## Configuration Options

```typescript
interface PillarInitConfig {
  /** Your product key from app.trypillar.com */
  productKey: string;

  /** Additional SDK configuration */
  config?: {
    panel?: {
      /** Panel placement: 'auto' | 'manual' */
      container?: string;
      /** Use Shadow DOM for style isolation */
      useShadowDOM?: boolean;
    };
    theme?: {
      /** Theme mode: 'light' | 'dark' | 'auto' */
      mode?: string;
      /** Custom colors */
      colors?: {
        primary?: string;
        // ... other color options
      };
    };
    // ... other options
  };

  /** Global task handler */
  onTask?: (task: TaskExecutePayload) => void;

  /** Custom card components */
  cards?: Record<string, Type<any>>;
}
```

## License

MIT