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.ts with your real credential exchange.
  • Transport: Swap createProxyTransport for createSessionApiTransport if you connect directly to the Conversational Engine Service.
  • Tool bodies: Wire lookupOrder and checkWarranty to your actual order and warranty APIs.
  • Renderers: Add entries to the renderers map to customize how any part type renders. The RendererMap keys match ChatMessagePart['type'] values (text, markdown, product-carousel, order-summary, error, etc.).
  • Error handling: The onError callback 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 ChatSession modifications.
  • Sentiment-aware escalation. Add a signals.adapters + signals.escalation block to ChatClientConfig so 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 same ChatSession without changing the rest of the surface. See Voice Integration.
  • Resolution semantics. Have the cockpit call completeSession() when a handoff is resolved, distinct from a generic endSession(). See Agent Cockpit.
  • Eval gate. Lock in the support chat's quality with gecx eval scenarios — tool-call-accuracy, no-handoff on deflectable queries, latency-p95-under for first-token. See Evaluation.
Source: docs/examples/support-chat.md