Example: Support Chat
A complete support chat built with GECX Chat SDK in a Next.js app. Includes a server-side token route, two client tools (lookup_order and check_warranty), custom citation rendering, suggestion chips, streaming indicator, and error display.
File structure
app/
api/chat/token/route.ts -- server token endpoint
support/page.tsx -- page that mounts the chat
lib/
supportClient.ts -- chat client setup
supportTools.ts -- client tool definitions
components/
SupportChat.tsx -- React chat component
1. Server token route
// app/api/chat/token/route.ts
import { NextResponse } from 'next/server';
export async function POST() {
// Exchange your service credentials for a short-lived chat token.
// In production this calls your backend or the Google token broker.
const res = await fetch(process.env.TOKEN_BROKER_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.SERVICE_ACCOUNT_KEY}`,
},
body: JSON.stringify({ scope: 'chat' }),
});
const { token, expiresAt } = await res.json();
return NextResponse.json({ token, expiresAt });
}
2. Client tools
// lib/supportTools.ts
import { defineClientTool } from 'gecx-chat';
export const lookupOrder = defineClientTool({
name: 'lookup_order',
description: 'Look up order details by order ID',
inputSchema: {
type: 'object',
required: ['orderId'],
properties: { orderId: { type: 'string' } },
},
execute: async (input) => {
const { orderId } = input as { orderId: string };
const res = await fetch(`/api/orders/${orderId}`);
if (!res.ok) return { error: 'Order not found' };
return res.json();
},
});
export const checkWarranty = defineClientTool({
name: 'check_warranty',
description: 'Check warranty status for a product serial number',
inputSchema: {
type: 'object',
required: ['serialNumber'],
properties: { serialNumber: { type: 'string' } },
},
execute: async (input) => {
const { serialNumber } = input as { serialNumber: string };
const res = await fetch(`/api/warranty/${serialNumber}`);
if (!res.ok) return { valid: false, reason: 'Serial number not found' };
return res.json();
},
});
export const supportTools = [lookupOrder, checkWarranty];
3. Chat client
// lib/supportClient.ts
import {
createChatClient,
tokenEndpointAuth,
createProxyTransport,
} from 'gecx-chat';
import { supportTools } from './supportTools';
export function createSupportClient() {
return createChatClient({
auth: tokenEndpointAuth({ endpoint: '/api/chat/token' }),
transport: createProxyTransport({
endpoint: '/api/chat/proxy',
}),
tools: supportTools,
environment: 'production',
});
}
4. React component
// components/SupportChat.tsx
'use client';
import { useMemo, useCallback } from 'react';
import {
ChatProvider,
useChatSession,
MessageList,
Composer,
SuggestionBar,
ChatSurface,
} from 'gecx-chat/react';
import type { CitationPart, ChatMessage, RendererMap } from 'gecx-chat/react';
import { createSupportClient } from '@/lib/supportClient';
// Custom renderer for citation parts
function CitationCard({ title, url, snippet }: CitationPart) {
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="citation-card"
>
<strong>{title}</strong>
{snippet && <p>{snippet}</p>}
</a>
);
}
const renderers: RendererMap = {
citation: (part) => <CitationCard {...part} />,
};
function SupportChatInner() {
const {
messages,
status,
error,
isStreaming,
canSend,
input,
send,
stop,
} = useChatSession({
onError: (err) => console.error('[SupportChat]', err),
});
const handleSend = useCallback(
(text: string) => { send(text); },
[send],
);
// Extract suggestion chips from the last agent message
const suggestions = useMemo(() => {
const last = [...messages].reverse().find((m) => m.role === 'agent');
if (!last) return [];
for (const part of last.parts) {
if (part.type === 'suggestion-chips') {
return part.chips.map((c) => ({
label: c.label,
value: c.value ?? c.label,
}));
}
}
return [];
}, [messages]);
return (
<ChatSurface brand="Acme" title="Support" status={status}>
{/* Error banner */}
{error && (
<div role="alert" className="error-banner">
{error.message}
</div>
)}
{/* Messages */}
<MessageList
messages={messages}
emptyState={<p>How can we help you today?</p>}
/>
{/* Streaming indicator */}
{isStreaming && <p className="typing-indicator">Agent is typing...</p>}
{/* Suggestion chips */}
{suggestions.length > 0 && (
<SuggestionBar suggestions={suggestions} onSelect={handleSend} />
)}
{/* Composer */}
<Composer
input={input}
canSend={canSend}
isStreaming={isStreaming}
onSend={handleSend}
onStop={stop}
placeholder="Describe your issue..."
/>
</ChatSurface>
);
}
export default function SupportChat() {
const client = useMemo(() => createSupportClient(), []);
return (
<ChatProvider client={client} renderers={renderers}>
<SupportChatInner />
</ChatProvider>
);
}
5. Mount it
// app/support/page.tsx
import SupportChat from '@/components/SupportChat';
export default function SupportPage() {
return (
<main>
<h1>Customer Support</h1>
<SupportChat />
</main>
);
}
What to adapt
- Token route: Replace the fetch in
route.tswith your real credential exchange. - Transport: Swap
createProxyTransportforcreateSessionApiTransportif you connect directly to the Conversational Engine Service. - Tool bodies: Wire
lookupOrderandcheckWarrantyto your actual order and warranty APIs. - Renderers: Add entries to the
renderersmap to customize how any part type renders. TheRendererMapkeys matchChatMessagePart['type']values (text,markdown,product-carousel,order-summary,error, etc.). - Error handling: The
onErrorcallback fires for both transport and tool errors. Surface these in your UI as you see fit.
Going further
- Split one prompt into specialists. Once the system prompt covers more than a couple of domains, it becomes hard to reason about. Agent Graph shows how to route to specialists over A2A with one transport-wrapper change and no
ChatSessionmodifications. - Sentiment-aware escalation. Add a
signals.adapters+signals.escalationblock toChatClientConfigso frustrated users get to a human without manually requesting transfer. See Sentiment and Intent. - Long-term memory. Save preferences and prior context with
memory: { adapter: createHybridMemoryAdapter({ remote, cache }) }. See Memory. - Voice.
voice: 'auto'plus<VoiceToggle>adds voice to the sameChatSessionwithout changing the rest of the surface. See Voice Integration. - Resolution semantics. Have the cockpit call
completeSession()when a handoff is resolved, distinct from a genericendSession(). See Agent Cockpit. - Eval gate. Lock in the support chat's quality with
gecx evalscenarios —tool-call-accuracy,no-handoffon deflectable queries,latency-p95-underfor first-token. See Evaluation.
Source:
docs/examples/support-chat.md