
Research
/Security News
Laravel Lang Compromised with RCE Backdoor Across 700+ Versions
Laravel Lang packages were compromised with an RCE backdoor across hundreds of versions, exposing cloud, CI/CD, and developer secrets.
@syncagent/angular
Advanced tools
Angular SDK for SyncAgent — injectable service with Angular Signals and RxJS observables for AI database chat.
Works with MongoDB, PostgreSQL, MySQL, SQLite, SQL Server, and Supabase.
sa_)Every new project gets a 14-day trial with 500 free requests — no credit card required. After the trial, you get 100 free requests/month on the Free plan.
npm install @syncagent/angular @syncagent/js
// app.component.ts
import { Component, OnInit } from "@angular/core";
import { SyncAgentService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-root",
providers: [SyncAgentService],
template: `
<div class="chat">
<div *ngIf="agent.status() as s" class="status">⏳ {{ s.label }}</div>
<div class="messages">
<div *ngFor="let msg of agent.messages()" [class]="'message ' + msg.role">
{{ msg.content }}
</div>
</div>
<div *ngIf="agent.error() as err" class="error">⚠️ {{ err.message }}</div>
<div class="input-row">
<input [(ngModel)]="input" (keydown.enter)="send()" placeholder="Ask about your data..." [disabled]="agent.isLoading()" />
<button (click)="send()" [disabled]="agent.isLoading() || !input.trim()">Send</button>
<button *ngIf="agent.isLoading()" (click)="agent.stop()">Stop</button>
<button (click)="agent.reset()">Clear</button>
</div>
</div>
`,
})
export class AppComponent implements OnInit {
input = "";
constructor(public agent: SyncAgentService) {}
ngOnInit() {
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
}
send() {
if (!this.input.trim()) return;
this.agent.sendMessage(this.input);
this.input = "";
}
}
// environments/environment.ts
export const environment = {
production: false,
syncagentKey: "sa_your_api_key",
databaseUrl: "mongodb+srv://user:pass@cluster/db",
};
⚠️ Security note: In production, pass the connection string from your backend API instead of bundling it in the client.
@Component({ providers: [SyncAgentService] })
export class DashboardComponent implements OnInit {
constructor(
public agent: SyncAgentService,
private authService: AuthService,
) {}
ngOnInit() {
const user = this.authService.currentUser;
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
filter: { organizationId: user.orgId },
operations: user.isAdmin
? ["read", "create", "update", "delete"]
: ["read"],
context: { userId: user.id, userRole: user.role },
});
}
}
import { Subject, takeUntil, filter } from "rxjs";
@Component({ providers: [SyncAgentService] })
export class ChatComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(public agent: SyncAgentService) {}
ngOnInit() {
this.agent.configure({ apiKey: "...", connectionString: "..." });
// Subscribe to messages stream
this.agent.messages$
.pipe(takeUntil(this.destroy$))
.subscribe(messages => console.log("Messages:", messages.length));
// Subscribe to each new assistant message
this.agent.message$
.pipe(takeUntil(this.destroy$))
.subscribe(msg => console.log("New:", msg.content.slice(0, 50)));
// Subscribe to status updates
this.agent.status$
.pipe(takeUntil(this.destroy$), filter(Boolean))
.subscribe(({ step, label }) => console.log(`[${step}] ${label}`));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
tools: {
sendEmail: {
description: "Send an email to a user",
inputSchema: {
to: { type: "string", description: "Recipient email" },
subject: { type: "string", description: "Subject line" },
body: { type: "string", description: "Email body" },
},
execute: async ({ to, subject, body }) => {
await this.emailService.send({ to, subject, body });
return { sent: true };
},
},
},
onData: (data) => {
if (data.collection === "orders") {
this.orders = data.data;
}
},
});
Use toolsOnly: true when you want the agent to only call your custom tools — no database access:
this.agent.configure({
apiKey: environment.syncagentKey,
toolsOnly: true,
tools: {
searchProducts: {
description: "Search products by name",
inputSchema: { query: { type: "string", description: "Search query" } },
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
createTicket: {
description: "Create a support ticket",
inputSchema: {
title: { type: "string" },
message: { type: "string" },
},
execute: async ({ title, message }) => {
return await this.ticketService.create({ title, message });
},
},
},
});
The CustomerChatService is a dedicated Angular service for building customer-facing support interfaces. It routes messages through SyncAgent's customer support pipeline — using your configured persona, flows, knowledge base, and escalation rules — instead of direct database access.
Provide it at the component or module level and call configure() before sending messages.
import { Component, OnInit } from "@angular/core";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-support-widget",
providers: [CustomerChatService],
template: `
<div class="chat">
@if (chat.welcomeMessage()) {
<p class="welcome">{{ chat.welcomeMessage() }}</p>
}
<div class="messages">
@for (msg of chat.messages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
@if (chat.isEscalated()) {
<p class="notice">You've been connected to a human agent.</p>
}
@if (chat.error() as err) {
<p class="error">{{ err.message }}</p>
}
<div class="input-row">
<input
[(ngModel)]="input"
(keydown.enter)="send()"
placeholder="How can we help?"
[disabled]="chat.isLoading()"
/>
<button (click)="send()" [disabled]="chat.isLoading() || !input.trim()">Send</button>
</div>
@if (chat.isResolved()) {
<div class="rating">
<p>How was your experience?</p>
<button *ngFor="let r of [1,2,3,4,5]" (click)="rate(r)">{{ r }}⭐</button>
</div>
}
</div>
`,
})
export class SupportWidgetComponent implements OnInit {
input = "";
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "user_abc123",
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Conversation resolved:", id),
});
}
send() {
if (!this.input.trim()) return;
this.chat.sendMessage(this.input);
this.input = "";
}
rate(rating: number) {
this.chat.rateConversation(rating);
}
}
| Signal | Type | Description |
|---|---|---|
messages() | Message[] | Conversation message history |
conversationId() | string | null | Current conversation ID |
isLoading() | boolean | True while waiting for a response |
isEscalated() | boolean | True when escalated to a human agent |
isResolved() | boolean | True when the conversation is resolved |
error() | Error | null | Last error, if any |
welcomeMessage() | string | null | Welcome message (first interaction only) |
isConfigured() | boolean | True after configure() has been called |
| Observable | Type | Description |
|---|---|---|
messages$ | Observable<Message[]> | Conversation history stream |
isLoading$ | Observable<boolean> | Loading state stream |
error$ | Observable<Error | null> | Error stream |
escalated$ | Observable<void> | Emits when escalated to a human agent |
resolved$ | Observable<string> | Emits the conversation ID when resolved |
CustomerChatServiceConfigExtends SyncAgentConfig with customer-mode-specific options:
| Property | Type | Description |
|---|---|---|
apiKey | string | Your SyncAgent API key |
connectionString | string | Database connection string |
customerMode | true | Enables customer agent mode |
externalUserId | string | Unique identifier for the end-user |
onEscalated | () => void | Optional callback when escalated to a human agent |
onResolved | (conversationId: string) => void | Optional callback when the conversation is resolved |
| Method | Signature | Description |
|---|---|---|
configure(config) | configure(config: CustomerChatServiceConfig): void | Initialize the service with customer mode config. Must be called before other methods. |
sendMessage(content, metadata?) | sendMessage(content: string, metadata?: Record<string, any>): Promise<void> | Send a message and receive an AI response. Ignored if content is empty or already loading. |
rateConversation(rating) | rateConversation(rating: number): Promise<void> | Rate the current conversation (1-5). Throws if no active conversation. |
reset() | reset(): void | Clear all state (messages, conversation ID, errors) and start fresh. |
When no externalUserId is provided, the CustomerChatService activates the guest identification flow. Guests must identify themselves before sending messages. The service exposes Angular signals and RxJS observables for reactive state management.
| Signal / Observable | Type | Description |
|---|---|---|
isIdentified() | Signal<boolean> | true when the guest has been identified (via form or stored identity) |
guestIdentity() | Signal<GuestIdentity | null> | The current guest identity, or null if not yet identified |
isIdentified$ | Observable<boolean> | Emits the current identification state (BehaviorSubject) |
guestIdentified$ | Observable<GuestIdentity> | Emits the GuestIdentity when identification completes |
identifyGuest(data)| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Guest's display name |
email | string | Yes | Guest's email address |
phone | string | No | Guest's phone number |
Validates input, generates a deterministic guest identifier from the email, persists the identity to localStorage, updates signals/observables, and invokes the onGuestIdentified callback if configured.
import { Component, OnInit } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-support-widget",
providers: [CustomerChatService],
imports: [FormsModule],
template: `
@if (!chat.isIdentified()) {
<form (ngSubmit)="identify()">
<h2>Welcome</h2>
<p>Please introduce yourself to get started</p>
<label for="name">Name</label>
<input id="name" [(ngModel)]="name" name="name" required />
<label for="email">Email</label>
<input id="email" [(ngModel)]="email" name="email" type="email" required />
<label for="phone">Phone (optional)</label>
<input id="phone" [(ngModel)]="phone" name="phone" type="tel" />
@if (chat.error() as err) {
<p class="error">{{ err.message }}</p>
}
<button type="submit">Start Chat</button>
</form>
} @else {
<div class="chat">
<div class="messages">
@for (msg of chat.messages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
<div class="input-row">
<input
[(ngModel)]="input"
(keydown.enter)="send()"
placeholder="How can we help?"
[disabled]="chat.isLoading()"
/>
<button (click)="send()" [disabled]="chat.isLoading() || !input.trim()">Send</button>
</div>
</div>
}
`,
})
export class SupportWidgetComponent implements OnInit {
name = "";
email = "";
phone = "";
input = "";
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
onGuestIdentified: (identity) => {
console.log("Guest identified:", identity.guestId);
},
});
}
identify() {
this.chat.identifyGuest({
name: this.name,
email: this.email,
phone: this.phone || undefined,
});
}
send() {
if (!this.input.trim()) return;
this.chat.sendMessage(this.input);
this.input = "";
}
}
guestIdentified$import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-guest-chat",
providers: [CustomerChatService],
template: `<!-- ... -->`,
})
export class GuestChatComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
// React to guest identification
this.chat.guestIdentified$
.pipe(takeUntil(this.destroy$))
.subscribe((identity) => {
console.log(`Guest ${identity.name} identified as ${identity.guestId}`);
// e.g., send analytics event, update UI state
});
// Track identification state changes
this.chat.isIdentified$
.pipe(takeUntil(this.destroy$))
.subscribe((identified) => {
console.log("Identification state:", identified);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
⚠️ Deprecated:
createDual()will be removed in a future major version. UseDualChatServiceinstead — passexternalUserIddirectly to enable both modes on a single instance.
Legacy pattern (deprecated):
import { Component, OnInit } from "@angular/core";
import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentService, CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-dual-chat",
providers: [SyncAgentService, CustomerChatService],
template: `<!-- admin + customer UIs -->`,
})
export class DualChatComponent implements OnInit {
constructor(
public admin: SyncAgentService,
public support: CustomerChatService,
) {}
ngOnInit() {
const { db, support } = SyncAgentClient.createDual({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
});
// Admin: direct database queries
this.admin.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
// Customer: support pipeline
this.support.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
customerMode: true,
externalUserId: "customer_123",
});
}
}
Before (deprecated):
const { db, support } = SyncAgentClient.createDual({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
});
After (recommended):
import { DualChatService } from "@syncagent/angular";
@Component({
providers: [DualChatService],
})
export class DualChatComponent implements OnInit {
constructor(public dual: DualChatService) {}
ngOnInit() {
this.dual.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
});
}
}
Tip: In Angular, you can also use
createDual()to get both client instances and pass them to your services' internal logic, or simply configure each service independently as shown above.
The DualChatService provides a single injectable service that manages both database agent and customer support chat from one component. Instead of wiring up SyncAgentService and CustomerChatService separately, inject DualChatService and call configure() with your config including externalUserId — both modes become available immediately.
import { Component, OnInit } from "@angular/core";
import { DualChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-dual-chat",
providers: [DualChatService],
template: `
<div class="dual-chat">
<section class="db-panel">
<h3>Database Agent</h3>
<div class="messages">
@for (msg of dual.dbMessages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
@if (dual.dbError() as err) {
<p class="error">{{ err.message }}</p>
}
<div class="input-row">
<input
[(ngModel)]="dbInput"
(keydown.enter)="sendDb()"
placeholder="Ask about your data..."
[disabled]="dual.dbIsLoading()"
/>
<button (click)="sendDb()" [disabled]="dual.dbIsLoading() || !dbInput.trim()">Send</button>
</div>
</section>
<section class="support-panel">
<h3>Customer Support</h3>
<div class="messages">
@for (msg of dual.supportMessages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
@if (dual.supportIsEscalated()) {
<p class="notice">Connected to a human agent.</p>
}
@if (dual.supportError() as err) {
<p class="error">{{ err.message }}</p>
}
<div class="input-row">
<input
[(ngModel)]="supportInput"
(keydown.enter)="sendSupport()"
placeholder="How can we help?"
[disabled]="dual.supportIsLoading()"
/>
<button (click)="sendSupport()" [disabled]="dual.supportIsLoading() || !supportInput.trim()">Send</button>
</div>
</section>
</div>
`,
})
export class DualChatComponent implements OnInit {
dbInput = "";
supportInput = "";
constructor(public dual: DualChatService) {}
ngOnInit() {
this.dual.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "user_abc123",
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Resolved:", id),
});
}
sendDb() {
if (!this.dbInput.trim()) return;
this.dual.sendDbMessage(this.dbInput);
this.dbInput = "";
}
sendSupport() {
if (!this.supportInput.trim()) return;
this.dual.sendSupportMessage(this.supportInput);
this.supportInput = "";
}
}
If sendDbMessage(), sendSupportMessage(), stop(), or reset() is called before configure(), the service throws:
DualChatService: call configure() first
If sendSupportMessage() is called without externalUserId in the config, the service throws:
DualChatService: externalUserId is required for customer chat
| Signal | Type | Description |
|---|---|---|
dbMessages() | Message[] | Database agent conversation message history |
dbIsLoading() | boolean | True while the database agent is processing |
dbError() | Error | null | Last error from the database agent, if any |
supportMessages() | Message[] | Customer support conversation message history |
supportIsLoading() | boolean | True while the support agent is processing |
supportError() | Error | null | Last error from the support agent, if any |
supportConversationId() | string | null | Current support conversation ID |
supportIsEscalated() | boolean | True when escalated to a human agent |
supportIsResolved() | boolean | True when the support conversation is resolved |
| Observable | Type | Description |
|---|---|---|
dbMessages$ | Observable<Message[]> | Database agent message history stream |
dbIsLoading$ | Observable<boolean> | Database agent loading state stream |
dbError$ | Observable<Error | null> | Database agent error stream |
supportMessages$ | Observable<Message[]> | Support agent message history stream |
supportIsLoading$ | Observable<boolean> | Support agent loading state stream |
supportError$ | Observable<Error | null> | Support agent error stream |
DualChatServiceConfig| Property | Type | Description |
|---|---|---|
apiKey | string | Your SyncAgent API key |
connectionString | string | Database connection string |
externalUserId | string | Unique identifier for the end-user (enables customer mode) |
context | Record<string, any> | Optional context passed to both agents |
onData | (data: ToolData) => void | Optional callback when the database agent returns query results |
onEscalated | () => void | Optional callback when the support conversation is escalated to a human agent |
onResolved | (conversationId: string) => void | Optional callback when the support conversation is resolved |
| Method | Signature | Description |
|---|---|---|
configure(config) | configure(config: DualChatServiceConfig): void | Initialize the service with dual-mode config. Must be called before other methods. |
sendDbMessage(content) | sendDbMessage(content: string): Promise<void> | Send a message to the database agent and receive a streamed response. |
sendSupportMessage(content, metadata?) | sendSupportMessage(content: string, metadata?: Record<string, any>): Promise<void> | Send a message to the customer support agent. Requires externalUserId in config. |
stop() | stop(): void | Abort any in-progress streams for both agents. |
reset() | reset(): void | Clear all state (messages, errors, conversation) for both agents and start fresh. |
The <syncagent-customer-chat> component is a pre-built, drop-in customer support chat widget. It handles the full conversation lifecycle — guest identification, AI messaging, escalation to human agents via Pusher, and satisfaction rating — with zero custom UI required.
import { SyncAgentCustomerChatComponent } from "@syncagent/angular";
@Component({
imports: [SyncAgentCustomerChatComponent],
template: `
<syncagent-customer-chat
[apiKey]="'sa_your_api_key'"
[connectionString]="'your_connection_string'"
(escalated)="onEscalated()"
(resolved)="onResolved($event)"
/>
`,
})
export class AppComponent {
onEscalated() { console.log("Escalated to human agent"); }
onResolved(id: string) { console.log("Resolved:", id); }
}
| Input | Type | Default | Description |
|---|---|---|---|
config | SyncAgentConfig | — | Pre-built config object. When provided, apiKey/connectionString are ignored. |
apiKey | string | — | API key for authentication (ignored if config provided) |
connectionString | string | — | Database connection string (ignored if config provided) |
externalUserId | string | — | Authenticated user ID — skips guest form when provided |
mode | "floating" | "inline" | "floating" | Floating toggle button or embedded inline panel |
position | "bottom-right" | "bottom-left" | "bottom-right" | Position of the floating widget (floating mode only) |
defaultOpen | boolean | false | Whether the floating panel starts open (floating mode only) |
title | string | "Customer Support" | Header title text (max 100 chars) |
subtitle | string | "How can we help you?" | Header subtitle text (max 200 chars) |
placeholder | string | "Type your message..." | Input placeholder text (max 150 chars) |
welcomeMessage | string | — | Initial welcome message displayed before any interaction |
accentColor | string | "#6366f1" | Primary accent color (hex, rgb, or hsl) |
darkMode | boolean | false | Enable dark mode color scheme |
className | string | — | Additional CSS class on the root container |
guestForm | GuestFormConfig | — | Custom guest form configuration (title, subtitle, placeholders, button text) |
pusherKey | string | — | Pusher app key for real-time human agent messages |
pusherCluster | string | "us2" | Pusher cluster |
metadata | Record<string, any> | — | Custom metadata attached to conversations |
| Output | Payload | Description |
|---|---|---|
escalated | void | Emitted when the conversation is escalated to a human agent |
resolved | string | Emitted when the conversation is resolved (payload is the conversation ID) |
guestIdentified | GuestIdentity | Emitted after guest form submission with the guest identity object |
Basic template usage:
@Component({
imports: [SyncAgentCustomerChatComponent],
template: `
<syncagent-customer-chat
[apiKey]="'sa_your_api_key'"
[connectionString]="'postgresql://user:pass@host:5432/db'"
[externalUserId]="currentUser.id"
/>
`,
})
export class SupportPageComponent {
currentUser = { id: "user_123" };
}
Using a config object:
import { SyncAgentConfig } from "@syncagent/js";
@Component({
imports: [SyncAgentCustomerChatComponent],
template: `<syncagent-customer-chat [config]="chatConfig" />`,
})
export class SupportPageComponent {
chatConfig: SyncAgentConfig = {
apiKey: "sa_your_api_key",
connectionString: "postgresql://user:pass@host:5432/db",
customerMode: true,
externalUserId: "user_123",
};
}
Floating mode (default):
<syncagent-customer-chat
[apiKey]="apiKey"
[connectionString]="connectionString"
mode="floating"
position="bottom-right"
[defaultOpen]="false"
/>
Inline mode:
<div style="height: 600px;">
<syncagent-customer-chat
[apiKey]="apiKey"
[connectionString]="connectionString"
mode="inline"
/>
</div>
Dark mode with custom accent color:
<syncagent-customer-chat
[apiKey]="apiKey"
[connectionString]="connectionString"
[darkMode]="true"
accentColor="#8b5cf6"
title="Night Owl Support"
subtitle="We're here around the clock"
/>
Handling output events:
@Component({
imports: [SyncAgentCustomerChatComponent],
template: `
<syncagent-customer-chat
[apiKey]="apiKey"
[connectionString]="connectionString"
(escalated)="onEscalated()"
(resolved)="onResolved($event)"
(guestIdentified)="onGuestIdentified($event)"
/>
`,
})
export class SupportComponent {
apiKey = "sa_your_api_key";
connectionString = "postgresql://user:pass@host:5432/db";
onEscalated() {
console.log("Conversation escalated to human agent");
}
onResolved(conversationId: string) {
console.log("Conversation resolved:", conversationId);
}
onGuestIdentified(identity: GuestIdentity) {
console.log("Guest identified:", identity.name, identity.guestId);
}
}
The CustomerChatService is the underlying service used by the component. You can also use it directly to build fully custom chat UI while keeping the state management and API integration.
Provide it at the component level so each instance gets its own state:
import { CustomerChatService } from "@syncagent/angular";
@Component({
providers: [CustomerChatService],
template: `<!-- your custom UI -->`,
})
export class CustomChatComponent implements OnInit {
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: "sa_your_api_key",
connectionString: "postgresql://user:pass@host:5432/db",
externalUserId: "user_123",
onEscalated: () => console.log("Escalated"),
onResolved: (id) => console.log("Resolved:", id),
});
}
}
| Method | Signature | Description |
|---|---|---|
configure | configure(config: CustomerChatServiceConfig): void | Initialize the service. Must be called before other methods. |
sendMessage | sendMessage(content: string, metadata?: Record<string, any>): Promise<void> | Send a message and receive an AI response. Ignored if empty or already loading. |
rateConversation | rateConversation(rating: number): Promise<void> | Rate the conversation (1–5). Throws if no active conversation. |
identifyGuest | identifyGuest(data: { name: string; email: string; phone?: string }): void | Submit guest identification. Validates, generates ID, persists to localStorage. |
reset | reset(): void | Clear all state and start fresh. |
| Signal | Type | Description |
|---|---|---|
messages() | Message[] | Conversation message history |
conversationId() | string | null | Current conversation ID |
isLoading() | boolean | True while waiting for a response |
isEscalated() | boolean | True when escalated to a human agent |
isResolved() | boolean | True when the conversation is resolved |
error() | Error | null | Last error, if any |
welcomeMessage() | string | null | Welcome message (first interaction only) |
isConfigured() | boolean | True after configure() has been called |
isIdentified() | boolean | True when the guest has been identified |
guestIdentity() | GuestIdentity | null | Current guest identity |
| Observable | Type | Description |
|---|---|---|
messages$ | Observable<Message[]> | Conversation history stream |
isLoading$ | Observable<boolean> | Loading state stream |
error$ | Observable<Error | null> | Error stream |
escalated$ | Observable<void> | Emits when escalated to a human agent |
resolved$ | Observable<string> | Emits the conversation ID when resolved |
isIdentified$ | Observable<boolean> | Identification state stream |
guestIdentified$ | Observable<GuestIdentity> | Emits the identity when guest identification completes |
The component uses the shared theme engine (computeTheme() from @syncagent/js) to generate a full color palette from your accent color and dark mode flag.
Accent color — Set the accentColor input to any valid CSS color (hex, rgb, hsl). This controls buttons, user message bubbles, focus outlines, and the header background.
<syncagent-customer-chat accentColor="#8b5cf6" />
Dark mode — Set [darkMode]="true" to switch to a dark color scheme. All generated colors maintain WCAG AA contrast ratios.
<syncagent-customer-chat [darkMode]="true" />
Host element classes — Use the className input to add CSS classes to the root container for additional styling:
<syncagent-customer-chat className="my-chat-widget" />
.my-chat-widget {
/* Override positioning, sizing, or add transitions */
max-height: 80vh;
}
The component uses inline styles and scoped CSS class prefixes (syncagent-cc-*) to prevent style leakage in both directions. A CSS custom property --syncagent-accent is set on focus-visible styles for focus indicator theming.
The component is built to meet WCAG 2.1 AA standards:
ARIA roles and labels:
role="region" with aria-label="Customer support chat"role="log" with aria-live="polite" and aria-relevant="additions"role="form" with aria-label="Message input"role="group" with aria-label="Rate your experience"role="radiogroup" with individual role="radio" and aria-checkedrole="status" with aria-live="polite"aria-expanded reflecting panel stateKeyboard navigation:
Tab moves focus through interactive elements (toggle, close, input, send, rating stars)Enter / Space activates buttons and submits formsArrow Left / Arrow Right navigates between rating stars (roving tabindex)Enter on the text input submits the messageFocus management:
Contrast requirements:
SyncAgentServiceMethods:
| Method | Description |
|---|---|
configure(config) | Initialize with API key, connection string, and options |
sendMessage(content) | Send a message and start streaming |
stop() | Abort the current stream |
reset() | Clear all messages |
Angular Signals:
| Signal | Type | Description |
|---|---|---|
messages() | Message[] | Conversation history |
isLoading() | boolean | True while streaming |
error() | Error | null | Last error |
status() | {step,label} | null | Live status (connecting, querying, thinking, done) |
lastData() | ToolData | null | Last DB query result |
RxJS Observables:
| Observable | Type | Description |
|---|---|---|
messages$ | Observable<Message[]> | Conversation history stream |
isLoading$ | Observable<boolean> | Loading state stream |
error$ | Observable<Error|null> | Error stream |
status$ | Observable<{step,label}|null> | Status stream |
message$ | Observable<Message> | Emits each new assistant message |
| Database | Connection String Format |
|---|---|
| MongoDB | mongodb+srv://user:pass@cluster.mongodb.net/mydb |
| PostgreSQL | postgresql://user:pass@host:5432/mydb |
| MySQL | mysql://user:pass@host:3306/mydb |
| SQLite | /absolute/path/to/database.sqlite |
| SQL Server | Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true; |
| Supabase | https://xxx.supabase.co|your-anon-key |
| Plan | Requests/mo | Collections | Price |
|---|---|---|---|
| Free (+ 14-day trial) | 100 (500 during trial) | 5 | GH₵0 |
| Starter | 5,000 | 20 | GH₵150/mo |
| Pro | 50,000 | Unlimited | GH₵500/mo |
| Enterprise | Unlimited | Unlimited | Custom |
MIT
FAQs
SyncAgent Angular SDK — service and component for AI database chat
The npm package @syncagent/angular receives a total of 406 weekly downloads. As such, @syncagent/angular popularity was classified as not popular.
We found that @syncagent/angular demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Laravel Lang packages were compromised with an RCE backdoor across hundreds of versions, exposing cloud, CI/CD, and developer secrets.

Security News
Socket found a malicious postinstall hook across 700+ GitHub repos, including PHP packages on Packagist and Node.js project repositories.

Security News
Vibe coding at scale is reshaping how packages are created, contributed, and selected across the software supply chain