本页还没有译文
本页 67 段还没译成Português (BR),下面显示的是英文原文。@tanstack/ai-angular
还没翻译译自英文(tanstack.com)本页 67 段都还没译
Angular signal-based bindings for TanStack AI, providing convenient Angular bindings for the headless client.
Injection context requirement: Every
inject*function in this package calls Angular'sinject()internally. They must be called within an Angular injection context — a component or directive class field initializer, the constructor, or insiderunInInjectionContext. Calling them outside an injection context will throw a runtime error.
Installation
injectRegisterWebMCPTools(tools, options?)
Register executable client tools for the current Angular injection owner. Angular removes them when it destroys that owner.
For a complete setup and behavior guide, see WebMCP Tools.
import { Component } from "@angular/core";
import {
injectRegisterWebMCPTools,
type InjectRegisterWebMCPToolsOptions,
} from "@tanstack/ai-angular";
import { searchProducts } from "./tools";
const tools = [searchProducts];
const options: InjectRegisterWebMCPToolsOptions<typeof tools> = {
onError(error) {
console.error(error);
},
};
@Component({ selector: "app-products", standalone: true, template: "" })
export class ProductsComponent {
registration = injectRegisterWebMCPTools(tools, options);
}InjectRegisterWebMCPToolsOptions<TTools, TContext> contains toolOptions, context, and onError. The injectable owns the registration signal.
The context field is required when a tool declares a required runtime context. Call this function only in an Angular injection context.
injectPageWebMCPTools(options?)
Read the WebMCP tools on the page as client tools. The signal starts empty and updates when the page adds or removes a tool. Pass the signal to injectChat as tools. Call it in an injection context.
import { Component } from "@angular/core";
import { injectPageWebMCPTools } from "@tanstack/ai-angular";
@Component({ selector: "app-page-tools", standalone: true, template: "" })
export class PageTools {
pageTools = injectPageWebMCPTools({
filter: (tool) => tool.origin === location.origin,
});
}filter skips a tool when it returns false. onError gets a failed WebMCP read. For a complete guide, see Page WebMCP Tools in Chat.
injectChat(options?)
Main injectable for managing chat state in Angular with full type safety.
import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
template: `...`,
})
export class ChatComponent {
// injectChat is called in a field initializer — valid injection context.
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Options
Extends ChatClientOptions from @tanstack/ai-client (minus internal state callbacks):
connection- Connection adapter (required, or usefetcher)fetcher?- Direct async function for one-shot generation (alternative toconnection)tools?- Array of client tool implementations (with.client()method). Reactive: pass aSignalor getter to change the tools after the chat is created.initialMessages?- Initial messages arraythreadId?- The only identity for this chat. Required when persistence is on. If omitted, minted after mount.forwardedProps?- Arbitrary client-controlled JSON forwarded to the server in the AG-UIRunAgentInput.forwardedPropsfield. Reactive — accepts a plain value, an AngularSignal, or a zero-arg getter; changes sync automatically viaeffectbody?- Deprecated. UseforwardedPropsinstead. Still works for backward compatibility; values are merged intoforwardedPropson the wire. Reactive (same forms asforwardedProps)byok?- Optional BYOK keyring fromdefineByok. On each send the client prepares the resolved provider and stampsx-byok-*request headers. Keys never go in the bodybyokProvider?- Optional function that returns the provider slug for this chat. If it returns a slug, only that key is prepared and sent. Otherwise the mergedproviderfromforwardedProps,body, and per-callsendMessagebodyis used. Later sources win. If no slug resolves, the send throws instead of attaching every stored keycontext?- Typed client-local runtime context passed to client tool implementations. Reactive (same forms). This value is not serialized to the serverlive?- Enable live subscription mode (auto-subscribes/unsubscribes). Reactive (same forms)outputSchema?- Standard-schema-compatible schema (Zod, Valibot, ArkType, or JSON Schema). When provided, adds typedpartialandfinalsignals to the return valuepersistence?- Persistence configurationdevtools?- Display options for TanStack AI DevtoolsonResponse?- Callback when response is receivedonChunk?- Callback when stream chunk is receivedonFinish?- Callback when response finishesonError?- Callback when error occursonInterruptStateChange?- Callback when interrupt state changes; context source ishydratefor restored state andlivefor streamed or client-initiated updatesonCustomEvent?- Callback for custom stream eventsstreamProcessor?- Stream processing configuration
Reactive options (body, forwardedProps, context, live) accept a ReactiveOption<T>, which is one of:
import type { Signal } from "@angular/core";
type ReactiveOption<T> = T | Signal<T> | (() => T);A plain value becomes a constant; a Signal is read directly; a zero-arg getter is wrapped in computed so any signals read inside it are tracked.
Note: Client tools are automatically executed — no onToolCall callback needed!
Returns
import type { Signal } from "@angular/core";
import type {
UIMessage,
MultimodalContent,
DeepPartial,
} from "@tanstack/ai-angular";
import type { ModelMessage, InferSchemaType } from "@tanstack/ai/client";
import type {
ChatClientState,
ConnectionStatus,
SendMessageOptions,
} from "@tanstack/ai-client";
type TSchema = any;
interface InjectChatResult {
messages: Signal<UIMessage[]>;
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
clear: () => void;
setMessages: (messages: UIMessage[]) => void;
isLoading: Signal<boolean>;
error: Signal<Error | undefined>;
status: Signal<ChatClientState>;
isSubscribed: Signal<boolean>;
connectionStatus: Signal<ConnectionStatus>;
sessionGenerating: Signal<boolean>;
isHydrating: Signal<boolean>;
// Only present when outputSchema is supplied:
partial: Signal<DeepPartial<InferSchemaType<TSchema>>>;
final: Signal<InferSchemaType<TSchema> | null>;
}Note: All reactive state (messages, isLoading, error, status, isSubscribed, connectionStatus, sessionGenerating, isHydrating) is exposed as read-only Angular Signals. Read them by calling them as functions (e.g., chat.messages(), chat.isLoading()). Cleanup is automatic via DestroyRef.onDestroy.
injectByok(client)
Subscribe to a ByokClient snapshot in Angular. Call it in an injection context. The return value is a read-only Signal.
import { Component } from "@angular/core";
import { injectByok } from "@tanstack/ai-angular";
import { byok } from "./byok";
@Component({
selector: "app-key-status",
standalone: true,
template: `<p>{{ last4() }}</p>`,
})
export class KeyStatusComponent {
snapshot = injectByok(byok);
last4() {
const openai = this.snapshot().status.openai;
return openai && "masked" in openai ? openai.masked : "No key";
}
}snapshot() has status, locked, and prompt. Call byok.update(provider, value) from your own UI to save a key. See Bring Your Own Key.
Connection Adapters
Re-exported from @tanstack/ai-client for convenience:
import {
fetchServerSentEvents,
fetchHttpStream,
fetchJson,
xhrServerSentEvents,
xhrHttpStream,
stream,
rpcStream,
type ConnectionAdapter,
} from "@tanstack/ai-angular";Example: Basic Chat
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
imports: [CommonModule],
template: `
<ul>
@for (message of chat.messages(); track message.id) {
<li>
<strong>{{ message.role }}:</strong>
@for (part of message.parts; track $index) {
@if (part.type === 'thinking') {
<em>Thinking: {{ part.content }}</em>
} @else if (part.type === 'text') {
<span>{{ part.content }}</span>
}
}
</li>
}
</ul>
<input #input placeholder="Type a message..." />
<button
(click)="chat.sendMessage(input.value); input.value = ''"
[disabled]="chat.isLoading()"
>
Send
</button>
@if (chat.isLoading()) {
<p>Thinking...</p>
}
`,
})
export class ChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Example: Tool Approval
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-approval-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (
part.type === 'tool-call' &&
part.state === 'approval-requested' &&
part.approval
) {
<div>
<p>Approve: {{ part.name }}</p>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: true })">
Approve
</button>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: false })">
Deny
</button>
</div>
}
}
}
`,
})
export class ApprovalChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Example: Client Tools with Type Safety
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import {
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const updateUIDef = toolDefinition({
name: "updateUI",
description: "Show a notification in the UI",
inputSchema: z.object({ message: z.string(), type: z.string() }),
});
const saveToStorageDef = toolDefinition({
name: "saveToStorage",
description: "Save a value to localStorage",
inputSchema: z.object({ key: z.string(), value: z.string() }),
});
@Component({
selector: "app-typed-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (part.type === 'tool-call' && part.name === 'updateUI') {
<div>Tool executed: {{ part.name }}</div>
}
}
}
`,
})
export class TypedChatComponent {
// Create client implementations
private updateUI = updateUIDef.client((input) => {
// input is fully typed!
return { success: true };
});
private saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
private tools = [this.updateUI, this.saveToStorage];
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
tools: this.tools, // Automatic execution, full type safety
});
}Example: Reactive Options with Signals
import { Component, signal } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-reactive-chat",
standalone: true,
template: `
<button (click)="toggleLanguage()">Toggle Language</button>
@for (message of chat.messages(); track message.id) {
<p>{{ message.role }}: {{ message.parts[0]?.content }}</p>
}
`,
})
export class ReactiveChatComponent {
language = signal("en");
// forwardedProps is reactive — the signal is read on every request
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
forwardedProps: () => ({ language: this.language() }),
});
toggleLanguage() {
this.language.set(this.language() === "en" ? "fr" : "en");
}
}Example: Structured Output
import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import { z } from "zod";
const recipeSchema = z.object({
title: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
});
@Component({
selector: "app-recipe-chat",
standalone: true,
template: `
<button (click)="chat.sendMessage('Give me a pasta recipe')">Ask</button>
@if (chat.partial().title) {
<h2>{{ chat.partial().title }}</h2>
}
@if (chat.final()) {
<ul>
@for (step of chat.final()!.steps; track $index) {
<li>{{ step }}</li>
}
</ul>
}
`,
})
export class RecipeChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
outputSchema: recipeSchema,
});
}Generation Injectables
Angular injectables for one-shot generation tasks (images, audio, speech, transcription, summarization, video). All share the same pattern: provide a connection or fetcher, call generate(), and read reactive signals.
injectGeneration(options)
Base injectable for custom generation types. All specialized injectables below are built on this.
import { Component } from "@angular/core";
import { injectGeneration } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({ selector: "app-custom", standalone: true, template: `...` })
export class CustomGenerationComponent {
gen = injectGeneration({
connection: fetchServerSentEvents("/api/generate/custom"),
});
// Call gen.generate(input), read gen.result(), gen.isLoading(), etc.
}Options: connection?, fetcher?, threadId?, body? (reactive), devtools?, onResult?, onError?, onProgress?, onChunk?
Returns: generate, result, isLoading, error, status, stop, reset, runId. All reactive state is a read-only Signal<T>.
injectGenerateImage(options)
Image generation injectable. generate() accepts ImageGenerateInput, result is ImageGenerationResult.
import { Component } from "@angular/core";
import { injectGenerateImage } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-image",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A mountain at sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<img [src]="gen.result()!.images[0]!.url" alt="Generated image" />
}
`,
})
export class ImageComponent {
gen = injectGenerateImage({
connection: fetchServerSentEvents("/api/generate/image"),
});
}injectGenerateAudio(options)
Audio generation injectable (music, sound effects). generate() accepts AudioGenerateInput, result is AudioGenerationResult.
import { Component } from "@angular/core";
import { injectGenerateAudio } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-audio",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'An upbeat electronic track', duration: 10 })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<audio [src]="gen.result()!.audio.url" controls></audio>
}
`,
})
export class AudioComponent {
gen = injectGenerateAudio({
connection: fetchServerSentEvents("/api/generate/audio"),
});
}
injectGenerateSpeech(options)
Text-to-speech injectable. generate() accepts SpeechGenerateInput, result is TTSResult.
injectTranscription(options)
Audio transcription injectable. generate() accepts TranscriptionGenerateInput, result is TranscriptionResult.
injectSummarize(options)
Text summarization injectable. generate() accepts SummarizeGenerateInput, result is SummarizationResult.
injectGenerateVideo(options)
Video generation injectable with job polling. Returns additional jobId and videoStatus signals. Accepts extra onJobCreated? and onStatusUpdate? callbacks.
import { Component } from "@angular/core";
import { injectGenerateVideo } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-video",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A time-lapse of a sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.videoStatus()) {
<p>Status: {{ gen.videoStatus()!.status }}</p>
}
@if (gen.result()) {
<video [src]="gen.result()!.url" controls></video>
}
`,
})
export class VideoComponent {
gen = injectGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
onJobCreated: (jobId) => console.log("Job created:", jobId),
});
}
Additional returns (video only):
jobId: Signal<string | null>— The polling job ID, once the server creates itvideoStatus: Signal<VideoStatusInfo | null>— Real-time status updates from the polling loop
All generation injectables automatically clean up via DestroyRef.onDestroy.
Injection Context
Angular's DI system requires that inject() is called during component construction. Every inject* function in this package calls inject() internally. Valid call sites:
import { inject, runInInjectionContext, Injector } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
const injector = inject(Injector);
// Field initializer (recommended)
export class MyComponent {
chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
// Constructor
export class MyComponentAlt {
chat: ReturnType<typeof injectChat>;
constructor() {
this.chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
}
// Inside runInInjectionContext
const chat = runInInjectionContext(injector, () =>
injectChat({ connection: fetchServerSentEvents("/api/chat") }),
);
createChatClientOptions(options)
Helper to create typed chat options (re-exported from @tanstack/ai-client).
import {
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { fetchServerSentEvents } from "@tanstack/ai-angular";
import { tool1, tool2 } from "./tools";
// Create typed tools array (no 'as const' needed!)
const tools = [tool1, tool2];
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;
Types
Re-exported from @tanstack/ai-angular (sourced from @tanstack/ai-client):
UIMessage<TTools>- Message type with tool type parameterInjectChatOptions<TTools, TSchema, TContext>- Chat injectable optionsInjectChatResult<TTools, TSchema>- Chat injectable return typeReactiveOption<T>- Union ofT | Signal<T> | (() => T)for reactive option fieldsDeepPartial<T>- Recursive partial; used to type the in-flightpartialvalueChatRequestBody- Request body typeMultimodalContent- Multimodal content type forsendMessageConnectionAdapter- Connection adapter interfaceInferChatMessages<T>- Extract message type from optionsGenerationClientState- Generation lifecycle stateImageGenerateInput- Image generation input typeAudioGenerateInput- Audio generation input typeSpeechGenerateInput- Speech generation input typeTranscriptionGenerateInput- Transcription input typeSummarizeGenerateInput- Summarization input typeVideoGenerateInput- Video generation input typeVideoGenerateResult- Video generation result typeVideoStatusInfo- Video job status info
Tool authoring types — import directly from @tanstack/ai (not re-exported by @tanstack/ai-angular):
toolDefinition()- Create isomorphic tool definitionToolDefinitionInstance- Tool definition typeClientTool- Client tool typeServerTool- Server tool type
Next Steps
- Getting Started - Learn the basics
- Tools Guide - Learn about the isomorphic tool system
- Client Tools - Learn about client-side tools