Skip to content

Angular Guide

Install

bash
npm install @askable-ui/angular @askable-ui/core

Quick start

Inject AskableService into any component and apply the [askable] directive to annotate elements.

ts
// app.component.ts
import { Component, inject } from '@angular/core';
import { AskableService } from '@askable-ui/angular';
import { AskableDirective } from '@askable-ui/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AskableDirective],
  template: `
    <div [askable]="{ metric: 'revenue', value: revenue, period: 'Q3' }">
      <revenue-chart [value]="revenue" />
    </div>

    <button (click)="askAI()">Ask AI</button>
  `,
})
export class AppComponent {
  revenue = '$128k';
  private readonly askable = inject(AskableService);

  async askAI() {
    const prompt = this.askable.promptContext();
    await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        messages: [
          { role: 'system', content: `UI context: ${prompt}` },
          { role: 'user', content: 'What is the user looking at?' },
        ],
      }),
    });
  }
}

AskableService

Provided at root by default — one context instance per app. Inject it anywhere to read focus and prompt context.

ts
import { inject } from '@angular/core';
import { AskableService } from '@askable-ui/angular';

class MyComponent {
  private readonly askable = inject(AskableService);

  // Reactive Angular signal — use in templates with promptContext()
  promptContext = this.askable.promptContext;

  // Current focus signal
  focus = this.askable.focus;

  // Raw AskableContext for advanced usage
  ctx = this.askable.context;
}

Signals

SignalTypeDescription
promptContextSignal<string>Serialized prompt string for the current focus
focusSignal<AskableFocus | null>Current focused element metadata

Methods

MethodDescription
contextRaw AskableContext — for agent requests, region capture, etc.

Use promptContext in a template

html
<p>{{ askable.promptContext() }}</p>

Because promptContext is a Signal, Angular automatically rerenders only the affected part when focus changes.

[askable] directive

Apply to any element to annotate it with context metadata.

html
<!-- Object meta -->
<div [askable]="{ widget: 'churn-rate', value: '4.2%' }">
  <churn-chart />
</div>

<!-- String meta -->
<section askable="pricing page hero">
  <hero-section />
</section>

<!-- With scope -->
<div [askable]="{ chart: 'revenue' }" askableScope="dashboard">
  <revenue-chart />
</div>

AskableModule (NgModule apps)

For apps that use NgModules instead of standalone components, import AskableModule:

ts
import { NgModule } from '@angular/core';
import { AskableModule } from '@askable-ui/angular';

@NgModule({
  imports: [AskableModule],
  // ...
})
export class AppModule {}

Then use [askable] in any template within the module.

Sources

Sources expose app state (cart contents, multistep progress, form data, etc.) to the AI. Each source is an Angular injectable service.

Cart source

ts
import { Component, inject, OnInit } from '@angular/core';
import { AskableCartSourceService } from '@askable-ui/angular';
import type { AskableCartItem } from '@askable-ui/angular';

@Component({
  selector: 'app-cart',
  standalone: true,
  providers: [AskableCartSourceService],
  template: `
    <p>{{ cart.snapshot?.itemCount }} items — {{ cart.snapshot?.total | currency }}</p>
  `,
})
export class CartComponent implements OnInit {
  readonly cart = inject(AskableCartSourceService);

  ngOnInit() {
    this.cart.init({
      items: [],
      totals: { currency: 'USD' },
    });
  }

  addItem(item: AskableCartItem) {
    this.cart.addItem(item);
  }

  removeItem(id: string) {
    this.cart.removeItem(id);
  }

  checkout() {
    this.cart.clearCart();
  }
}

Multistep / wizard source

ts
import { Component, inject, OnInit } from '@angular/core';
import { AskableMultistepSourceService } from '@askable-ui/angular';

@Component({
  selector: 'app-checkout',
  standalone: true,
  providers: [AskableMultistepSourceService],
  template: `
    <p>Step {{ wizard.snapshot?.currentIndex + 1 }} of {{ wizard.snapshot?.totalSteps }}</p>
    <button (click)="wizard.next()">Next</button>
    <button (click)="wizard.prev()">Back</button>
  `,
})
export class CheckoutComponent implements OnInit {
  readonly wizard = inject(AskableMultistepSourceService);

  ngOnInit() {
    this.wizard.init({
      steps: [
        { id: 'cart',     label: 'Cart' },
        { id: 'shipping', label: 'Shipping' },
        { id: 'payment',  label: 'Payment' },
        { id: 'confirm',  label: 'Confirm' },
      ],
    });
  }
}

Other sources

All sources follow the same inject → init() pattern:

ServicePurpose
AskablePageSourceServiceCurrent page title, URL, description
AskableFormSourceServiceForm field values and validation state
AskableTableSourceServiceTable rows, columns, selection
AskableNavigationSourceServiceRoute history
AskableUserSourceServiceAuthenticated user info
AskableNotificationSourceServiceActive toasts and alerts
AskableErrorSourceServiceRecent errors
AskableLoadingSourceServiceLoading/pending states
AskableSearchSourceServiceSearch query and results

Agent requests

Use context.toAgentRequest() to send a structured request that includes the full context packet:

ts
async askAI(question: string) {
  const req = await this.askable.context.toAgentRequest(question, {
    history: 3,
    packet: true,
  });

  await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(req),
  });
}

Region and text selection capture

Region and text selection capture use the raw AskableContext directly:

ts
import { Component, inject, OnInit, OnDestroy } from '@angular/core';
import { createAskableRegionCapture } from '@askable-ui/core';
import type { AskableRegionCaptureHandle } from '@askable-ui/core';
import { AskableService } from '@askable-ui/angular';

@Component({ selector: 'app-annotate', standalone: true, template: `
  <button (click)="startCapture()">Mark region</button>
` })
export class AnnotateComponent implements OnDestroy {
  private readonly askable = inject(AskableService);
  private captureHandle: AskableRegionCaptureHandle | null = null;

  startCapture() {
    this.captureHandle?.destroy();
    this.captureHandle = createAskableRegionCapture(this.askable.context, {
      shape: 'rect',
      onCapture: (packet) => {
        console.log('Region captured:', packet.text);
      },
    });
    this.captureHandle.start();
  }

  ngOnDestroy() {
    this.captureHandle?.destroy();
  }
}

Server-side rendering

AskableService checks typeof document !== 'undefined' before observing the DOM, so it is safe to use in Angular Universal / SSR apps. The service constructor is a no-op on the server and activates automatically in the browser.

Released under the MIT License.