Complete API reference for building custom components that integrate with Pika chat applications.
Overview
Section titled “Overview”Pika supports two types of custom components:
- Custom Compiled-In Components - Svelte components built into your app
- Web Components - Standalone custom elements dynamically loaded
Custom Compiled-In Components
Section titled “Custom Compiled-In Components”Component Location
Section titled “Component Location”apps/pika-chat/src/lib/client/features/chat/└── message-segments/ └── custom-components/ ├── index.ts # Component registry ├── my-component.svelte # Your components └── another-component.svelteComponent Registry
Section titled “Component Registry”Register components in index.ts:
import type { Component } from 'svelte';import MyComponent from './my-component.svelte';import AnotherComponent from './another-component.svelte';
export const customRenderers: Record<string, Component<any>> = { 'my-component': MyComponent, 'another-component': AnotherComponent};Component Props
Section titled “Component Props”Components receive props from tag attributes:
<script lang="ts"> // Props are passed as attributes export let orderId: string; export let status: string = 'pending'; // With default export let priority?: number; // Optional</script>
<div class="order-card"> <h3>Order #{orderId}</h3> <p>Status: {status}</p> {#if priority} <span>Priority: {priority}</span> {/if}</div>Usage in Messages
Section titled “Usage in Messages”<!-- LLM generates this in response --><my-component orderId="12345" status="shipped" priority="1" />Advanced Props
Section titled “Advanced Props”JSON Objects
Section titled “JSON Objects”<script lang="ts"> export let data: string; // Receives JSON string
let parsed = $derived(JSON.parse(data));</script>
<div> <p>Name: {parsed.name}</p> <p>Value: {parsed.value}</p></div>Arrays
Section titled “Arrays”<script lang="ts"> export let items: string; // JSON array string
let itemList = $derived(JSON.parse(items));</script>
<ul> {#each itemList as item} <li>{item}</li> {/each}</ul>Accessing Chat Context
Section titled “Accessing Chat Context”Components can access chat app state through context:
<script lang="ts"> import { getContext } from 'svelte'; import type { IChatAppState } from 'pika-ux/types';
const chatAppState = getContext<IChatAppState>('chatAppState');
// Access user info const user = chatAppState.user; const customData = chatAppState.userCustomData;
// Access session info const sessionId = chatAppState.sessionId; const chatAppId = chatAppState.chatAppId;</script>
<div> <p>Hello, {user.firstName}!</p> <p>Account: {customData.accountId}</p></div>Widget Metadata API
Section titled “Widget Metadata API”Components can interact with the widget system:
<script lang="ts"> import { getContext } from 'svelte'; import type { IWidgetMetadataAPI } from 'pika-ux/types';
const widgetAPI = getContext<IWidgetMetadataAPI>('widgetMetadataAPI');
// Show loading state function handleAction() { widgetAPI.setLoadingStatus(true, 'Processing...');
// Do work... await someAsyncOperation();
widgetAPI.setLoadingStatus(false); }
// Get/set widget data (persisted per user) const savedData = await widgetAPI.getUserWidgetData(); await widgetAPI.setUserWidgetData({ myField: 'value' });</script>IChatAppState Interface
Section titled “IChatAppState Interface”interface IChatAppState { // User information user: ChatUser; userCustomData: Record<string, any>;
// Session information sessionId: string; chatAppId: string; agentId: string;
// Chat app configuration chatApp: ChatApp; features: ChatAppFeatures;
// UI state mode: 'standalone' | 'embedded';
// Methods sendMessage(message: string): Promise<void>; uploadFile(file: File): Promise<string>;}IWidgetMetadataAPI Interface
Section titled “IWidgetMetadataAPI Interface”interface IWidgetMetadataAPI { // Loading state setLoadingStatus(loading: boolean, loadingMsg?: string): void;
// User data persistence (max 400KB per widget) getUserWidgetData(): Promise<Record<string, unknown> | undefined>; setUserWidgetData(data: Record<string, unknown>, partial?: boolean): Promise<void>; deleteUserWidgetData(): Promise<void>;
// Widget information getWidgetId(): { scope: string; tag: string };
// Invoke agent (for interactive widgets) invokeAgent(message: string, config?: { componentAgentInstructionName: string; }): Promise<string>;}Complete Component Example
Section titled “Complete Component Example”<script lang="ts"> import { getContext } from 'svelte'; import { Button } from 'pika-ux/shadcn/button'; import * as Card from 'pika-ux/shadcn/card'; import type { IChatAppState, IWidgetMetadataAPI } from 'pika-ux/types';
// Props from tag attributes export let orderId: string; export let initialStatus: string = 'pending';
// Context const chatAppState = getContext<IChatAppState>('chatAppState'); const widgetAPI = getContext<IWidgetMetadataAPI>('widgetMetadataAPI');
// State let status = $state(initialStatus); let loading = $state(false);
// Load saved data $effect(() => { loadSavedData(); });
async function loadSavedData() { const saved = await widgetAPI.getUserWidgetData(); if (saved?.status) { status = saved.status as string; } }
async function refreshStatus() { loading = true; widgetAPI.setLoadingStatus(true, 'Refreshing...');
try { // Call agent to get updated status const response = await widgetAPI.invokeAgent( `Get the current status for order ${orderId}`, { componentAgentInstructionName: 'order-status-refresh' } );
status = extractStatus(response);
// Save for next time await widgetAPI.setUserWidgetData({ status }, true); } finally { loading = false; widgetAPI.setLoadingStatus(false); } }
function extractStatus(response: string): string { // Parse response... return 'shipped'; }</script>
<Card.Root> <Card.Header> <Card.Title>Order #{orderId}</Card.Title> <Card.Description> Customer: {chatAppState.user.firstName} {chatAppState.user.lastName} </Card.Description> </Card.Header> <Card.Content> <p class="text-lg font-semibold">Status: {status}</p> </Card.Content> <Card.Footer> <Button onclick={refreshStatus} disabled={loading} size="sm" > {loading ? 'Refreshing...' : 'Refresh Status'} </Button> </Card.Footer></Card.Root>
<style> /* Scoped styles */</style>Web Components
Section titled “Web Components”Creating Web Components
Section titled “Creating Web Components”Web components are standard custom elements:
class OrderStatusWidget extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); }
connectedCallback() { this.render(); }
disconnectedCallback() { // Cleanup }
attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { this.render(); } }
static get observedAttributes() { return ['order-id', 'status']; }
render() { const orderId = this.getAttribute('order-id'); const status = this.getAttribute('status') || 'unknown';
this.shadowRoot.innerHTML = ` <style> .order-card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; font-family: system-ui, sans-serif; } .status { font-weight: bold; color: #0066cc; } </style> <div class="order-card"> <h3>Order #${orderId}</h3> <p class="status">Status: ${status}</p> </div> `; }}
customElements.define('order-status-widget', OrderStatusWidget);Accessing Pika Context
Section titled “Accessing Pika Context”Web components can access Pika context via custom events:
class InteractiveWidget extends HTMLElement { connectedCallback() { this.render();
// Request context from Pika this.dispatchEvent(new CustomEvent('pika:request-context', { bubbles: true, detail: { callback: (context) => { this.handleContext(context); } } })); }
handleContext(context) { const { user, chatAppState, widgetAPI } = context;
// Use context... this.user = user; this.widgetAPI = widgetAPI; this.render(); }
async saveData(data) { // Use widget API if (this.widgetAPI) { await this.widgetAPI.setUserWidgetData(data); } }
render() { // Render with context }}Handling User Interaction
Section titled “Handling User Interaction”class ClickableWidget extends HTMLElement { connectedCallback() { this.render(); this.setupEventListeners(); }
setupEventListeners() { const button = this.shadowRoot.querySelector('button'); button?.addEventListener('click', () => this.handleClick()); }
handleClick() { // Dispatch event to Pika this.dispatchEvent(new CustomEvent('pika:widget-action', { bubbles: true, detail: { action: 'button-clicked', widgetId: this.getAttribute('widget-id'), data: { /* action data */ } } })); }
render() { this.shadowRoot.innerHTML = ` <button>Click Me</button> `; }}Async Data Loading
Section titled “Async Data Loading”class DataWidget extends HTMLElement { async connectedCallback() { this.renderLoading();
try { const data = await this.fetchData(); this.renderData(data); } catch (error) { this.renderError(error); } }
renderLoading() { this.shadowRoot.innerHTML = ` <div class="loading">Loading...</div> `; }
renderData(data) { this.shadowRoot.innerHTML = ` <div class="data">${JSON.stringify(data)}</div> `; }
renderError(error) { this.shadowRoot.innerHTML = ` <div class="error">Error: ${error.message}</div> `; }
async fetchData() { // Fetch from API const response = await fetch('/api/data'); return response.json(); }}Styling
Section titled “Styling”Svelte Component Styling
Section titled “Svelte Component Styling”<div class="my-component"> <!-- content --></div>
<style> /* Scoped to this component */ .my-component { padding: 16px; border: 1px solid var(--border); border-radius: 8px; }
/* Use Tailwind variables */ .my-component { @apply p-4 border rounded-lg; }</style>Web Component Styling
Section titled “Web Component Styling”this.shadowRoot.innerHTML = ` <style> :host { display: block; padding: 16px; }
.card { border: 1px solid #e2e8f0; border-radius: 8px; }
/* Respect user's theme */ @media (prefers-color-scheme: dark) { .card { border-color: #374151; } } </style> <div class="card">...</div>`;Testing Components
Section titled “Testing Components”Svelte Component Testing
Section titled “Svelte Component Testing”import { render } from '@testing-library/svelte';import { describe, it, expect } from 'vitest';import MyComponent from './my-component.svelte';
describe('MyComponent', () => { it('renders with props', () => { const { getByText } = render(MyComponent, { props: { orderId: '12345', status: 'shipped' } });
expect(getByText('Order #12345')).toBeInTheDocument(); expect(getByText('Status: shipped')).toBeInTheDocument(); });});Web Component Testing
Section titled “Web Component Testing”describe('OrderStatusWidget', () => { it('renders correctly', () => { const widget = document.createElement('order-status-widget'); widget.setAttribute('order-id', '12345'); widget.setAttribute('status', 'shipped');
document.body.appendChild(widget);
const content = widget.shadowRoot.textContent; expect(content).toContain('Order #12345'); expect(content).toContain('Status: shipped');
document.body.removeChild(widget); });});Best Practices
Section titled “Best Practices”Deployment
Section titled “Deployment”Svelte Components
Section titled “Svelte Components”No special deployment - built with your app:
cd apps/pika-chatpnpm buildWeb Components
Section titled “Web Components”- Build your component:
pnpm run build # Outputs to dist/widget.js- Compress (optional but recommended):
gzip dist/widget.js- Upload to S3:
aws s3 cp dist/widget.js.gz s3://my-bucket/widgets/ \ --content-encoding gzip \ --content-type application/javascript \ --acl public-read- Create tag definition (see Widget System Reference)
Troubleshooting
Section titled “Troubleshooting”Component Not Rendering
Section titled “Component Not Rendering”- Check component is registered in
customRenderers - Verify tag definition exists and is enabled
- Check tag name matches exactly (case-sensitive)
- Ensure chat app has tag enabled
Props Not Working
Section titled “Props Not Working”- Verify prop names match tag attributes
- Check prop types (strings vs objects)
- Remember: all attributes are strings, parse JSON if needed
Context Not Available
Section titled “Context Not Available”- Ensure you're using
getContextin component setup - Check context keys:
'chatAppState','widgetMetadataAPI' - Context only available in compiled-in components
Web Component Not Loading
Section titled “Web Component Not Loading”- Verify S3 permissions allow public read
- Check S3 key path is correct
- Ensure CORS configured on S3 bucket
- Check browser console for loading errors
Related Documentation
Section titled “Related Documentation”- Widget System Reference - Tag definition system
- pika-ux Module - Pre-built components
- Web Components Guide - How-to guide
- Custom Tags Guide - Creating custom tags
Type Definitions
Section titled “Type Definitions”Core Interfaces
Section titled “Core Interfaces”// Available from pika-shared/types/chatbot/chatbot-typesexport interface IChatAppState { /* ... */ }export interface IWidgetMetadataAPI { /* ... */ }export interface ChatUser { /* ... */ }export interface ChatApp { /* ... */ }Widget Metadata and Actions
Section titled “Widget Metadata and Actions”// Available from pika-shared/types/chatbot/webcomp-typesexport interface WidgetMetadata { title: string; lucideIconName?: string; iconSvg?: string; iconColor?: string; actions?: WidgetAction[]; loadingStatus?: { loading: boolean; loadingMsg?: string; };}
export interface WidgetAction { id: string; title: string; iconSvg: string; disabled?: boolean; primary?: boolean; /** * Callback when action is clicked. * Automatically receives WidgetCallbackContext parameter. * @since 0.11.0 */ callback: (context: WidgetCallbackContext) => void | Promise<void>;}
export interface WidgetCallbackContext { /** The web component element */ element: HTMLElement; /** Unique instance ID */ instanceId: string; /** Full Pika context */ context: PikaWCContext;}
export interface SpotlightWidgetDefinition { tag: string; scope: string; tagTitle: string; customElementName?: string; sizing?: WidgetSizing; componentAgentInstructionsMd?: Record<string, string>; autoCreateInstance?: boolean; displayOrder?: number; singleton?: boolean; showInUnpinnedMenu?: boolean; /** @since 0.11.0 */ metadata?: WidgetMetadata;}IChatAppState Methods
Section titled “IChatAppState Methods”Key methods available on chatAppState:
interface IChatAppState { // Rendering renderTag( tagId: string, context: WidgetRenderingContextType, data?: Record<string, any>, metadata?: WidgetMetadata // @since 0.11.0 ): Promise<void>; closeCanvas(): void; closeDialog(): void;
// Widget Management getWidgetInstance(instanceId: string): WidgetInstance | undefined; getWidgetContext(instanceId: string): PikaWCContext | undefined; // @since 0.11.0 getWidgetMetadataAPI(scope: string, tag: string, instanceId: string, context: WidgetRenderingContextType): IWidgetMetadataAPI; manuallyRegisterSpotlightWidget(definition: SpotlightWidgetDefinition): void;
// State Access readonly user: ChatUser; // @since 0.11.0 readonly currentSession: ChatSession; readonly currentSessionMessages: ChatMessageForRendering[]; readonly customDataForChatApp: Record<string, unknown> | undefined; readonly widgetInstances: Map<string, WidgetInstance>;
// Actions sendMessage(): Promise<void>; uploadFiles(files: File[]): Promise<void>;}IWidgetMetadataAPI Methods
Section titled “IWidgetMetadataAPI Methods”Methods for updating widget metadata dynamically:
interface IWidgetMetadataAPI { // Set all metadata at once setMetadata(metadata: WidgetMetadata): void;
// Update individual fields updateTitle(title: string): void; setLoadingStatus(loading: boolean, loadingMsg?: string): void;
// Manage actions addAction(action: WidgetAction): void; updateAction(actionId: string, updates: Partial<Omit<WidgetAction, 'id' | 'callback'>>): void; removeAction(actionId: string): void;}Import Statements
Section titled “Import Statements”// Tag definitions and core typesimport type { TagDefinition, WidgetInstance, ChatSession, ChatUser} from 'pika-shared/types/chatbot/chatbot-types';
// Widget metadata and action typesimport type { WidgetMetadata, WidgetAction, WidgetCallbackContext, SpotlightWidgetDefinition, PikaWCContext} from 'pika-shared/types/chatbot/webcomp-types';
// Utility functionsimport { getPikaContext, extractIconSvg } from 'pika-shared/util/wc-utils';Full type definitions available in pika-shared source code.