Embed the Chat Widget in React
Add a fully functional natural language query interface to your React application with minimal setup.
Installation
npm install @nexatron/chat
Wrap Your App in the Provider
The NexatronProvider manages authentication, connection state, and conversation history:
// app/layout.tsx or app/providers.tsx
import { NexatronProvider } from '@nexatron/chat/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<NexatronProvider
apiKey={process.env.NEXT_PUBLIC_NEXATRON_API_KEY}
baseUrl="https://api.nexatron.dev"
theme="auto" // "light" | "dark" | "auto"
>
{children}
</NexatronProvider>
);
}
Use the Chat Hook
The useNexatronChat hook provides everything you need to build a custom query interface:
import { useNexatronChat } from '@nexatron/chat/react';
function QueryPanel() {
const {
query, // (text: string) => Promise<QueryResult>
messages, // Array of user and assistant messages
isLoading, // true while a query is in flight
stage, // Current pipeline stage or null
error, // Last error or null
clearHistory, // Reset the conversation
conversationId, // Current conversation ID
} = useNexatronChat();
const [input, setInput] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const text = input;
setInput('');
await query(text);
};
return (
<div className="flex flex-col h-full">
{/* Message list */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<MessageBubble key={msg.id} message={msg} />
))}
{stage && (
<div className="text-sm text-gray-500 animate-pulse">
{stage.name}: {stage.message}
</div>
)}
{error && (
<div className="text-sm text-red-600">
Error: {error.message}
</div>
)}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="border-t p-4 flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask a question about your data..."
className="flex-1 border rounded-lg px-3 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-indigo-600 text-white px-4 py-2 rounded-lg disabled:opacity-50"
>
Ask
</button>
</form>
</div>
);
}
Message Bubble Component
Render results with SQL preview, data tables, and summaries:
import type { ChatMessage } from '@nexatron/chat/react';
function MessageBubble({ message }: { message: ChatMessage }) {
if (message.role === 'user') {
return (
<div className="flex justify-end">
<div className="bg-indigo-600 text-white rounded-lg px-4 py-2 max-w-md">
{message.content}
</div>
</div>
);
}
return (
<div className="flex justify-start">
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg px-4 py-3 max-w-2xl space-y-3">
{/* Natural language summary */}
{message.summary && <p>{message.summary}</p>}
{/* Data table */}
{message.data && message.data.length > 0 && (
<div className="overflow-x-auto">
<table className="text-sm border-collapse w-full">
<thead>
<tr>
{Object.keys(message.data[0]).map((col) => (
<th key={col} className="border-b px-3 py-1 text-left font-medium">
{col}
</th>
))}
</tr>
</thead>
<tbody>
{message.data.map((row, i) => (
<tr key={i}>
{Object.values(row).map((val, j) => (
<td key={j} className="border-b px-3 py-1">
{String(val)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{/* SQL toggle */}
{message.sql && (
<details className="text-xs">
<summary className="cursor-pointer text-gray-500">View SQL</summary>
<pre className="mt-1 bg-gray-900 text-green-400 p-2 rounded overflow-x-auto">
{message.sql}
</pre>
</details>
)}
{/* Follow-up suggestions */}
{message.suggestions && (
<div className="flex flex-wrap gap-2">
{message.suggestions.map((s, i) => (
<button
key={i}
className="text-xs border rounded-full px-3 py-1 hover:bg-indigo-50"
>
{s}
</button>
))}
</div>
)}
</div>
</div>
);
}
Full Working Example
A self-contained page you can drop into a Next.js app:
// app/ask/page.tsx
'use client';
import { useState } from 'react';
import { NexatronProvider, useNexatronChat } from '@nexatron/chat/react';
import type { ChatMessage } from '@nexatron/chat/react';
function ChatInterface() {
const { query, messages, isLoading, stage } = useNexatronChat();
const [input, setInput] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const text = input;
setInput('');
await query(text);
};
return (
<div className="max-w-3xl mx-auto h-screen flex flex-col">
<header className="p-4 border-b">
<h1 className="text-xl font-semibold">Data Assistant</h1>
</header>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<p className="text-gray-400 text-center mt-20">
Ask a question about your data to get started.
</p>
)}
{messages.map((msg) => (
<div
key={msg.id}
className={msg.role === 'user' ? 'text-right' : 'text-left'}
>
<div
className={`inline-block rounded-lg px-4 py-2 max-w-lg ${
msg.role === 'user'
? 'bg-indigo-600 text-white'
: 'bg-gray-100 dark:bg-gray-800'
}`}
>
{msg.role === 'user' ? msg.content : msg.summary}
{msg.data && (
<pre className="text-xs mt-2 overflow-x-auto">
{JSON.stringify(msg.data.slice(0, 5), null, 2)}
</pre>
)}
</div>
</div>
))}
{stage && (
<p className="text-sm text-gray-500 animate-pulse">
{stage.name}...
</p>
)}
</div>
<form onSubmit={handleSubmit} className="border-t p-4 flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="e.g. What were total sales last month?"
className="flex-1 border rounded-lg px-3 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-indigo-600 text-white px-4 py-2 rounded-lg disabled:opacity-50"
>
{isLoading ? 'Thinking...' : 'Ask'}
</button>
</form>
</div>
);
}
export default function AskPage() {
return (
<NexatronProvider apiKey={process.env.NEXT_PUBLIC_NEXATRON_API_KEY!}>
<ChatInterface />
</NexatronProvider>
);
}
Next Steps
- SDK Quickstart for programmatic usage
- MCP Integration for Claude Desktop
- API Reference for direct REST calls