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

@smartdatahq/embedded-agent

v0.3.1

Published

SmartData Embedded Agent

Readme

npm license

Installation Guide: Embedded Agent

N:B: This installation guide is for installing the npm package, to run the app locally, check the readme in the repo root.

The @smartdatahq/embedded-agent package is a powerful tool for integrating AI-driven conversational agents into your web applications. It offers two integration paths:

  • Embedded Agent — A pre-built, full-featured chat UI with all the bells and whistles (Ant Design components, theming, popups, etc.)
  • Headless Agent — React hooks and a provider that give you complete control over the UI while handling all the WebSocket, messaging, and state management under the hood.

Table of Contents

Prerequisites

Before using the @smartdatahq/embedded-agent package, ensure the following:

  1. You have Node.js (version 14 or higher) and npm installed on your system.
  2. You have a valid identifier to connect to the @smartdatahq/embedded-agent service. Make sure you contact us to get your identifier.

Installation

To install the package, use npm or yarn:

Using npm

npm install @smartdatahq/embedded-agent

Using yarn

yarn add @smartdatahq/embedded-agent

Embedded Agent (Pre-built UI)

The embedded agent gives you a ready-made chat interface with theming, minimization, popups, and all standard UI features.

Import the Component
import { EmbeddedAgent } from "@smartdatahq/embedded-agent";
import "@smartdatahq/embedded-agent/dist/index.css";

If your application is mostly SSR, you might need to dynamically import the package since it needs to run on the browser. For example, in next.js, you would want to import it like this

const EmbeddedAgent = dynamic(
  () => import("@smartdatahq/embedded-agent").then((mod) => mod.EmbeddedAgent),
  {
    ssr: false,
  },
);
Minimal Example

Use the following example to set up the agent in your application:

const App = () => {
  const [minimized, setMinimized] = useState(true);
  const [isRendered, setIsRendered] = useState(true);

  return (
    <>
    <button
      type="button"
      onClick={() => setIsRendered(!isRendered)}
      style={{ padding: "8px 16px", }}
    >
      Toggle Rendered
    </button>
    <div style={{ height: minimized ? 68 : 400 }}>
      <h1>My Embedded Agent Application</h1>
      <EmbeddedAgent
        identifier="YOUR_IDENTIFIER" // Replace with your unique identifier
        minimized={minimized}
        onMinimizedChange={() => setMinimized(!minimized)}
        isRendered={isRendered}
      />
    </div>
    </>
  );
};

Headless Agent (Custom UI)

The headless agent gives you full control over the UI while the package handles WebSocket connections, message parsing, streaming, conversation state, and audio.

Import from the headless subpath — no CSS import needed:

import { AgentProvider, useAgent } from "@smartdatahq/embedded-agent/headless";

Quick Start

Wrap your component tree with AgentProvider and use the useAgent() convenience hook:

import { AgentProvider, useAgent } from "@smartdatahq/embedded-agent/headless";

function ChatUI() {
  const {
    messages,
    sendMessage,
    isBotThinking,
    isStreaming,
    isConnected,
    conversationId,
    configLoading,
    configError,
  } = useAgent();

  if (configLoading) return <p>Loading agent...</p>;
  if (configError) return <p>Failed to load agent configuration.</p>;

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i} className={msg.user === "user" ? "user-msg" : "bot-msg"}>
          {msg.message}
        </div>
      ))}

      {isBotThinking && <p>Thinking...</p>}

      <input
        type="text"
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            sendMessage(e.currentTarget.value);
            e.currentTarget.value = "";
          }
        }}
        disabled={!isConnected}
      />
    </div>
  );
}

export default function App() {
  return (
    <AgentProvider config={{ identifier: "YOUR_IDENTIFIER" }}>
      <ChatUI />
    </AgentProvider>
  );
}

Message Format

Bot messages are returned in Markdown format. When building your own UI, use a Markdown renderer (e.g. react-markdown, marked, etc.) to display them properly:

import ReactMarkdown from "react-markdown";

{messages.map((msg, i) => (
  <div key={i}>
    {msg.user === "bot" ? (
      <ReactMarkdown>{msg.message}</ReactMarkdown>
    ) : (
      <p>{msg.message}</p>
    )}
  </div>
))}

Using Individual Hooks

For more granular control, use the composable hooks instead of useAgent():

import {
  useAgentConnection,
  useAgentMessages,
  useAgentSend,
  useAgentConversation,
  useAgentAudio,
  useAgentConfig,
} from "@smartdatahq/embedded-agent/headless";

function MyChat() {
  // Connection state
  const { isConnected, reconnect } = useAgentConnection();

  // Read-only message state
  const { messages, isBotThinking, isStreaming, botActivity } = useAgentMessages();

  // Send actions
  const { sendMessage, sendReply, sendFeedback, respondToBrowserToolCall } = useAgentSend();

  // Conversation lifecycle
  const { conversationId, startNewConversation, deleteConversation, downloadChatHistory, getChatUrl } =
    useAgentConversation();

  // Audio (voice input)
  const audio = useAgentAudio();

  // Remote agent configuration + loading state
  const { agentConfig, configLoading, configError } = useAgentConfig();

  // ... build your UI
}

Sending Messages

const { sendMessage, sendReply, sendFeedback } = useAgentSend();

// Send a text message
sendMessage("Hello, how can you help me?");

// Reply to a specific message (by its timestamp)
sendReply("Thanks, that's helpful!", "2025-01-15T10:30:00.000Z");

// Submit feedback survey
sendFeedback({ rating: 5, comments: "Great experience!" });

File Uploads

File uploads work through the same sendMessage function. Pass a file object instead of a string:

const { sendMessage } = useAgentSend();

function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
  const file = e.target.files?.[0];
  if (!file) return;

  sendMessage({
    file: file,
    type: file.type,
    name: file.name,
    lastModified: file.lastModified,
    size: file.size,
  });
}

// In your JSX:
<input type="file" onChange={handleFileChange} />

The file is read as base64 and sent over the WebSocket automatically.

Connection Management

const { isConnected, reconnect } = useAgentConnection();

// Check connection status
if (!isConnected) {
  // Messages sent while disconnected are queued
  // and delivered automatically on reconnect
  reconnect();
}

Conversation Management

const {
  conversationId,
  startNewConversation,
  deleteConversation,
  downloadChatHistory,
  getChatUrl,
} = useAgentConversation();

// Start fresh
startNewConversation();

// Delete current conversation
deleteConversation();

// Trigger chat history download
downloadChatHistory();

// Get a shareable URL for the current conversation
const url = getChatUrl();

Audio / Voice

const audio = useAgentAudio();

if (audio) {
  // Voice input is available
  const { isVoiceActive, activateVoice, deactivateVoice } = audio;

  return (
    <button onClick={isVoiceActive ? deactivateVoice : activateVoice}>
      {isVoiceActive ? "Disable Voice" : "Enable Voice"}
    </button>
  );
}

Agent Configuration

The remote agent configuration (theme, welcome message, etc.) is fetched automatically by AgentProvider. Access loading state and the config via useAgentConfig():

const { agentConfig, configLoading, configError } = useAgentConfig();

if (configLoading) return <Spinner />;
if (configError) return <p>Failed to load: {configError}</p>;

// agentConfig contains the remote configuration:
// welcome_message, icon, default_language, on_prem, etc.
console.log(agentConfig?.welcome_message);

Notifications

By default, internal notifications (e.g. microphone permission errors) are logged to the console. You can provide your own handler:

import { AgentProvider } from "@smartdatahq/embedded-agent/headless";
import type { NotifyFn } from "@smartdatahq/embedded-agent/headless";

const myNotify: NotifyFn = (message, type, options) => {
  // type is "success" | "info" | "warning" | "error"
  toast[type](message); // e.g. using react-hot-toast, sonner, etc.
};

<AgentProvider config={{ identifier: "YOUR_IDENTIFIER", onNotify: myNotify }}>
  <ChatUI />
</AgentProvider>

Full Headless Example

A complete custom chat UI using only headless hooks:

import { AgentProvider, useAgent } from "@smartdatahq/embedded-agent/headless";

function CustomChatUI() {
  const {
    messages,
    sendMessage,
    isBotThinking,
    isStreaming,
    botActivity,
    isConnected,
    reconnect,
    conversationId,
    startNewConversation,
    deleteConversation,
    downloadChatHistory,
    getChatUrl,
    audio,
    configLoading,
    configError,
  } = useAgent();

  const inputRef = useRef<HTMLInputElement>(null);

  if (configLoading) return <p>Loading...</p>;
  if (configError) return <p>Error: {configError}</p>;

  const handleSend = () => {
    const val = inputRef.current?.value?.trim();
    if (!val) return;
    sendMessage(val);
    inputRef.current!.value = "";
  };

  return (
    <div>
      {/* Connection status */}
      <div>
        {isConnected ? "Connected" : "Disconnected"}
        {!isConnected && <button onClick={() => reconnect()}>Reconnect</button>}
      </div>

      {/* Messages */}
      <div style={{ height: 400, overflowY: "auto" }}>
        {messages.map((msg, i) => (
          <div key={i} style={{ textAlign: msg.user === "user" ? "right" : "left" }}>
            <span>{msg.message}</span>
            <small>
              {msg.user === "user" ? "You" : "Bot"} - {new Date(msg.time).toLocaleTimeString()}
            </small>
          </div>
        ))}
        {isBotThinking && <p>Thinking...</p>}
        {isStreaming && botActivity && <p>{botActivity}</p>}
      </div>

      {/* Input */}
      <input
        ref={inputRef}
        onKeyDown={(e) => e.key === "Enter" && handleSend()}
        disabled={!isConnected}
        placeholder="Type a message..."
      />
      <button onClick={handleSend} disabled={!isConnected}>Send</button>

      {/* Actions */}
      <div>
        <button onClick={startNewConversation}>New Chat</button>
        <button onClick={deleteConversation}>Delete</button>
        <button onClick={downloadChatHistory}>Download</button>
        <button onClick={() => navigator.clipboard.writeText(getChatUrl())}>Copy URL</button>
        {audio && (
          <button onClick={audio.isVoiceActive ? audio.deactivateVoice : audio.activateVoice}>
            {audio.isVoiceActive ? "Disable Voice" : "Enable Voice"}
          </button>
        )}
      </div>
    </div>
  );
}

export default function App() {
  return (
    <AgentProvider config={{ identifier: "YOUR_IDENTIFIER" }}>
      <CustomChatUI />
    </AgentProvider>
  );
}

Browser Tools Integration

The agent supports browser tools integration, allowing the AI agent to interact with your application's functionality directly. This powerful feature enables the agent to perform actions like navigation, data manipulation, API calls, and more.

Browser tools work with both the Embedded Agent and the Headless Agent.

Overview

Browser tools integration consists of three main components:

  1. browserToolsRegistration: Define available tools with their schemas
  2. onBrowserToolCall: Handle when the agent wants to use a tool
  3. browserToolCallResponse (Embedded) / respondToBrowserToolCall (Headless): Send the tool execution result back to the agent

Basic Browser Tools Example

const App = () => {
  const [minimized, setMinimized] = useState(true);
  const [toolResponse, setToolResponse] = useState(undefined);
  const [shoppingCart, setShoppingCart] = useState([]);

  const handleBrowserToolCall = (data) => {
    console.log("Tool called:", data);

    if (data.name === "add_to_cart") {
      // Add item to shopping cart
      const newItem = {
        id: Math.random().toString(36).substr(2, 9),
        product_sku: data.args.product_sku,
        quantity: data.args.quantity || 1,
        addedAt: new Date().toISOString()
      };

      setShoppingCart(prev => [...prev, newItem]);

      setToolResponse({
        id: data.id,
        output: {
          success: true,
          message: `Added ${data.args.product_sku} to cart`,
          cartCount: shoppingCart.length + 1
        }
      });
    }
  };

  return (
    <EmbeddedAgent
      identifier="YOUR_IDENTIFIER"
      minimized={minimized}
      onMinimizedChange={() => setMinimized(!minimized)}
      browserToolsRegistration={[
        {
          title: "add_to_cart",
          scope: "agent",
          description: "Adds a product to the shopping cart. Call this tool whenever the user has confirmed that he wants to buy the product with the product SKU identifier. Ask the user how many items he wants, if it is not clear from the context of the conversation.",
          type: "object",
          properties: {
            product_sku: {
              title: "Product SKU",
              description: "The product identifier to add to cart",
              type: "string"
            },
            quantity: {
              title: "Quantity",
              description: "Number of items to add",
              type: "integer",
              minimum: 1,
              default: 1
            }
          },
          required: ["product_sku"]
        }
      ]}
      onBrowserToolCall={handleBrowserToolCall}
      browserToolCallResponse={toolResponse}
    />
  );
};

Advanced Browser Tools Example

Here's a more comprehensive example showing multiple tool types:

const App = () => {
  const [minimized, setMinimized] = useState(true);
  const [toolResponse, setToolResponse] = useState(undefined);
  const [shoppingCart, setShoppingCart] = useState([]);

  const handleBrowserToolCall = (data) => {
    const { name, args, id } = data;

    switch (name) {
      case "show_product":
        // Navigate to product page
        window.history.pushState({}, '', `/product/${args.product_id}`);
        setToolResponse({
          id,
          output: {
            success: true,
            action: "show_product",
            message: `Showing product: ${args.product_id}`,
            response: { product_id: args.product_id }
          }
        });
        break;

      case "list_products":
        // Display product list
        setToolResponse({
          id,
          output: {
            success: true,
            action: "list_products",
            message: "Listing products...",
            response: {
              product_ids: args.product_ids,
              description: args.description,
              reasoning: args.reasoning
            }
          }
        });
        break;

      case "compare_products":
        // Navigate to comparison page
        const idsParam = args.product_ids.join(',');
        window.history.pushState({}, '', `/compare?ids=${idsParam}`);
        setToolResponse({
          id,
          output: {
            success: true,
            action: "compare_products",
            message: `Comparing products: ${args.product_ids.join(', ')}`,
            response: { product_ids: args.product_ids }
          }
        });
        break;

      case "search_products":
        // Navigate to search results
        window.history.pushState({}, '', `/search?q=${encodeURIComponent(args.query)}`);
        setToolResponse({
          id,
          output: {
            success: true,
            action: "search_products",
            message: `Showing search results for: ${args.query}`,
            response: {
              navigateTo: `/search?q=${encodeURIComponent(args.query)}`,
              query: args.query
            }
          }
        });
        break;

      case "add_to_cart":
        // Add products to cart
        const newItems = args.products.map(item => ({
          id: item.product_id,
          quantity: item.quantity,
          addedAt: new Date().toISOString()
        }));
        setShoppingCart(prev => [...prev, ...newItems.filter(item => item.quantity > 0)]);
        setToolResponse({
          id,
          output: {
            success: true,
            action: "add_to_cart",
            message: "Added products to cart",
            response: { product_ids: args.products.map(p => p.product_id) }
          }
        });
        break;

      case "clear_cart":
        setShoppingCart([]);
        setToolResponse({
          id,
          output: {
            success: true,
            action: "clear_cart",
            message: "Cart has been cleared",
            response: {}
          }
        });
        break;

      default:
        setToolResponse({
          id,
          output: {
            success: false,
            error: `Unknown tool: ${name}`
          }
        });
    }
  };

  return (
    <EmbeddedAgent
      identifier="YOUR_IDENTIFIER"
      minimized={minimized}
      onMinimizedChange={() => setMinimized(!minimized)}
      browserToolsRegistration={[
        {
          scope: "agent",
          title: "show_product",
          description: "Display a single product page for the user to view. Use when the user wants to see details of a specific product.",
          type: "object",
          properties: {
            product_id: {
              title: "Product ID",
              description: "The unique identifier of the product to display",
              type: "string"
            }
          },
          required: ["product_id"]
        },
        {
          scope: "agent",
          title: "list_products",
          description: "Display multiple products as a list for the user to browse. Use when showing product recommendations or search results.",
          type: "object",
          properties: {
            product_ids: {
              title: "Product IDs",
              description: "Array of product identifiers to display",
              type: "array",
              items: { type: "string" },
              minItems: 2
            },
            description: {
              title: "List Description",
              description: "A short title describing the product list (e.g., 'Recommended TVs')",
              type: "string"
            },
            reasoning: {
              title: "Selection Reasoning",
              description: "Explanation of why these products were selected for the user",
              type: "string"
            }
          },
          required: ["product_ids", "description", "reasoning"]
        },
        {
          scope: "agent",
          title: "compare_products",
          description: "Display a side-by-side comparison of 2-20 products. Use when the user explicitly requests a product comparison.",
          type: "object",
          properties: {
            product_ids: {
              title: "Product IDs",
              description: "Array of product identifiers to compare",
              type: "array",
              items: { type: "string" },
              minItems: 2,
              maxItems: 20
            }
          },
          required: ["product_ids"]
        },
        {
          scope: "agent",
          title: "search_products",
          description: "Navigate to search results page for the given query.",
          type: "object",
          properties: {
            query: {
              title: "Search Query",
              description: "The search term to look for",
              type: "string"
            }
          },
          required: ["query"]
        },
        {
          scope: "agent",
          title: "add_to_cart",
          description: "Add products to the shopping cart. Use when the user wants to purchase or add items to their cart.",
          type: "object",
          properties: {
            products: {
              title: "Products",
              description: "Array of products to add to cart",
              type: "array",
              items: {
                type: "object",
                properties: {
                  product_id: {
                    title: "Product ID",
                    description: "The unique identifier of the product",
                    type: "string"
                  },
                  quantity: {
                    title: "Quantity",
                    description: "Number of items to add (use 0 to remove)",
                    type: "number",
                    minimum: 0,
                    default: 1
                  }
                },
                required: ["product_id", "quantity"]
              }
            }
          },
          required: ["products"]
        },
        {
          scope: "agent",
          title: "clear_cart",
          description: "Remove all items from the shopping cart. Only use with explicit user permission.",
          type: "object",
          properties: {},
          required: []
        }
      ]}
      onBrowserToolCall={handleBrowserToolCall}
      browserToolCallResponse={toolResponse}
    />
  );
};

Browser Tools with Headless Agent

With the headless agent, browser tools are configured via AgentProvider props and handled using useAgentSend():

import { AgentProvider, useAgent, useAgentSend } from "@smartdatahq/embedded-agent/headless";
import type { BrowserToolCallData, AskQuestionArgs, AskQuestionAnswer } from "@smartdatahq/embedded-agent/headless";

function ToolAwareChat() {
  const { messages, sendMessage, isBotThinking, isConnected } = useAgent();
  const { respondToBrowserToolCall } = useAgentSend();

  // The onBrowserToolCall callback is set on AgentProvider config,
  // so handle it there. Use respondToBrowserToolCall to send results back.

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{msg.message}</div>
      ))}
      {/* ... your UI */}
    </div>
  );
}

function App() {
  const [shoppingCart, setShoppingCart] = useState([]);

  return (
    <AgentProvider
      config={{
        identifier: "YOUR_IDENTIFIER",
        browserTools: [
          {
            scope: "agent",
            title: "add_to_cart",
            description: "Add a product to the shopping cart",
            type: "object",
            properties: {
              product_sku: { type: "string", description: "Product SKU" },
              quantity: { type: "integer", minimum: 1, default: 1 },
            },
            required: ["product_sku"],
          },
        ],
        onBrowserToolCall: (data) => {
          if (data.name === "add_to_cart") {
            setShoppingCart((prev) => [
              ...prev,
              { sku: data.args.product_sku, qty: data.args.quantity || 1 },
            ]);
            // Respond to the agent from inside a component using respondToBrowserToolCall,
            // or handle it here with any state management approach.
          }
        },
      }}
    >
      <ToolAwareChat />
    </AgentProvider>
  );
}

Handling Internal Tool Calls (ask_question)

The agent may invoke an internal ask_question tool to present the user with questions and options. If you are using the Embedded Agent (pre-built UI), this is handled automatically — no extra work needed. This section only applies if you are using the Headless Agent and ask_question has been activated for your agent. In headless mode, you handle it via the onBrowserToolCall callback and render your own UI.

You receive all questions upfront, present them however you like, and send one respondToBrowserToolCall() when all answers are collected. The headless engine automatically adds the questions and answers to chat history — you don't need to manage messages yourself.

Tool call data shape:

{
  name: "ask_question",
  type: "tool_call",
  id: "call_abc123",          // Use this as the response ID
  args: {
    questions: [
      {
        title: "How would you like to be contacted?",
        options: [
          { id: "email", label: "Email" },
          { id: "phone", label: "Phone" },
          { id: "sms", label: "SMS" },
        ],
      },
      {
        title: "What time works best for you?",
        options: [
          { id: "morning", label: "Morning" },
          { id: "afternoon", label: "Afternoon" },
          { id: "evening", label: "Evening" },
        ],
      },
    ],
  },
}

Example:

import { AgentProvider, useAgent, useAgentSend } from "@smartdatahq/embedded-agent/headless";
import type { BrowserToolCallData, AskQuestionArgs, AskQuestionAnswer } from "@smartdatahq/embedded-agent/headless";

function App() {
  const [pendingQuestions, setPendingQuestions] = useState(null);

  return (
    <AgentProvider
      config={{
        identifier: "YOUR_IDENTIFIER",
        onBrowserToolCall: (data: BrowserToolCallData) => {
          if (data.name === "ask_question") {
            // Store questions to render in your UI
            setPendingQuestions(data);
          }
        },
      }}
    >
      <MyChatUI
        pendingQuestions={pendingQuestions}
        onQuestionsAnswered={() => setPendingQuestions(null)}
      />
    </AgentProvider>
  );
}

function MyChatUI({ pendingQuestions, onQuestionsAnswered }) {
  const { messages } = useAgent();
  const { respondToBrowserToolCall } = useAgentSend();

  const handleSubmitAnswers = (selectedOptions) => {
    const args = pendingQuestions.args as AskQuestionArgs;
    const questions = args.questions ?? [];

    const answers: AskQuestionAnswer[] = questions.map((q, i) => ({
      questionId: q.id ?? `question-${i + 1}`,
      question: q.title,
      answerId: selectedOptions[i].id,
      answerLabel: selectedOptions[i].label,
    }));

    // Send the response — chat history is updated automatically
    respondToBrowserToolCall({
      id: pendingQuestions.id,
      output: { answers },
    });
    onQuestionsAnswered();
  };

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{msg.message}</div>
      ))}
      {pendingQuestions && (
        <MyQuestionForm
          questions={pendingQuestions.args.questions}
          onComplete={handleSubmitAnswers}
        />
      )}
    </div>
  );
}

Example MyQuestionForm component:

The component below steps through each question one at a time, collects the selected option, and calls onComplete with all answers once the last question is answered:

import { useState } from "react";
import type { AskQuestionQuestion } from "@smartdatahq/embedded-agent/headless";

function MyQuestionForm({
  questions,
  onComplete,
}: {
  questions: AskQuestionQuestion[];
  onComplete: (selectedOptions: { id: string; label: string }[]) => void;
}) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [selectedOptions, setSelectedOptions] = useState<{ id: string; label: string }[]>([]);

  const handleSelect = (option: { id: string; label: string }) => {
    const updated = [...selectedOptions, option];

    if (currentIndex + 1 < questions.length) {
      // More questions to go — advance to the next one
      setSelectedOptions(updated);
      setCurrentIndex((i) => i + 1);
    } else {
      // All questions answered — submit
      onComplete(updated);
    }
  };

  const current = questions[currentIndex];

  return (
    <div style={{ padding: 12, background: "#f5f5f5", borderRadius: 8 }}>
      <p style={{ fontWeight: 600, marginBottom: 8 }}>
        {current.title}
        {questions.length > 1 && (
          <span style={{ fontWeight: 400, fontSize: 12, color: "#666", marginLeft: 6 }}>
            ({currentIndex + 1}/{questions.length})
          </span>
        )}
      </p>
      <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
        {current.options.map((opt) => (
          <button
            key={opt.id}
            onClick={() => handleSelect(opt)}
            style={{
              padding: "6px 12px",
              borderRadius: 6,
              border: "1px solid #007bff",
              background: "white",
              color: "#007bff",
              cursor: "pointer",
              textAlign: "left",
            }}
          >
            {opt.label}
          </button>
        ))}
      </div>
    </div>
  );
}

Note: If you are using the Embedded Agent (pre-built UI), ask_question is rendered automatically — you do not need to implement any of the above.

Tool Schema Format

Browser tools use JSON Schema format for defining parameters. Each tool should include:

  • title: Unique identifier for the tool
  • scope: Either "agent" or "conversation". This registers the tool for either the current conversation or globally for the agent.
  • description: Clear description of what the tool does and when to call it. Describe under what conditions the AI should use the tool and explain the goal of calling the tool. For example, if it's for adding a product to the cart, then explain that it should be called once the user has confirmed that he wishes to buy a product.
  • type: Should be "object" for complex tools
  • properties: Object defining all parameters
  • required: Array of required parameter names

Parameter Types

You can use various JSON Schema types and constraints:

// String with enum constraints
{
  type: "string",
  enum: ["option1", "option2", "option3"],
  description: "Choose from predefined options"
}

// Number with range constraints
{
  type: "number",
  minimum: 0,
  maximum: 100,
  description: "Value between 0 and 100"
}

// Complex nested objects
{
  type: "object",
  properties: {
    nested_field: {
      type: "string",
      description: "Nested parameter"
    }
  },
  required: ["nested_field"]
}

// Arrays with item constraints
{
  type: "array",
  items: {
    type: "string",
    enum: ["item1", "item2"]
  },
  description: "Array of predefined items"
}

Error Handling

Always include error handling in your tool call handler:

const handleBrowserToolCall = (data) => {
  try {
    // Your tool logic here
    setToolResponse({
      id: data.id,
      output: { success: true, result: "..." },
    });
  } catch (error) {
    setToolResponse({
      id: data.id,
      output: {
        success: false,
        error: error.message,
      },
    });
  }
};

Best Practices

  1. Clear Descriptions: Provide clear, detailed descriptions for tools and parameters
  2. Validation: Use JSON Schema constraints to validate inputs
  3. Error Handling: Always handle errors gracefully
  4. Response Format: Maintain consistent response format with success/error indicators
  5. Async Operations: Handle asynchronous operations properly
  6. State Management: Update your application state based on tool results

Checkout Integration (Headless)

When the agent decides the user is ready to pay, it emits a checkout_initiated signal carrying an Adyen session payload. The Embedded Agent renders the payment UI for you — no extra wiring needed. With the Headless Agent you own the UI, so you receive the signal via the onCheckoutInitiated callback and ship the outcome back via respondToCheckout from useAgentSend().

Checkout Flow

  1. Signal in: onCheckoutInitiated(config) fires on AgentProvider. The config contains the Adyen clientKey, sessionId, sessionData, and environment.
  2. Render: Mount your payment UI (Adyen Drop-in or any compatible flow) using that config.
  3. Signal out: When the payment finishes, call respondToCheckout({ status: "completed" | "failed", resultCode }) so the agent can continue the conversation accordingly.
import type { CheckoutConfig, CheckoutResult } from "@smartdatahq/embedded-agent/headless";

// CheckoutConfig — what you receive
{
  clientKey: string;
  sessionId: string;
  sessionData: string;
  environment: "test" | "live" | "live-us" | "live-au" | "live-apse" | "live-in" | (string & {});
}

// CheckoutResult — what you send back
{ status: "completed"; resultCode: string } | { status: "failed"; resultCode: string }

Adyen Drop-in Example

import { useEffect, useRef, useState } from "react";
import {
  AgentProvider,
  useAgent,
  useAgentSend,
} from "@smartdatahq/embedded-agent/headless";
import type { CheckoutConfig } from "@smartdatahq/embedded-agent/headless";
import { AdyenCheckout, Dropin, Card } from "@adyen/adyen-web";
import "@adyen/adyen-web/dist/es/adyen.css";

function CheckoutSurface({ config }: { config: CheckoutConfig }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const { respondToCheckout } = useAgentSend();

  useEffect(() => {
    if (!containerRef.current) return;
    let dropin: InstanceType<typeof Dropin> | null = null;

    (async () => {
      const checkout = await AdyenCheckout({
        environment: config.environment,
        clientKey: config.clientKey,
        session: { id: config.sessionId, sessionData: config.sessionData },
        onPaymentCompleted: (result) => {
          respondToCheckout({ status: "completed", resultCode: result.resultCode });
        },
        onPaymentFailed: (result) => {
          respondToCheckout({ status: "failed", resultCode: result?.resultCode ?? "ERROR" });
        },
      });
      dropin = new Dropin(checkout, { paymentMethodComponents: [Card] }).mount(
        containerRef.current!,
      );
    })();

    return () => {
      dropin?.unmount();
    };
  }, [config, respondToCheckout]);

  return <div ref={containerRef} />;
}

function ChatWithCheckout({
  checkoutConfig,
  onClose,
}: {
  checkoutConfig: CheckoutConfig | null;
  onClose: () => void;
}) {
  const { messages } = useAgent();

  return (
    <>
      {messages.map((msg, i) => (
        <div key={i}>{msg.message}</div>
      ))}
      {checkoutConfig && (
        <Modal onClose={onClose}>
          <CheckoutSurface config={checkoutConfig} />
        </Modal>
      )}
    </>
  );
}

function App() {
  const [checkoutConfig, setCheckoutConfig] = useState<CheckoutConfig | null>(null);

  return (
    <AgentProvider
      config={{
        identifier: "YOUR_IDENTIFIER",
        onCheckoutInitiated: (config) => setCheckoutConfig(config),
      }}
    >
      <ChatWithCheckout
        checkoutConfig={checkoutConfig}
        onClose={() => setCheckoutConfig(null)}
      />
    </AgentProvider>
  );
}

Note: respondToCheckout must be called from inside an <AgentProvider> subtree (it's exposed via useAgentSend()). If your payment surface lives outside the provider, lift the onCheckoutInitiated payload into shared state and render the payment component as a child of AgentProvider.


API Reference

EmbeddedAgent Props

identifier (Required)
type: string
Description: Unique identifier for connecting to the embedded agent server.
Browser Tools Properties

browserToolsRegistration

Type: Array<{ scope: "agent" | "conversation"; [key: string]: any; }>
Description: Array of tool definitions using JSON Schema format. Each tool defines parameters and constraints for browser-based actions the agent can perform.

onBrowserToolCall

Type: (data: { name: string; type: string; id: string; args?: Record<string, any>; [key: string]: any; }) => void
Description: Callback function triggered when the agent wants to execute a browser tool. Handle the tool execution and update browserToolCallResponse with the result.

browserToolCallResponse

Type: { id: string; output: any; } | undefined
Description: Response object containing the result of a browser tool execution. The id should match the tool call request id, and output contains the execution result.
Additional Props

className: Custom class for styling the embedded agent container.
minimized: Default minimized state of the embedded agent. If being used, you need to update it to match the value you get from the onMinimizedChange function.
onMinimizedChange: Callback for changes in the minimized state.
onUltraMinimize: Optional callback function triggered when the ultra minimize action is performed (minus icon clicked). This allows you to implement custom behavior for collapsing the agent to a minimal one-line state, such as completely hiding the widget or storing the state. Example: onUltraMinimize={() => { console.log("Ultra minimize triggered!"); setIsRendered(false); }}.
conversationId: Unique conversation ID for the session. This is generated internally if not provided. It should preferably be a UUID string.
isRendered: boolean. You can set this to false if you don't want the agent to show on render. Instead, we will display a message that can be clicked to load the agent. This is false by default. If set to true, the agent is displayed instead of the message prompt.
onChatbotRender: This function is called when the agent is rendered. It only runs if you set render to false and manually click the message that displays in place of the agent.
onNewConversation: Callback invoked when a new conversation is started, either from the menu or after an inactivity timeout. () => void.
onBeforeAgentClosed: Callback invoked right before the embedded agent closes. Return false to prevent closing (useful when navigating away immediately). () => boolean | void.
onAgentClosed: Callback invoked after the embedded agent is closed. () => void.
onError: Callback for handling errors.
onResponseStreamed: Callback invoked when the assistant finishes streaming a response. () => void.
onResponseStreamStart: Callback invoked when the assistant starts streaming a response. () => void.
language: The language of the agent. This can be used to change the language of the agent. It must be a valid ISO 639-1 code with a country code. For example, "en-US", "en-GB", "fr-FR", "fr-CA", "es-ES", "de-DE", "it-IT", "ja-JP", "ko-KR", "pt-BR". This must be a valid language available in our agent conversation interface. If not provided, the language will be set to the configured language of the agent and can be changed by the user in the conversation interface.
inactiveComponent: The component to display when the agent is inactive (not rendered).
inactiveText: The text to display when the agent is inactive (not rendered). This is useful for overwriting the configured message shown to users before they interact with the agent.
showInPopup: This boolean determines whether the agent is shown as a popup on the page or embedded directly in the content. When true, the agent will appear in a floating popup window that can be moved around the page.
processingIcon: Optional JSX element to customize the loading indicator shown while the agent is processing responses.
offset: Optional object to determine the left, right, top and bottom offset of the agent when it's a popup.
mobileOffset: Optional object to determine the left, right, top and bottom offset of the agent when it's a popup on mobile or tablet devices (screens with max-width: 1024px). This allows for different positioning on mobile devices compared to desktop. When not provided, falls back to the offset prop values.
resizeable: Optional boolean that enables resizing functionality for the chat window. When true, the height of the window can be resized using the line at the mid-top of the header. When false, the window maintains a fixed size. Note that resizing is only available when the window is not minimized.
height: Optional number that sets the initial height of the chat window in pixels. If not provided, defaults to 600.
mobileHeight: Optional number that sets the initial height of the chat window in pixels on mobile devices. If not provided, defaults to 300.
width: Optional number that sets the initial width of the chat window in pixels. If not provided, defaults to 500.
devMode: Optional boolean that allows us to enable devMode features.

AgentProvider Props (AgentConfig)

These props are passed via the config prop on <AgentProvider>:

| Prop | Type | Required | Description | |------|------|----------|-------------| | identifier | string | Yes | The identifier of the agent. Required to connect to the server. | | conversationId | string | No | Optional conversation ID. Auto-generated UUID if omitted. | | language | string | No | Language code (ISO 639-1 with country, e.g. "en-US"). | | browserTools | Array<{ scope: "agent" \| "conversation"; ... }> | No | Browser tools to register with the agent. | | onError | (error: string) => void | No | Called when an error occurs during agent processing. | | onBrowserToolCall | (data: BrowserToolCallData) => void | No | Called when a browser tool call is made by the agent. Also receives internal browser tools like ask_question in headless mode — see Handling Internal Tool Calls. | | onToolCall | (data: { toolName: string; conversationId: string }) => void | No | Called when an internal tool is invoked. Useful for analytics. | | onUserMessage | (data: { conversationId: string }) => void | No | Called when a user message is sent. Useful for analytics. | | onCheckoutInitiated | (config: CheckoutConfig) => void | No | Called when the agent initiates a checkout flow. Render your own payment UI and reply via respondToCheckout — see Checkout Integration. | | onResponseStreamStart | () => void | No | Called when the assistant starts streaming a response. | | onResponseStreamed | () => void | No | Called when the assistant finishes streaming a response. | | onNewConversation | () => void | No | Called when a new conversation is started. | | onNotify | NotifyFn | No | Custom notification handler. Falls back to console logging. | | shouldConnect | boolean | No | Controls whether the agent opens its WebSocket connection. Defaults to true. Set to false to defer connecting (e.g. while gating on auth, feature flags, or a user gesture); flip back to true to connect. Messages sent while disconnected are queued and flushed on connect. |

Headless Hooks

| Hook | Returns | Description | |------|---------|-------------| | useAgent() | All fields below combined | Convenience hook composing all primitives. | | useAgentConnection() | { isConnected, reconnect } | WebSocket connection state and reconnect action. | | useAgentMessages() | { messages, isBotThinking, isStreaming, botActivity } | Read-only message and streaming state. | | useAgentSend() | { sendMessage, sendReply, sendFeedback, respondToBrowserToolCall, respondToCheckout } | Actions for sending messages, replies, feedback, tool responses, and checkout outcomes. | | useAgentConversation() | { conversationId, startNewConversation, deleteConversation, downloadChatHistory, getChatUrl } | Conversation lifecycle management. | | useAgentAudio() | { isVoiceActive, activateVoice, deactivateVoice } \| null | Audio/voice input controls. null if unavailable. | | useAgentConfig() | { agentConfig, configLoading, configError } | Remote agent configuration and loading state. |

Utility Functions

getConversationHistory()
Type: () => IChatMessage[]
Description: Returns an array of all messages from the current conversation history. This function provides access to the complete conversation including messages from users, the bot, and human agents.

Example:
```typescript
import { EmbeddedAgent, getConversationHistory } from '@smartdatahq/embedded-agent';

// In your component or function
const conversationHistory = getConversationHistory();
console.log('Conversation history:', conversationHistory);

// Each message object contains:
// - message: string (the message content)
// - time: string (timestamp)
// - user: "user" | "bot" | "humanAgent" (sender type)
// - replyTo?: string (optional reply reference)
// - thumbsUp?: boolean (optional feedback)

// You can filter by message type if needed:
const userMessages = conversationHistory.filter(msg => msg.user === "user");
const botMessages = conversationHistory.filter(msg => msg.user === "bot");
const humanAgentMessages = conversationHistory.filter(msg => msg.user === "humanAgent");
```

Note: This function accesses the global conversation store, so it will return messages from the currently active conversation session.