PX-UI
Components

Xandi

AI chatbot component for workforce and hiring applications

Overview

Xandi is a composable AI chatbot component designed for workforce and hiring applications. It provides a ready-to-use chat interface with welcome messages, prompt suggestions, message actions, and chat history management.

Import

import { Xandi, XandiProvider, XHeader, XSidebar, type XandiHandlers } from "@px-ui/ai";

Usage

XXandi
X

How can I help you today?

Enter to send · Shift+Enter for new line
import { Xandi, XandiProvider, type XandiHandlers, type XandiResponse } from "@px-ui/ai";

const handlers: XandiHandlers = {
  fetchResp: async (message, options) => {
    const response = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message, conversation_id: options?.conversationId }),
      signal: options?.signal, // Enable request cancellation
    });
    const data = await response.json();
    return {
      content: data.message,
      conversationId: data.data.conversation_id,
      debugTrace: data.trace,
    };
  },
};

function MyPage() {
  return (
    <XandiProvider handlers={handlers}>
      <Xandi />
    </XandiProvider>
  );
}

Architecture

Xandi uses a provider pattern to manage chat state. The XandiProvider wraps your chat components and provides context for:

  • Message state - Manages conversation history
  • Conversation management - Handles chat sessions with optional restoration
  • API communication - Delegates to your custom handler functions

Basic Setup

import { type XandiHandlers, type XandiResponse, type FeedbackType } from "@px-ui/ai";

const handlers: XandiHandlers = {
  // Required: Fetch AI response
  fetchResp: async (message, options): Promise<XandiResponse> => {
    const response = await fetch("https://api.example.com/chat", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer your-token",
      },
      body: JSON.stringify({ message, conversation_id: options?.conversationId }),
      signal: options?.signal, // Enable request cancellation
    });
    const data = await response.json();
    return {
      content: data.message,
      conversationId: data.data.conversation_id,
      debugTrace: data.trace,
    };
  },

  // Optional: Handle feedback
  onFeedback: (messageId: string, conversationId: string, feedback: FeedbackType) => {
    console.log("Feedback:", { messageId, conversationId, feedback });
  },

  // Optional: Handle stop request (for server-side cleanup)
  onStop: (conversationId: string) => {
    console.log("Stopped:", conversationId);
  },
};

<XandiProvider
  handlers={handlers}
  conversationId="existing-conversation-id" // optional, for restoring sessions
>
  <Xandi
    welcomeMessage="How can I help you today?"
    suggestions={[
      { id: "1", label: "Open Jobs", prompt: "Show me all open jobs" },
      { id: "2", label: "Pending Timesheets", prompt: "Show me pending timesheets" },
    ]}
  />
</XandiProvider>

With Header and Sidebar

import { useState } from "react";
import { Xandi, XandiProvider, XHeader, XSidebar, type XandiConfig, type XandiHandlers } from "@px-ui/ai";

// Custom configuration
const config: XandiConfig = {
  avatarUrl: "https://example.com/my-assistant-avatar.png",
  assistantName: "Assistant",
};

const handlers: XandiHandlers = {
  fetchResp: async (message, options) => {
    const response = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message, conversation_id: options?.conversationId }),
      signal: options?.signal,
    });
    const data = await response.json();
    return {
      content: data.message,
      conversationId: data.data.conversation_id,
    };
  },
  onFeedback: (messageId, conversationId, feedback) => {
    // Send feedback to API
  },
  onStop: (conversationId) => {
    // Server-side cleanup if needed
  },
};

function ChatWindow() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div className="flex h-full">
      <XandiProvider handlers={handlers} config={config}>
        <XSidebar
          isOpen={sidebarOpen}
          onClose={() => setSidebarOpen(false)}
        />
        <div className="flex flex-1 flex-col">
          <XHeader
            onToggleHistory={() => setSidebarOpen((prev) => !prev)}
          />
          <Xandi />
        </div>
      </XandiProvider>
    </div>
  );
}

API Reference

XandiProvider

The context provider that manages chat state. Requires a handlers object with API communication functions.

PropTypeRequiredDescription
handlersXandiHandlersYesObject containing all handler functions
conversationIdstringNoExisting conversation ID to restore a chat
configXandiConfigNoConfiguration for assistant appearance

Xandi

The main chat component. Must be wrapped in XandiProvider.

PropTypeDefaultDescription
welcomeMessagestring"How can I help you today?"Message shown when chat is empty
suggestionsSuggestion[][]Prompt suggestions shown below the input
uiModeXandiUIMode"full"UI mode: full = no close button (default); sidebar / floating = show close button in header. Overrides provider config when set.

XHeader

Header component for floating windows and sidebars. Must be inside XandiProvider. Avatar and title come from context config; New Chat uses startNewConversation from context. Close button is hidden when config.uiMode is "full".

PropTypeDefaultDescription
onClose() => void-Callback when close button is clicked
onToggleHistory() => void-Callback when menu button is clicked

XSidebar

Sidebar component for chat history. Must be inside XandiProvider. Fetches conversation history via getConvHistory from context when opened and manages its own loading state.

PropTypeDefaultDescription
isOpenbooleantrueWhether the sidebar is visible
onClose() => void-Callback when close button is clicked

useXandi Hook

Access the chat context from any component within XandiProvider.

import { useXandi } from "@px-ui/ai";

function CustomComponent() {
  const {
    conversation,
    isLoading,
    sendMessage,
    stopRequest,
    loadConversation,
    startNewConversation,
    submitFeedback,
    config,
  } = useXandi();

  // Access messages from conversation
  const messages = conversation.messages;
  const conversationId = conversation.id;
  // ...
}
PropertyTypeDescription
conversationConversationCurrent conversation object with messages, id, title, and timestamps
isLoadingbooleanWhether a response is being fetched
sendMessage(text: string) => voidFunction to send a message
stopRequest() => voidStop the current request
loadConversation(conversationId: string, options?: GetConvOptions) => Promise<void>Load an existing conversation (page/perPage default 1/20)
startNewConversation() => voidReset to a new empty conversation
submitFeedback(messageId: string, feedback: FeedbackType) => voidSubmit feedback for a message
getConvHistory() => Promise<ConversationSummary[]>Handler to fetch conversation list (from provider)
configXandiConfigConfiguration object with avatar URL and assistant name

Types

// Options for fetchResp handler
interface FetchRespOptions {
  conversationId?: string;
  signal?: AbortSignal; // For canceling requests
}

// Options for getConv handler (defaults: page 1, perPage 20)
interface GetConvOptions {
  page?: number;
  perPage?: number;
}

// Handler functions for API communication
interface XandiHandlers {
  fetchResp: (message: string, options?: FetchRespOptions) => Promise<XandiResponse>;
  getConv?: (conversationId: string, options?: GetConvOptions) => Promise<Conversation>;
  getConvHistory?: () => Promise<ConversationSummary[]>;
  onFeedback?: (messageId: string, conversationId: string, feedback: FeedbackType) => void;
  onStop?: (conversationId: string) => void;
}

// Configuration for assistant appearance
interface XandiConfig {
  avatarUrl?: string;     // URL for the assistant's avatar
  assistantName?: string; // Name of the assistant (default: "Xandi")
}

// Response from fetchResp handler
interface XandiResponse {
  content: string;
  type?: "text" | "markdown";
  debugTrace?: unknown;
  conversationId?: string;
}

// API response structure from backend
interface XandiApiResponse {
  success: boolean;
  message: string;
  data: {
    intent: string;
    data: unknown;
    conversation_id: string;
  };
  trace?: {
    trace_id: string;
    execution_mode: string;
    intent: string;
    tool_id: string;
    debug_trace: unknown;
  };
}

// Full conversation with messages (id is null for new unsaved conversations)
interface Conversation {
  id: string | null;
  title: string;
  messages: Message[];
  createdAt: Date;
  updatedAt: Date;
}

// Conversation summary for history list
interface ConversationSummary {
  id: string;
  title: string;
  timestamp: Date;
}

interface Suggestion {
  id: string;
  label: string;
  prompt: string;
}

interface Message {
  id: string;
  role: "user" | "assistant";
  content: string;
  type?: "text" | "markdown";
  debugTrace?: unknown;
}

type FeedbackType = "up" | "down" | null;

interface ChatHistoryItem {
  id: string;
  title: string;
  timestamp: Date;
}

Message Actions

Assistant messages include action buttons for feedback, copying, and debugging.

  • Thumbs Up/Down - Provide feedback on message quality
  • Copy - Copy message content to clipboard
  • Debug - View debug trace (when debugTrace is present in the message)

Installation

pnpm add @px-ui/ai

Don't forget to import the styles:

import "@px-ui/ai/styles.css";