npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@syncagent/react

v0.6.0

Published

SyncAgent React SDK — AI database chat widget & hooks

Downloads

917

Readme

@syncagent/react

React SDK for SyncAgent — drop-in AI database chat widget and hooks for React apps.

Works with MongoDB, PostgreSQL, MySQL, SQLite, SQL Server, and Supabase.

npm version License: MIT

Get Your API Key

  1. Sign up for a free account
  2. Go to your DashboardNew Project → choose your database type
  3. Copy your API key (starts with 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.

Install

npm install @syncagent/react @syncagent/js

Quick Start

import { SyncAgentChat } from "@syncagent/react";

export default function App() {
  return (
    <SyncAgentChat
      config={{
        apiKey: "sa_your_api_key",
        connectionString: process.env.DATABASE_URL,
      }}
    />
  );
}

A floating chat button appears in the bottom-right corner. Your users can now query your database in plain English.

<SyncAgentChat> Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | SyncAgentConfig | Required* | API key, connection string, tools, filter, operations | | mode | "floating" \| "inline" | "floating" | Floating FAB or embedded inline panel | | position | "bottom-right" \| "bottom-left" | "bottom-right" | FAB position (floating mode only) | | defaultOpen | boolean | false | Start with the panel open | | title | string | "SyncAgent" | Header title | | subtitle | string | "AI Database Assistant" | Header subtitle | | placeholder | string | "Ask anything..." | Input placeholder | | welcomeMessage | string | "Hi! I can query..." | Empty state message | | accentColor | string | "#10b981" | Brand color for header, FAB, send button | | suggestions | string[] | 3 defaults | Quick-start suggestion chips | | persistKey | string | — | localStorage key for conversation persistence | | context | Record<string, any> | — | Extra context injected into every message | | filter | Record<string, any> | — | Mandatory query filter for multi-tenancy | | operations | ("read"\|"create"\|"update"\|"delete")[] | — | Restrict operations for this session | | onReaction | (idx, reaction, content) => void | — | Called when user reacts 👍/👎 | | onData | (data: ToolData) => void | — | Called when a DB tool returns structured data |

*config is required unless wrapped in <SyncAgentProvider>.

Inline Mode

Embed the chat inside your layout instead of a floating button:

<div style={{ height: 600 }}>
  <SyncAgentChat
    config={{ apiKey: "...", connectionString: "..." }}
    mode="inline"
  />
</div>

Custom UI with useSyncAgent

Build your own chat UI with full control:

import { SyncAgentProvider, useSyncAgent } from "@syncagent/react";

export default function App() {
  return (
    <SyncAgentProvider config={{ apiKey: "...", connectionString: "..." }}>
      <MyChat />
    </SyncAgentProvider>
  );
}

function MyChat() {
  const { messages, isLoading, error, status, lastData, sendMessage, stop, reset } = useSyncAgent();

  return (
    <div>
      {status && <div>⏳ {status.label}</div>}
      {messages.map((msg, i) => (
        <div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
      ))}
      <button onClick={() => sendMessage("Show all users")}>Ask</button>
      <button onClick={stop}>Stop</button>
      <button onClick={reset}>Clear</button>
    </div>
  );
}

useSyncAgent Returns

| Return | Type | Description | |--------|------|-------------| | messages | Message[] | Full conversation history | | isLoading | boolean | true while streaming | | error | Error \| null | Last error | | status | { step, label } \| null | Live status while agent is working | | lastData | ToolData \| null | Last structured data from a DB tool | | sendMessage | (content: string) => void | Send a user message | | stop | () => void | Abort the current stream | | reset | () => void | Clear all messages |

Features

  • Auto page detection — detects current page, record ID, and query params from the URL
  • Live status — shows ● Querying users... while the agent works
  • Markdown rendering — tables, code blocks, bold, italic, lists
  • Streaming — blinking cursor while text streams in
  • Copy button — on every AI response
  • Reactions — 👍/👎 on AI messages
  • Conversation persistence — saves history to localStorage
  • Suggestion chips — configurable quick-start prompts
  • Export CSV — download tables as CSV
  • Bar charts — auto-renders aggregation results
  • Resize handle — drag to resize the floating panel
  • Mobile responsive — full-width on small screens
  • Dark mode — respects prefers-color-scheme

Multi-tenant SaaS

Pass filter to scope every agent operation to the current user's organization. Enforced server-side.

<SyncAgentChat
  config={{
    apiKey: "sa_your_key",
    connectionString: process.env.DATABASE_URL,
    filter: { organizationId: currentUser.orgId },
    operations: currentUser.isAdmin
      ? ["read", "create", "update", "delete"]
      : ["read"],
  }}
/>

Custom Tools

Give the agent capabilities beyond your database:

<SyncAgentChat
  config={{
    apiKey: "sa_your_key",
    connectionString: process.env.DATABASE_URL,
    tools: {
      createInvoice: {
        description: "Create a Stripe invoice for a customer",
        inputSchema: {
          customerId: { type: "string", description: "Stripe customer ID" },
          amount:     { type: "number", description: "Amount in cents" },
        },
        execute: async ({ customerId, amount }) => {
          const inv = await stripe.invoices.create({ customer: customerId });
          return { invoiceId: inv.id };
        },
      },
    },
  }}
/>

Tools-only Mode

Use the agent with only your custom tools — no database access:

<SyncAgentChat
  config={{
    apiKey: "sa_your_key",
    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();
        },
      },
    },
  }}
/>

Customer Agent Mode

Use the useCustomerChat hook to build customer-facing support interfaces powered by SyncAgent's customer agent pipeline — including persona, flows, knowledge base, escalation, and AI fallback.

import { SyncAgentProvider, useCustomerChat } from "@syncagent/react";

function App() {
  return (
    <SyncAgentProvider
      config={{
        apiKey: "sa_your_api_key",
        connectionString: process.env.DATABASE_URL,
        externalUserId: currentUser.id,
      }}
    >
      <CustomerChat />
    </SyncAgentProvider>
  );
}

function CustomerChat() {
  const {
    messages,
    conversationId,
    isLoading,
    isEscalated,
    isResolved,
    error,
    welcomeMessage,
    sendMessage,
    rateConversation,
    reset,
  } = useCustomerChat({
    onEscalated: () => console.log("Escalated to human agent"),
    onResolved: (id) => console.log("Conversation resolved:", id),
  });

  return (
    <div>
      {welcomeMessage && <p>{welcomeMessage}</p>}
      {messages.map((msg, i) => (
        <div key={i}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}
      {isEscalated && <p>You've been connected to a human agent.</p>}
      {isResolved && (
        <div>
          <p>Conversation resolved!</p>
          <button onClick={() => rateConversation(5)}>⭐ Rate 5/5</button>
        </div>
      )}
      {error && <p>Error: {error.message}</p>}
      <button onClick={() => sendMessage("I need help with my order")}>
        Send
      </button>
      <button onClick={reset}>New Conversation</button>
    </div>
  );
}

useCustomerChat Return Values

| Return | Type | Description | |--------|------|-------------| | messages | Message[] | Full conversation history (user and assistant messages) | | conversationId | string \| null | Current conversation ID, set after first message | | isLoading | boolean | true while waiting for a response | | isEscalated | boolean | true when conversation has been escalated to a human agent | | isResolved | boolean | true when the conversation has been resolved | | error | Error \| null | Last error encountered, or null | | welcomeMessage | string \| null | Welcome message returned on first interaction | | sendMessage | (content: string, metadata?: Record<string, any>) => Promise<void> | Send a message to the customer agent | | rateConversation | (rating: number) => Promise<void> | Rate the conversation (1-5). Throws if no active conversation. | | reset | () => void | Clear all state and start a new conversation |

UseCustomerChatOptions

| Field | Type | Description | |-------|------|-------------| | client | SyncAgentClient? | Optional client instance. If omitted, uses the client from SyncAgentProvider context. | | onEscalated | () => void? | Called when the conversation is escalated to a human agent | | onResolved | (conversationId: string) => void? | Called when the conversation is resolved |

Client resolution: useCustomerChat first checks for a client passed directly in options. If none is provided, it falls back to the client from the nearest <SyncAgentProvider>. If neither is available, it throws an error prompting you to provide one.

Guest Identification

When no externalUserId is provided in the config, the customer chat enters guest mode — anonymous visitors must identify themselves before sending messages. The useCustomerChat hook exposes guest identification state and a method to submit guest data programmatically.

Hook Fields

| Return | Type | Description | |--------|------|-------------| | isIdentified | boolean | true if the user has an externalUserId or has completed guest identification | | guestIdentity | GuestIdentity \| null | The stored guest identity object, or null if not yet identified | | identifyGuest | (data: { name: string; email: string; phone?: string }) => void | Submit guest form data — validates, generates a guest ID, persists to localStorage, and transitions to identified state |

Using GuestIdentificationForm

The GuestIdentificationForm component provides a ready-made form with built-in validation, accessibility attributes, and customizable text:

import { useCustomerChat, GuestIdentificationForm } from "@syncagent/react";

function CustomerChat() {
  const {
    messages,
    isIdentified,
    identifyGuest,
    sendMessage,
  } = useCustomerChat({
    onGuestIdentified: (identity) => {
      console.log("Guest identified:", identity.guestId);
    },
  });

  if (!isIdentified) {
    return (
      <GuestIdentificationForm
        onSubmit={(identity) => identifyGuest(identity)}
        config={{
          title: "Welcome!",
          subtitle: "Tell us a bit about yourself",
          submitButtonText: "Start Chat",
          namePlaceholder: "Your name",
          emailPlaceholder: "[email protected]",
          phonePlaceholder: "Phone (optional)",
        }}
        className="my-guest-form"
      />
    );
  }

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
      ))}
      <button onClick={() => sendMessage("Hello!")}>Send</button>
    </div>
  );
}

GuestIdentificationFormProps

| Prop | Type | Required | Description | |------|------|----------|-------------| | onSubmit | (identity: GuestIdentity) => void | Yes | Called with the complete GuestIdentity (including generated guestId) on valid submission | | config | GuestFormConfig | No | Customize title, subtitle, button text, and placeholder strings | | className | string | No | Custom CSS class applied to the form container |

Manual Guest Identification

If you prefer to build your own form UI, use the identifyGuest method directly:

import { useState } from "react";
import { useCustomerChat } from "@syncagent/react";

function CustomGuestForm() {
  const { isIdentified, identifyGuest, error } = useCustomerChat();
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  if (isIdentified) return null;

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        identifyGuest({ name, email });
      }}
    >
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
      <button type="submit">Continue</button>
      {error && <p>{error.message}</p>}
    </form>
  );
}

The identifyGuest method validates the input using validateGuestForm from @syncagent/js. If validation fails, it sets the error state with a descriptive message. On success, it generates a deterministic guest identifier from the email, persists the identity to localStorage, and sets isIdentified to true.

Dual Mode (Database + Customer Agent)

⚠️ Deprecated: createDual() will be removed in a future major version. Use useDualChat() instead — pass externalUserId directly to enable both modes on a single instance.

Before (deprecated):

import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentProvider, useCustomerChat, useSyncAgent } from "@syncagent/react";

const { db, support } = SyncAgentClient.createDual({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  externalUserId: currentUser.id,
});

function AdminChat() {
  return <SyncAgentProvider config={db}><AdminPanel /></SyncAgentProvider>;
}

function CustomerWidget() {
  const chat = useCustomerChat({ client: support });
  return <div>{/* customer chat UI */}</div>;
}

After (recommended):

import { SyncAgentProvider, useDualChat } from "@syncagent/react";

function App() {
  return (
    <SyncAgentProvider
      config={{
        apiKey: "sa_your_key",
        connectionString: process.env.DATABASE_URL,
        externalUserId: currentUser.id,
      }}
    >
      <DualChatUI />
    </SyncAgentProvider>
  );
}

function DualChatUI() {
  const { db, support } = useDualChat();
  // Use db.sendMessage(), support.sendMessage(), etc.
}

Legacy pattern (deprecated):

import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentProvider, useCustomerChat, useSyncAgent } from "@syncagent/react";

const { db, support } = SyncAgentClient.createDual({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  externalUserId: currentUser.id,
});

// Admin panel — database agent
function AdminChat() {
  return <SyncAgentProvider config={db}><AdminPanel /></SyncAgentProvider>;
}

// Customer widget — support agent
function CustomerWidget() {
  const chat = useCustomerChat({ client: support });
  return <div>{/* customer chat UI */}</div>;
}

Customer Chat Component

The <SyncAgentCustomerChat> component is a pre-built, drop-in customer support chat widget. It provides a complete experience including guest identification, real-time messaging with AI and human agents, escalation display, and satisfaction rating — all as a single component.

import { SyncAgentCustomerChat } from "@syncagent/react";

export default function App() {
  return (
    <SyncAgentCustomerChat
      apiKey="sa_your_api_key"
      connectionString={process.env.DATABASE_URL!}
    />
  );
}

<SyncAgentCustomerChat> Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | SyncAgentConfig | — | Server 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 root container | | style | CSSProperties | — | Inline styles merged onto 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 | | onEscalated | () => void | — | Called when conversation is escalated to a human agent | | onResolved | (conversationId: string) => void | — | Called when conversation is resolved | | onGuestIdentified | (identity: GuestIdentity) => void | — | Called after guest form submission |

Basic Usage

<SyncAgentCustomerChat
  apiKey="sa_your_api_key"
  connectionString={process.env.DATABASE_URL!}
/>

Using a Config Object

import { SyncAgentCustomerChat } from "@syncagent/react";

// Config from server (e.g., Next.js createCustomerServerConfig)
<SyncAgentCustomerChat config={serverConfig} />

Floating Mode (default)

<SyncAgentCustomerChat
  apiKey="sa_your_api_key"
  connectionString={process.env.DATABASE_URL!}
  mode="floating"
  position="bottom-right"
  defaultOpen={false}
/>

Inline Mode

Embed the chat inside your layout — the panel fills its parent container and is always visible:

<div style={{ height: 600, width: 400 }}>
  <SyncAgentCustomerChat
    apiKey="sa_your_api_key"
    connectionString={process.env.DATABASE_URL!}
    mode="inline"
  />
</div>

Dark Mode with Custom Accent Color

<SyncAgentCustomerChat
  apiKey="sa_your_api_key"
  connectionString={process.env.DATABASE_URL!}
  darkMode
  accentColor="#8b5cf6"
/>

Custom Guest Form

<SyncAgentCustomerChat
  apiKey="sa_your_api_key"
  connectionString={process.env.DATABASE_URL!}
  guestForm={{
    title: "Welcome!",
    subtitle: "Tell us about yourself to get started",
    submitButtonText: "Start Chat",
    namePlaceholder: "Your name",
    emailPlaceholder: "[email protected]",
    phonePlaceholder: "+1 (555) 000-0000",
  }}
  onGuestIdentified={(identity) => {
    console.log("Guest identified:", identity.guestId);
  }}
/>

useCustomerChat Hook

For full control over the UI, use the useCustomerChat hook directly to build a custom chat interface:

import { SyncAgentProvider, useCustomerChat } from "@syncagent/react";

function App() {
  return (
    <SyncAgentProvider config={{ apiKey: "sa_...", connectionString: "..." }}>
      <CustomChat />
    </SyncAgentProvider>
  );
}

function CustomChat() {
  const {
    messages,
    isLoading,
    isEscalated,
    isResolved,
    isIdentified,
    identifyGuest,
    sendMessage,
    rateConversation,
    error,
    welcomeMessage,
  } = useCustomerChat({
    onEscalated: () => console.log("Escalated"),
    onResolved: (id) => console.log("Resolved:", id),
    onGuestIdentified: (identity) => console.log("Guest:", identity),
  });

  if (!isIdentified) {
    return (
      <form onSubmit={(e) => {
        e.preventDefault();
        identifyGuest({ name: "Jane", email: "[email protected]" });
      }}>
        <button type="submit">Identify</button>
      </form>
    );
  }

  return (
    <div>
      {welcomeMessage && <p>{welcomeMessage}</p>}
      {messages.map((msg, i) => (
        <div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
      ))}
      {isEscalated && <p>Connected to a human agent</p>}
      {isResolved && <button onClick={() => rateConversation(5)}>⭐ Rate</button>}
      {error && <p>Error: {error.message}</p>}
      <button onClick={() => sendMessage("Hello!")} disabled={isLoading}>
        Send
      </button>
    </div>
  );
}

UseCustomerChatOptions

| Option | Type | Description | |--------|------|-------------| | client | SyncAgentClient | Optional client instance. If omitted, uses the client from SyncAgentProvider context. | | externalUserId | string | When provided, the hook considers the user pre-authenticated (bypasses guest flow). | | onEscalated | () => void | Called when the conversation is escalated to a human agent. | | onResolved | (conversationId: string) => void | Called when the conversation is resolved. | | onGuestIdentified | (identity: GuestIdentity) => void | Called when a guest completes identification. |

useCustomerChat Return Values

| Return | Type | Description | |--------|------|-------------| | messages | Message[] | Full conversation history | | conversationId | string \| null | Current conversation ID, set after first message | | isLoading | boolean | true while waiting for a response | | isEscalated | boolean | true when conversation has been escalated to a human agent | | isResolved | boolean | true when the conversation has been resolved | | error | Error \| null | Last error encountered | | welcomeMessage | string \| null | Welcome message returned on first interaction | | sendMessage | (content: string, metadata?: Record<string, any>) => Promise<void> | Send a message to the customer agent | | rateConversation | (rating: number) => Promise<void> | Rate the conversation (1–5). Throws if no active conversation. | | reset | () => void | Clear all state and start a new conversation | | isIdentified | boolean | true if the user has an externalUserId or has completed guest identification | | guestIdentity | GuestIdentity \| null | The stored guest identity object | | identifyGuest | (data: { name: string; email: string; phone?: string }) => void | Submit guest form data — validates, generates a guest ID, persists to localStorage |

Theming

The <SyncAgentCustomerChat> component uses a built-in theme engine to generate a complete color palette from two inputs:

  • accentColor — any valid CSS color (hex, rgb, hsl). Used for buttons, user message bubbles, focus outlines, and the floating toggle button. Default: "#6366f1".
  • darkMode — when true, renders a dark color scheme with light text on dark backgrounds.

The theme engine (computeTheme() from @syncagent/js) automatically adjusts all derived colors to meet WCAG AA contrast requirements:

  • Text contrast: All text-on-background pairs maintain a minimum 4.5:1 contrast ratio
  • Focus indicators: Focus outlines maintain a minimum 3:1 contrast ratio against adjacent colors
  • Accent contrast: The accent color against the background maintains a minimum 3:1 contrast ratio

All styles are applied inline — no external CSS is required and no styles leak to or from your application.

// Light mode with custom accent
<SyncAgentCustomerChat accentColor="#059669" />

// Dark mode with default accent
<SyncAgentCustomerChat darkMode />

// Dark mode with custom accent
<SyncAgentCustomerChat darkMode accentColor="#f59e0b" />

Accessibility

The <SyncAgentCustomerChat> component is built with WCAG AA compliance:

ARIA Roles and Labels:

  • The chat container uses role="region" with aria-label="Customer chat"
  • The message list uses role="log" to indicate a chronological message stream
  • All interactive elements have descriptive ARIA labels

Keyboard Navigation:

  • Tab moves focus through all interactive elements (buttons, inputs, rating stars)
  • Enter or Space activates buttons and submits forms
  • Arrow keys navigate the satisfaction rating stars

Focus Management:

  • When the guest form transitions to the chat panel, focus moves to the message input within 100ms
  • All interactive elements have visible focus indicators with a minimum 2px outline width
  • Focus indicator contrast meets the 3:1 minimum ratio against adjacent colors

Screen Reader Support:

  • New messages are announced via an ARIA live region (aria-live="polite")
  • Loading states, errors, and escalation status changes are communicated to assistive technology

Contrast Requirements:

  • All text maintains a minimum 4.5:1 contrast ratio against its background
  • Focus indicators maintain a minimum 3:1 contrast ratio
  • These guarantees hold in both light and dark mode, regardless of the accent color chosen

Unified Dual Mode

The useDualChat() hook provides a single interface for managing both database agent and customer support agent conversations from within a <SyncAgentProvider>. When your config includes externalUserId, both modes are available through namespaced db and support objects.

import { SyncAgentProvider, useDualChat } from "@syncagent/react";

function App() {
  return (
    <SyncAgentProvider
      config={{
        apiKey: "sa_your_api_key",
        connectionString: process.env.DATABASE_URL,
        externalUserId: currentUser.id,
      }}
    >
      <DualChatUI />
    </SyncAgentProvider>
  );
}

function DualChatUI() {
  const { db, support } = useDualChat({
    context: { tenant: currentUser.orgId },
    onData: (data) => console.log("DB tool result:", data),
    onEscalated: () => console.log("Escalated to human"),
    onResolved: (id) => console.log("Resolved:", id),
  });

  return (
    <div>
      {/* Database agent panel */}
      <section>
        <h2>Admin Database Chat</h2>
        {db.messages.map((msg, i) => (
          <div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
        ))}
        {db.isLoading && <p>Loading...</p>}
        {db.error && <p>Error: {db.error.message}</p>}
        <button onClick={() => db.sendMessage("Show all orders")}>
          Query DB
        </button>
        <button onClick={db.stop}>Stop</button>
        <button onClick={db.reset}>Clear</button>
      </section>

      {/* Customer support panel */}
      <section>
        <h2>Customer Support</h2>
        {support.welcomeMessage && <p>{support.welcomeMessage}</p>}
        {support.messages.map((msg, i) => (
          <div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
        ))}
        {support.isLoading && <p>Loading...</p>}
        {support.isEscalated && <p>Connected to a human agent.</p>}
        {support.isResolved && (
          <button onClick={() => support.rateConversation(5)}>⭐ Rate</button>
        )}
        {support.error && <p>Error: {support.error.message}</p>}
        <button onClick={() => support.sendMessage("I need help")}>
          Send
        </button>
        <button onClick={support.reset}>New Conversation</button>
      </section>
    </div>
  );
}

Error Handling

If useDualChat() is called outside of a <SyncAgentProvider>, it throws:

useDualChat must be used within a <SyncAgentProvider>

Ensure your component is wrapped in a <SyncAgentProvider> with a valid config that includes externalUserId.

DualChatReturn

The useDualChat() hook returns an object with two namespaces: db for database agent state and support for customer agent state.

db Properties

| Property | Type | Description | |----------|------|-------------| | messages | Message[] | Full database agent conversation history | | isLoading | boolean | true while the database agent is streaming | | error | Error \| null | Last error from the database agent, or null | | status | { step: string; label: string } \| null | Live status while the agent is working (e.g., querying) | | lastData | ToolData \| null | Last structured data returned by a DB tool | | sendMessage | (content: string) => void | Send a message to the database agent | | stop | () => void | Abort the current database agent stream | | reset | () => void | Clear all database agent messages and state |

support Properties

| Property | Type | Description | |----------|------|-------------| | messages | Message[] | Full customer support conversation history | | conversationId | string \| null | Current conversation ID, set after first message | | isLoading | boolean | true while waiting for a support response | | isEscalated | boolean | true when conversation has been escalated to a human agent | | isResolved | boolean | true when the conversation has been resolved | | error | Error \| null | Last error from the support agent, or null | | welcomeMessage | string \| null | Welcome message returned on first interaction | | sendMessage | (content: string, metadata?: Record<string, any>) => Promise<void> | Send a message to the customer support agent | | rateConversation | (rating: number) => Promise<void> | Rate the conversation (1-5). Throws if no active conversation. | | reset | () => void | Clear all support state and start a new conversation |

UseDualChatOptions

| Option | Type | Required | Description | |--------|------|----------|-------------| | context | Record<string, any> | No | Extra context injected into every database agent message | | onData | (data: ToolData) => void | No | Called when a DB tool returns structured data | | onEscalated | () => void | No | Called when the customer conversation is escalated to a human agent | | onResolved | (conversationId: string) => void | No | Called when the customer conversation is resolved |

Customization Options

All config options from @syncagent/js work here too:

<SyncAgentChat
  config={{
    apiKey: "sa_your_key",
    connectionString: process.env.DATABASE_URL,
    systemInstruction: "You are a friendly sales assistant for Acme Corp.",
    language: "French",
    confirmWrites: true,
    maxResults: 10,
    sensitiveFields: ["ssn", "salary", "creditCard"],
    onBeforeToolCall: (name, args) => { console.log(`[Audit] ${name}`, args); return true; },
    onAfterToolCall: (name, args, result) => { analytics.track("tool_call", { tool: name }); },
  }}
/>

Conversation Persistence

<SyncAgentChat
  config={{ apiKey: "...", connectionString: "..." }}
  persistKey={currentUser.id}
/>

History saves to localStorage under sa_chat_{persistKey}. The "New" button clears it.

Context & Auto Page Detection

The SDK automatically detects the current page from window.location — zero config needed:

URL: /dashboard/orders/ord_123?tab=details

Auto-detected:
  currentPage: "orders"
  currentPath: "/dashboard/orders/ord_123"
  currentRecordId: "ord_123"
  param_tab: "details"

Pass additional context:

<SyncAgentChat
  config={{ apiKey: "...", connectionString: "..." }}
  context={{ userId: currentUser.id, userRole: "admin" }}
/>

Vanilla JS Widget

No npm required — drop a script tag into any HTML page:

<script src="https://syncagentdev.vercel.app/api/v1/widget"></script>
<script>
  SyncAgent.init({
    apiKey: "sa_your_key",
    connectionString: "your_database_url",
    position: "right",
    accentColor: "#10b981",
    title: "AI Assistant",
    persistKey: "my-app",
  });
</script>

Security

  • Your database connection string is never stored on SyncAgent servers
  • Passed at runtime, used once, immediately discarded
  • API keys are hashed with bcrypt
  • Never expose your connection string in client-side code — use server components or API routes

Plans & Pricing

| 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 |

View full pricing →

Resources

TypeScript Types

The following types are exported from @syncagent/react for use in your TypeScript projects:

| Type | Description | Import | |------|-------------|--------| | DualChatReturn | Return type of the useDualChat() hook containing namespaced db and support objects | import { DualChatReturn } from "@syncagent/react" | | UseDualChatOptions | Options interface for configuring the useDualChat() hook (context, callbacks) | import { UseDualChatOptions } from "@syncagent/react" |

import { DualChatReturn, UseDualChatOptions } from "@syncagent/react";

License

MIT