@gravityai-dev/gravity-client
v0.3.10
Published
React SDK for Gravity AI - The experience layer for AI
Maintainers
Readme
@gravityai-dev/gravity-client
React SDK for Gravity AI - Real-time AI workflow streaming with WebSocket.
Installation
npm install @gravityai-dev/gravity-clientQuick Start
import { GravityClient, UserProvider } from "@gravityai-dev/gravity-client";
function App() {
return (
<UserProvider>
<ChatPage />
</UserProvider>
);
}
function ChatPage() {
return (
<GravityClient
config={{
apiUrl: "http://localhost:4100",
wsUrl: "ws://localhost:4100/ws/gravity-ds",
}}
session={{
conversationId: "conv_123",
userId: "user_456",
workflowId: "wf-abc123",
targetTriggerNode: "inputtrigger1",
}}
onReady={({ sendMessage }) => {
console.log("Connected to Gravity!");
}}
/>
);
}Core Concepts
Session Initialization
When GravityClient mounts, it:
- Connects to WebSocket
- Sends
INIT_SESSIONwithworkflowIdandtargetTriggerNode - Server loads the template configured on that InputTrigger node
- Template renders in the client
No message is sent - just the template loads.
Sending Messages
Templates use client.sendMessage() to send user messages to the workflow.
Template Switching
When you send a message with a different targetTriggerNode, the server:
- Checks if the new trigger has a different template
- If different, sends
COMPONENT_INITto switch templates - Executes the workflow from that trigger
Client API
Templates receive a client object with these methods:
client.sendMessage(message, options?)
Send a message to the workflow.
// Basic message (uses session's default targetTriggerNode)
client.sendMessage("Hello, I need help");
// Message to a specific trigger (may switch template)
client.sendMessage("I want to speak to a person", {
targetTriggerNode: "inputtrigger2",
});| Parameter | Type | Description |
| --------------------------- | --------- | --------------------------------------------------------- |
| message | string | The user's message |
| options.targetTriggerNode | string? | InputTrigger to route to (switches template if different) |
What happens:
- Adds user message to history
- Sends to server via GraphQL
- Server checks if template needs to switch
- Server executes workflow from the trigger
- Server streams components back via WebSocket
client.sendAgentMessage(data)
Send an agent message through the server pipeline. Used for live agent integrations (Amazon Connect, etc.).
client.sendAgentMessage({
content: "Hello, I'm your live agent",
chatId: "chat_123",
agentName: "Support Agent",
source: "amazon_connect",
props: { interactiveData: { type: "ListPicker", elements: [...] } },
metadata: { participantRole: "AGENT" },
});| Parameter | Type | Description |
| ----------- | ---------------------- | -------------------------------------------------- |
| content | string | Message content |
| chatId | string | Chat ID for conversation continuity |
| agentName | string? | Agent display name |
| source | string? | Source identifier (e.g., "amazon_connect") |
| props | Record<string, any>? | Additional component props (e.g., interactiveData) |
| metadata | Record<string, any>? | Additional metadata |
What happens:
- Sends
AGENT_MESSAGEto server via WebSocket - Server creates
WORKFLOW_STARTED→COMPONENT_INIT→WORKFLOW_COMPLETEDevents - Client receives events, loads component, adds to history
- Template renders agent message like any AI response
client.loadTemplate(targetTriggerNode, options?)
Switch to a different template without sending a message.
// Just switch template, no workflow execution
client.loadTemplate("inputtrigger1");
// Switch template and add to existing response (same chatId)
client.loadTemplate("inputtrigger1", { chatId: "chat_123" });| Parameter | Type | Description |
| ------------------- | --------- | ------------------------------------------------ |
| targetTriggerNode | string | InputTrigger whose template to load |
| options.chatId | string? | Optional chatId to add template to same response |
What happens:
- Sends
LOAD_TEMPLATEvia WebSocket - Server looks up template from the trigger (cached, fast)
- If different from current, sends
COMPONENT_INIT - Client loads and renders new template
- No workflow execution, no LLM call
If chatId is provided, the template is associated with that response, allowing you to continue adding components to the same conversation turn.
client.emitAction(type, data)
Emit a custom action event for cross-boundary communication.
// Emit from template (inside Shadow DOM)
client.emitAction("end_live_chat", { reason: "user_ended" });The host app can listen via onAction prop on GravityClient.
client.history
Access conversation history (read-only).
// Read all entries
client.history.entries;
// Get only AI responses
client.history.getResponses();Note: Templates should NOT modify history directly. Use
client.sendAgentMessage()for agent messages - this routes through the server pipeline ensuring consistent component loading.
client.sendVoiceCallMessage(data)
Send a voice call control message (START_CALL or END_CALL) for voice streaming.
// Start a voice call
await client.sendVoiceCallMessage({
message: "Start call",
userId: "user_123",
chatId: "voice_session_123",
conversationId: "conv_123",
workflowId: "wf-voice",
targetTriggerNode: "inputtrigger_voice",
action: "START_CALL",
});
// End a voice call
await client.sendVoiceCallMessage({
message: "END_CALL",
userId: "user_123",
chatId: "voice_session_123",
conversationId: "conv_123",
workflowId: "wf-voice",
targetTriggerNode: "inputtrigger_voice",
action: "END_CALL",
});| Parameter | Type | Description |
| ------------------- | ---------------------------- | -------------------------------- |
| message | string | Message content |
| userId | string | User ID |
| chatId | string | Chat ID for this voice session |
| conversationId | string | Conversation ID |
| workflowId | string | Workflow ID for voice processing |
| targetTriggerNode | string | InputTrigger for voice workflow |
| action | "START_CALL" \| "END_CALL" | Voice call action type |
What happens:
- Sends GraphQL mutation with
isAudio: trueflag - Server routes to voice workflow (e.g., AWS Nova Speech)
- Audio WebSocket connection handles bidirectional streaming
- Voice responses stream back via
/ws/audio/{sessionId}
client.session
Access session parameters.
client.session.conversationId;
client.session.userId;
client.session.workflowId;
client.session.targetTriggerNode;client.audio
Audio utilities for voice calls. Provides microphone capture, audio playback, and WebSocket transport.
interface AudioContext {
capture: {
startCapture: () => Promise<{ success: boolean; reason?: string }>;
stopCapture: () => Promise<{ success: boolean; reason?: string }>;
isCapturing: boolean;
isLoading: boolean;
error: string | null;
};
playback: {
playAudio: (audioData: ArrayBuffer) => void;
stopAll: () => void;
isPlaying: boolean;
};
websocket: {
connect: () => Promise<void>;
disconnect: () => void;
sendAudio: (audioData: ArrayBuffer) => void;
sendControl: (type: string, data?: Record<string, any>) => void;
isConnected: boolean;
};
}Usage in voice templates:
// Start a voice call
const startCall = async () => {
// 1. Connect WebSocket first
await client.audio.websocket.connect();
// 2. Send START_CALL via GraphQL
await client.sendVoiceCallMessage({ action: "START_CALL", ... });
// 3. Start microphone capture
await client.audio.capture.startCapture();
};
// End a voice call
const endCall = async () => {
await client.audio.capture.stopCapture();
client.audio.playback.stopAll();
client.audio.websocket.disconnect();
await client.sendVoiceCallMessage({ action: "END_CALL", ... });
};Key points:
- Audio utilities are instantiated by
TemplateRendererand passed to templates - Templates should NOT implement audio logic - use
client.audio.* - WebSocket connects to
/ws/audio/{conversationId}for bidirectional audio streaming - Microphone capture uses VAD (Voice Activity Detection) for speech detection
GravityClient Props
<GravityClient
config={GravityConfig}
session={SessionParams}
onReady?: ({ sendMessage, sessionParams }) => void
onAction?: (type: string, data: any) => void
onStateChange?: (state) => void
templateProps?: Record<string, any>
LoadingComponent?: React.ComponentType
/>config
interface GravityConfig {
apiUrl: string; // Server URL (e.g., "http://localhost:4100")
wsUrl: string; // WebSocket URL (e.g., "ws://localhost:4100/ws/gravity-ds")
getAccessToken?: () => Promise<string>; // Auth token provider
}session
interface SessionParams {
conversationId: string; // Unique conversation ID
userId: string; // User ID
workflowId: string; // Workflow to execute
targetTriggerNode: string; // Default InputTrigger (loads its template)
}onReady
Called when connection is established and ready.
onReady={({ sendMessage, sessionParams }) => {
// Store sendMessage for use outside templates
sendMessageRef.current = sendMessage;
}}onAction
Called when templates emit actions via client.emitAction().
onAction={(type, data) => {
if (type === "end_live_chat") {
// Handle end of live chat
sendMessage("Return to main chat", { targetTriggerNode: "inputtrigger1" });
}
}}templateProps
Props passed to all templates.
templateProps={{
amazonConnectConfig: { ... },
theme: "dark",
}}Template Development
Templates receive a client prop with the full API:
interface GravityTemplateProps {
client: {
sendMessage: (message: string, options?: { targetTriggerNode?: string }) => void;
sendAgentMessage: (data: {
content: string;
chatId: string;
agentName?: string;
source?: string;
props?: Record<string, any>;
metadata?: Record<string, any>;
}) => void;
sendVoiceCallMessage: (data: {
message: string;
userId: string;
chatId: string;
conversationId: string;
workflowId: string;
targetTriggerNode: string;
action: "START_CALL" | "END_CALL";
}) => Promise<void>;
emitAction: (type: string, data: any) => void;
history: {
entries: HistoryEntry[];
getResponses: () => AssistantResponse[];
};
session: SessionParams;
};
}
function MyTemplate({ client }: GravityTemplateProps) {
const handleSend = (message: string) => {
client.sendMessage(message);
};
const handleSwitchToLiveChat = () => {
client.sendMessage("Connect me to an agent", {
targetTriggerNode: "inputtrigger2", // Switches to live chat template
});
};
// For live agent integrations (Amazon Connect, etc.)
const handleAgentMessage = (response: AssistantResponse) => {
const props = response.components?.[0]?.props || {};
const { content, ...otherProps } = props;
client.sendAgentMessage({
content: content || "",
chatId: response.chatId || `agent_${Date.now()}`,
agentName: response.components?.[0]?.metadata?.agentName || "Agent",
source: "amazon_connect",
props: otherProps,
metadata: response.components?.[0]?.metadata,
});
};
const handleEndChat = () => {
// Emit action to host app
client.emitAction("end_live_chat", {});
};
return (
<div>
<ChatHistory entries={client.history.entries} />
<ChatInput onSend={handleSend} />
</div>
);
}Message Flow
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. GravityClient mounts │
│ ↓ │
│ 2. WebSocket connects │
│ ↓ │
│ 3. Sends INIT_SESSION { workflowId, targetTriggerNode } │
│ ↓ │
│ 4. Server sends COMPONENT_INIT with template │
│ ↓ │
│ 5. Template loads and renders │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 6. User types message │
│ ↓ │
│ 7. Template calls client.sendMessage("hello") │
│ ↓ │
│ 8. GraphQL mutation to server │
│ ↓ │
│ 9. Server executes workflow │
│ ↓ │
│ 10. Server streams components via WebSocket │
│ ↓ │
│ 11. Components render in template │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 12. User clicks "Speak to Live Person" │
│ ↓ │
│ 13. Template calls client.sendMessage("...", { │
│ targetTriggerNode: "inputtrigger2" │
│ }) │
│ ↓ │
│ 14. Server detects different template on inputtrigger2 │
│ ↓ │
│ 15. Server sends COMPONENT_INIT for new template │
│ ↓ │
│ 16. New template loads and renders │
│ ↓ │
│ 17. Server executes workflow from inputtrigger2 │
│ │
└─────────────────────────────────────────────────────────────────┘Low-Level Hooks
For advanced use cases, these hooks are available:
useUser()
const { userId, loading, updateUserId } = useUser();useGravityWebSocket(sessionParams, wsUrl)
const { isConnected, isReady, events, sendUserAction } = useGravityWebSocket(
sessionParams,
"ws://localhost:4100/ws/gravity-ds"
);useHistoryManager(sessionParams, options)
const { history, activeTemplate } = useHistoryManager(sessionParams, {
loadComponent,
sendComponentReady,
events,
});useComponentLoader(apiUrl)
const { loadComponent, componentCache } = useComponentLoader("http://localhost:4100");License
MIT
