Skip to content

Extra-Chill/chat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

98 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@extrachill/chat

React chat UI components with a built-in REST API client and backend-agnostic extension primitives.

Install

npm install @extrachill/chat

Quick Start

import { Chat } from '@extrachill/chat';
import '@extrachill/chat/css';

function ChatSurface() {
	return (
		<Chat
			basePath="/chat"
			fetchFn={fetchChatJson}
		/>
	);
}

What's Included

ComponentsChat, FloatingChatShell, ChatMessages, ChatMessage, ChatInput, TypingIndicator, ToolMessage, SessionSwitcher, ErrorBoundary, AvailabilityGate

HookuseChat manages messages, sessions, multi-turn continuation loops, and availability state

API clientsendMessage, continueResponse, listSessions, loadSession, deleteSession

Run controlcreateRunControlAdapter, useRunEvents, and typed run/event primitives for cancel, queue, status, and event access

NormalizernormalizeMessage, normalizeConversation, normalizeSession for mapping raw backend messages into the UI model

CSS@extrachill/chat/css provides base styles with 30+ CSS custom properties (--ec-chat-*) for theming

REST Contract

The package expects these endpoints at basePath:

Method Path Purpose
POST / Send a message (creates or continues session)
POST /continue Continue a multi-turn response
GET /sessions List sessions for the current user
GET /{session_id} Load a single session's conversation
DELETE /{session_id} Delete a session

Any backend implementing this contract works. The fetchFn prop accepts any function matching (options: { path, method?, data? }) => Promise<json>.

Theming

Override CSS custom properties on .ec-chat to match your design system:

.my-chat .ec-chat {
  --ec-chat-user-bg: var(--accent);
  --ec-chat-assistant-bg: var(--card-background);
  --ec-chat-font-family: var(--font-family-body);
  --ec-chat-border-radius: var(--border-radius-md);
}

Message Suggestions

Pass messageSuggestions to offer optional prompt starters on fresh conversations. Selecting one sends its message, or its label when message is omitted.

<Chat
	basePath="/chat"
	fetchFn={fetchChatJson}
	messageSuggestions={[
		{
      label: 'Plan my homepage',
      message: 'Help me plan the homepage for my site.',
      description: 'Start with goals and sections',
    },
    {
      label: 'Write an about page',
      message: 'Help me draft a friendly about page.',
    },
  ]}
/>

Floating Shell

Use FloatingChatShell when you want a launcher and floating drawer around the same backend-agnostic Chat component. The shell owns only visibility, expanded mode, unread badge state, and generic slots; product-specific UI should be supplied by the consumer.

import { FloatingChatShell } from '@extrachill/chat';

<FloatingChatShell
  basePath="/chat"
  fetchFn={fetchChatJson}
  title="Assistant"
  header={({ close }) => (
    <div className="my-chat-header">
      <strong>Assistant</strong>
      <button type="button" onClick={close}>Close</button>
    </div>
  )}
  launcher={({ toggleOpen, unreadCount }) => (
    <button type="button" onClick={toggleOpen}>
      Chat{unreadCount > 0 ? ` (${unreadCount})` : ''}
    </button>
  )}
/>

You can also control the shell externally with open, onOpenChange, expanded, and onExpandedChange.

Client Context

Register client-context providers when a surface has extra local state the backend may need. Chat includes that metadata only when clientContext is enabled, so existing inline usage is unchanged by default.

import { Chat, registerClientContextProvider } from '@extrachill/chat';

registerClientContextProvider({
  id: 'current-document',
  priority: 10,
  getContext: () => ({ documentId: getCurrentDocumentId() }),
});

<Chat
  basePath="/chat"
  fetchFn={fetchChatJson}
  clientContext
  clientContextOptions={{ metadataKey: 'client_context' }}
/>

Manual consumers can keep using useClientContextMetadata() and pass the result through metadata themselves.

Long-Running Turns

Backends that support long-running chat turns can opt into stop and queue UI without changing the default behavior. When no capability is provided, the input is disabled while a response is loading, matching earlier releases.

const runAdapter = createRunControlAdapter({
  basePath: '/api/chat',
  fetchFn: fetchChatJson,
  uploadFn: uploadAttachment,
});

<Chat
  basePath="/api/chat"
  fetchFn={fetchChatJson}
  initialSessionId="session-123"
  runAdapter={runAdapter}
/>

The adapter above uses generic REST-shaped run endpoints under basePath, exposes cancel/queue/status/events capabilities, uploads queued attachments through the supplied uploadFn, and normalizes event payloads into ChatRunEvent while preserving the raw event object.

Existing manual props continue to work for consumers that already own backend-specific callbacks:

<Chat
  basePath="/api/chat"
  fetchFn={fetchChatJson}
  initialSessionId="session-123"
  runCapabilities={{ cancel: true, queue: true }}
  activeRunId={activeRunId}
  onCancelRun={async ({ runId, sessionId }) => {
    await cancelRun({ runId, sessionId });
  }}
  onQueueMessage={async ({ sessionId, runId, content, files }) => {
    return queueMessage({ sessionId, runId, content, files });
  }}
/>

Capability behavior:

  • No support: input and message suggestions stay disabled while loading.
  • Cancel support: a stop control appears while loading once both activeRunId and sessionId are available.
  • Queue support: input and message suggestions stay usable while loading and submitted messages render optimistically with a queued state.
  • Cancel + queue support: both controls are enabled together.

The public TypeScript API uses camelCase (runId, queuedMessageId) while adapters can map whatever wire format their backend uses. If a backend returns run IDs in response metadata, adapters can expose them generically through getRunId:

<Chat
  basePath="/api/chat"
  fetchFn={fetchChatJson}
  runCapabilities={{ cancel: true }}
  getRunId={(metadata) => typeof metadata.run_id === 'string' ? metadata.run_id : null}
  onCancelRun={cancelRun}
/>

onQueueMessage may return { queuedMessageId, position, runId, sessionId, status } when the adapter has an acknowledgement payload. The UI does not require those fields, but the types preserve them for adapters that want to coordinate follow-up polling or session refreshes.

Run events can be consumed independently from the UI:

const { events, refresh } = useRunEvents({
  adapter: runAdapter,
  runId,
  sessionId,
  intervalMs: 2000,
});

License

GPL-2.0-or-later

About

React chat UI components with built-in REST API client. No adapters, no wrappers.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors