@paymanai/payman-ask-sdk
v4.0.46
Published
Reusable web chat SDK for Payman K2 Agents with streaming support
Maintainers
Readme
@paymanai/payman-ask-sdk
React chat component for Payman workflows — streaming, verification, voice, and session history out of the box.
Web UI v2 supports streaming Markdown, sandboxed HTML cards, images, audio, and video. See rich content examples and configuration.
v2.0 is a clean break from v1. The configuration API has been nested under
api,workflow,session,availability,ui, andobservability. See the Migrating from v1 section for a translation table.
Install
npm install @paymanai/payman-ask-sdk @paymanai/payman-typescript-ask-sdkimport "@paymanai/payman-ask-sdk/styles.css";
import { PaymanChat } from "@paymanai/payman-ask-sdk";Minimal example
<PaymanChat
config={{
api: {
baseUrl: "https://api.paymanai.com",
headers: { Authorization: `Bearer ${token}` },
},
workflow: {
id: "wf_01H…", // optional; required for session history
name: "my-agent",
version: 3,
stage: "PROD",
},
session: {
userId: "user-42", // client-side state-scope key
owner: { id: "user-42", label: "Alice" },
},
ui: {
emptyState: { text: "How can I help?" },
input: {
placeholder: "Ask anything…",
voice: true,
},
sessionHistory: true,
},
}}
callbacks={{
onStreamComplete: (msg) => console.log(msg),
}}
/>Configuration
The full config is a BaseChatConfig (from the core SDK) extended with availability, ui, and observability.
type ChatConfig = {
// ---- Core (shared with payman-typescript-ask-sdk) ------------------------
api: {
baseUrl: string;
apiKey?: string;
headers?: Record<string, string | undefined>;
streamEndpoint?: string; // default: "/api/workflows/ask/stream"
};
workflow: {
id?: string; // workflow uuid; required when `ui.sessionHistory` is enabled
name: string;
version?: number;
stage?: "DEV" | "SBX" | "PROD";
};
session?: {
userId?: string; // client-side state-scope key (NOT sent to API)
owner?: { id: string; label?: string };
autoGenerateId?: boolean; // default: true
initialId?: string;
initialMessages?: MessageDisplay[];
};
// ---- React-only ----------------------------------------------------------
availability?:
| { state: "ready" }
| { state: "readOnly" }
| { state: "disabled"; reason?: React.ReactNode };
ui?: {
layout?: "centered" | "full-width";
agent?: { name?: string; showName?: boolean };
emptyState?: {
text?: string;
icon?: boolean;
content?: React.ReactNode;
suggestions?: {
enabled?: boolean;
title?: string;
categories: Array<{
label: string;
icon?: React.ReactNode;
prompts: string[];
}>;
};
};
input?: {
placeholder?: string;
showResetButton?: boolean;
attachments?: boolean | { uploadImage?: boolean; attachFile?: boolean };
voice?: boolean | { lang?: string; interimResults?: boolean; continuous?: boolean };
};
messages?: {
avatars?: boolean | { user?: boolean; assistant?: boolean };
timestamps?: boolean;
streamingDot?: boolean;
actions?: {
userMessageActions?: { copy?: boolean; edit?: boolean; retry?: boolean };
assistantMessageActions?: { copy?: boolean; trace?: boolean };
};
};
execution?: {
showSteps?: boolean;
completedLabel?: string; // default: "Completed in {time}s ({count} steps)"
streamingLabel?: string; // default: "View progress ({count} steps)"
};
/**
* `true` — enable with defaults.
* object — override width/collapse/page-size/persistKey.
*/
sessionHistory?: boolean | {
defaultWidth?: number; // default: 260 px
minWidth?: number; // default: 220
maxWidth?: number; // default: 440
defaultCollapsed?: boolean; // desktop only; default: false
pageSize?: number; // default: 20
persistKey?: string; // default: `payman-chat-sidebar:${workflow.id}`
showNewSessionButton?: boolean; // default: false
};
};
observability?: {
sentryDsn?: string;
};
};Safe cosmetic defaults
Leaving the ui block out renders a usable ChatGPT-style surface:
- Layout:
full-width - Placeholder:
"Type your message..." - Empty-state text:
"What can I help with?" - Agent name:
"Assistant"(shown) - Streaming dot: on
- Execution steps: on
- No avatars, timestamps, or reset button
- No voice, no session history
Turn features on with booleans, and only reach for the full object form when you need custom labels, widths, or per-option toggles.
Suggested prompts
Use ui.emptyState.suggestions when an app wants SDK-rendered prompt shortcuts on the first screen. Apps own the content; the SDK owns rendering, accessibility, click handling, responsive layout, and styling.
The SDK renders categories as compact bubbles first. Clicking a category reveals that category's prompts below it; clicking a prompt sends the prompt text verbatim. On narrow screens, categories become a horizontal touch scroller so the empty state stays compact.
const isProd = process.env.NEXT_PUBLIC_ENV === "production";
const suggestions = {
enabled: true,
title: "Good evening\nHow can I help you today?",
categories: [
{ label: "ACCOUNTS", prompts: ["Show my account balance"] },
{ label: "PAYMENTS", prompts: ["Pay Sarah $50", "Transfer money to my savings"] },
...(isProd
? []
: [{ label: "SIMULATION TOOLS", prompts: ["Simulate a mid-stream error"] }]),
],
};
<PaymanChat
config={{
...config,
ui: {
...config.ui,
emptyState: {
...config.ui?.emptyState,
suggestions,
},
},
}}
/>enabled: false hides the bubbles without removing the data. Category and prompt arrays render in the order provided, and empty categories are ignored. Bubbles disappear after the first message because they are part of the empty state.
title is optional and falls back to ui.emptyState.text. Newlines are preserved, so pass "Good evening\nHow can I help you today?" when you want the original two-line empty-state layout. If a single-line greeting like "Good evening How can I help..." is provided, the SDK inserts a dash between the greeting and question.
icon is optional. When omitted, the SDK chooses a small built-in glyph from the category label for common labels like payments, accounts, insights, and products.
Suggested prompts use the same --payman-v2-* CSS variables as the rest of the chat surface, so app-level theme overrides automatically apply to bubble background, text, border, hover, radius, and focus states.
Session history
Pass ui.sessionHistory: true and provide workflow.id + session.owner.id. <PaymanChat/> mounts a resizable, collapsible sidebar on desktop and a hamburger-triggered drawer on mobile. Clicking a row calls loadSession(sessionId) — which cancels any in-flight stream, clears the current chat, fetches the conversation history, and renders it with isHistorical: true (renderers skip the thinking/step UI for those rows).
To show a sidebar reset button, opt in with ui.sessionHistory: { showNewSessionButton: true }. It runs the same confirmable New Session flow as the chat input button.
Need custom chrome? Mount the sidebar yourself:
import { SessionHistorySidebar, usePaymanChat } from "@paymanai/payman-ask-sdk";
function MyShell() {
const chat = usePaymanChat();
return (
<SessionHistorySidebar
config={config}
options={{ defaultWidth: 320 }}
onSelectSession={(s) => chat.loadSession(s.sessionId)}
mobileOpen={mobileOpen}
onMobileOpenChange={setMobileOpen}
/>
);
}The example above (
SessionHistorySidebar,ui.sessionHistory,chat.loadSession()) documents a config shape that isn't implemented in this package yet. For the sidebar that actually ships today, see Sessions sidebar below.
Sessions sidebar
Set config.sessions.enabled = true and <PaymanChat/> renders a "continue a chat" sidebar next to the conversation — fetching the list, loading a picked session's messages, and remounting the chat onto it are all handled internally. No extra state, endpoints, or loading/error UI to build in the host app.
<PaymanChat
config={{
api: { baseUrl, authToken, headers }, // same credential already used for streaming — JWT or API key, doesn't matter
agentId,
sessionParams,
sessions: {
enabled: true,
// Both default to these paths if omitted:
listEndpoint: "/api/ask/sessions",
messagesEndpoint: "/api/ask/sessions/{sessionId}/messages",
title: "Chats",
newChatLabel: "Start New Chat",
defaultOpen: true,
// Optional per-row "..." menu. Omit `actions` entirely to hide it.
actions: [
{
key: "delete",
label: "Delete",
destructive: true,
onSelect: async (session) => { /* ... */ },
},
],
},
}}
/>listEndpoint is appended to api.baseUrl and expected to return { data: ChatSessionSummary[] } (sessionId, title?, messageCount?, lastMessageAt?, status?). messagesEndpoint (with {sessionId} substituted) is expected to return { data: ChatSessionMessageEntry[] } (id, sessionUserIntent, agentResponse?, status?, startTime?, endTime?) — the same PageResponse<AgentUser...Response> shape K2's /conversations/sessions endpoint already returns. If your backend's JSON looks different, pass parseSessionsResponse / parseMessagesResponse to adapt it.
Auth is whatever you already pass in api.headers / api.authToken for streaming — there's no separate sessions-specific credential. ref.refreshSessions() force-refetches the list (e.g. after an out-of-band mutation); onStreamComplete triggers an automatic refetch after every turn.
Styling: see docs/v2-styling-reference.md for the full payman-v2-sessions-* class list and CSS vars.
usePaymanChat() hook
Call from any descendant of <PaymanChat/>:
const chat = usePaymanChat();
chat.resetSession();
chat.loadSession(sessionId);
chat.cancelStream();
chat.getSessionId();
chat.getMessages();Callbacks
The PaymanChatRef also exposes setDraft(text) to replace composer text without
sending or resetting the session, transcript, or attachments. On the web it focuses
the composer when enabled. Keep the component mounted when selecting a suggested
prompt and call ref.current?.setDraft(prompt).
Use ref.current?.replaceMessages(savedMessages) after reconciling saved history.
It atomically replaces only the transcript and returns true while idle. During
an active stream it returns false without changing anything. The session and
composer draft remain intact, including a newer draft selected during the request.
onStreamCancel fires once when Stop or ref.cancelStream() explicitly cancels
an active client stream. Idle/repeated cancellation, completion, errors, and
session resets do not emit it. Cancellation ends the client stream; it does not
confirm that server work was rolled back. Reconcile durable state before retrying
an action whose outcome is unknown.
type ChatCallbacks = {
onMessageSent?: (text: string) => void;
onStreamStart?: () => void;
onStreamComplete?: (message: MessageDisplay) => void;
onStreamCancel?: () => void;
onError?: (error: Error) => void;
onSessionIdChange?: (sessionId: string) => void;
onExecutionTraceClick?: (data: { message; tracingData?; executionId? }) => void;
onResetSession?: () => void;
onUploadImageClick?: () => void;
onAttachFileClick?: () => void;
onUserActionRequired?: (req: UserActionRequest) => void;
onUserActionEvent?: (eventType, message) => void;
};Migrating from v1
The v2 release removes the old flat config and the ui.engine = 1 path entirely. The v1 renderer has been deleted. Translation table:
| v1 (flat) | v2 (nested) |
| ------------------------------- | ------------------------------------------------ |
| api | api (unchanged) |
| workflowName | workflow.name |
| workflowVersion | workflow.version |
| stage | workflow.stage |
| sessionParams | session.owner |
| userId | session.userId |
| autoGenerateSessionId | session.autoGenerateId |
| initialSessionId | session.initialId |
| initialMessages | session.initialMessages |
| isChatDisabled / disabledComponent / hasAskPermission | availability: { state: "disabled" \| "readOnly" \| "ready", reason? } |
| sentryDsn | observability.sentryDsn |
| uiVersion | removed (v2 only) |
| enableVoice, voiceLang, voiceInterimResults, voiceContinuous | ui.input.voice: boolean \| VoiceOptions |
| enableVoiceMode | ui.input.voiceMode: boolean |
| placeholder | ui.input.placeholder |
| emptyStateText / showEmptyStateIcon / emptyStateComponent | ui.emptyState.{ text, icon, content } |
| showResetSession | ui.input.showResetButton |
| enableDeepModeToggle | removed — requests use the fast execution mode |
| showAttachmentButton / showUploadImageButton / showAttachFileButton | ui.input.attachments: boolean \| { uploadImage, attachFile } |
| messageActions | ui.messages.actions |
| showAvatars / showUserAvatar / showAssistantAvatar | ui.messages.avatars: boolean \| { user, assistant } |
| showTimestamps | ui.messages.timestamps |
| showStreamingDot | ui.messages.streamingDot |
| showExecutionSteps | ui.execution.showSteps |
| streamingStepsText | ui.execution.streamingLabel |
| completedStepsText | ui.execution.completedLabel |
| showAgentName / agentName | ui.agent.{ showName, name } |
| inputStyle / animated / sidebarBrandName / disableInput | removed (dead fields) |
| showSessionParams / header onLoadSession dropdown | removed — use ui.sessionHistory sidebar |
enableVoice enables browser dictation: the microphone transcript is inserted
into the composer for review and editing. enableVoiceMode enables the full K2
voice loop and replaces the composer with a listening/speaking surface while it
is active. Microphone audio, realtime transcription, listener acknowledgement,
pipeline execution, and selected response voice all stay behind K2. The
component never receives a provider credential; public agents reuse the
existing x-k2-api-key, while Studio uses its authenticated browser session.
Deleted APIs
useChat/UseChatReturn(v1). UseuseChatV2/UseChatV2Return.AgentMessage,MessageList,MessageRow,ChatInput,StreamingMessage,UserMessage,MessageRowSkeletoncomponent re-exports.ChatConfig.uiVersion.
New APIs
useChatV2().loadSession(sessionId)— replace the active chat with history.usePaymanChat().loadSession— same, from context.SessionHistorySidebarcomponent +useSessionHistoryhook.listSessions/listConversationsREST helpers (from the core SDK).buildScopeKey(config)— compose the client-side state-scope key fromsession.userId + workflow.id + workflow.version + workflow.stage.
License
MIT
Setup links as actions
Configure trusted setup links with linkActions. The matching markdown link becomes
a labeled button that opens the setup flow in another tab; it does not report completion.
Other links retain their normal rendering. Only the exact origin and path prefix match.
<PaymanChat config={{
...config,
linkActions: [{
origin: "https://yuki.example.com",
pathPrefix: "/connect/",
label: "Connect Yuki",
}],
}} />Use the setup backend's authenticated status when the customer returns to the chat. Never treat clicking a setup link as account access or payment approval.
The browser regression suite covers new and resumed chats, with and without a cache
identity, against explicit stream fixtures: bun run test:browser (Chromium required).
