# PostMessage Protocol
Source: https://docs.thesys.dev/agent-builder/advanced/postmessage-protocol
Low-level postMessage API for communicating with a Thesys agent embedded via iframe.
If you're using the **embed widget** npm package, you don't need this page — the widget handles all postMessage communication internally. This protocol is for developers who embed the agent using a **direct iframe** and want programmatic control.
The Thesys agent iframe communicates with the parent window using the browser's `postMessage` API. Messages are plain objects with a `type` field that identifies the action.
***
## Parent → Iframe
Send messages to the agent by calling `postMessage` on the iframe's `contentWindow`:
```javascript theme={null}
const iframe = document.getElementById('thesys-agent');
iframe.contentWindow.postMessage(
{ type: 'THESYS_SEND_MESSAGE', message: 'Hello!', newThread: false },
new URL(iframe.src).origin
);
// Reset the current thread (start a new chat)
iframe.contentWindow.postMessage(
{ type: 'THESYS_RESET_THREAD' },
new URL(iframe.src).origin
);
// Toggle the conversation history sidebar
iframe.contentWindow.postMessage(
{ type: 'THESYS_TOGGLE_SIDEBAR' },
new URL(iframe.src).origin
);
```
Always use a specific `targetOrigin` (e.g. `new URL(iframe.src).origin`) instead of `"*"` to prevent leaking data if the iframe navigates to an unexpected URL.
### Message Types
| Type | Payload | Description |
| --------------------------------- | ----------------------------------------- | ---------------------------------------------------------- |
| `THESYS_SEND_MESSAGE` | `{ message: string, newThread: boolean }` | Send a user message to the agent |
| `THESYS_SET_INPUT` | `{ message: string, newThread: boolean }` | Prefill the chat input without sending |
| `THESYS_IDENTITY_TOKEN_REFRESHED` | `{ identityToken: string }` | Provide a fresh identity token after a refresh request |
| `THESYS_RESET_THREAD` | *(none)* | Start a new conversation thread, clearing the current chat |
| `THESYS_TOGGLE_SIDEBAR` | *(none)* | Open or close the conversation history sidebar |
***
## Iframe → Parent
The agent sends messages to the parent window. Listen for them with `addEventListener`:
```javascript theme={null}
const expectedOrigin = new URL(iframe.src).origin;
window.addEventListener('message', (event) => {
if (event.origin !== expectedOrigin) return;
if (!event.data || typeof event.data.type !== 'string') return;
switch (event.data.type) {
case 'THESYS_APP_READY':
// Agent has finished loading and is ready
break;
case 'THESYS_THREAD_CHANGED':
// Active thread changed — event.data.threadId
break;
case 'THESYS_NEW_THREAD':
// User started a new chat
break;
case 'THESYS_USER_MESSAGE_SENT':
// User sent a message — event.data.message, event.data.threadId
break;
case 'THESYS_GENERATION_STARTED':
// Agent started generating — event.data.threadId, event.data.messageId
break;
case 'THESYS_GENERATION_ENDED':
// Agent finished generating — event.data.threadId, event.data.messageId
// event.data.message holds the full persisted assistant message,
// only when this origin is on the allowedParentOrigins list.
break;
case 'THESYS_TOOL_EXECUTION_STARTED':
// Tool started — event.data.toolName, event.data.threadId
break;
case 'THESYS_TOOL_EXECUTION_ENDED':
// Tool finished — event.data.toolName, event.data.threadId, event.data.error
break;
case 'THESYS_AGENT_ERROR':
// Error occurred — event.data.code, event.data.message
break;
case 'THESYS_WIDGET_CLOSE':
// Agent requested to close
break;
case 'THESYS_WIDGET_OPEN':
// Agent requested to open
break;
case 'THESYS_WIDGET_TOGGLE':
// Agent requested to toggle open/close
break;
case 'THESYS_IDENTITY_TOKEN_REFRESH_NEEDED':
// Identity token expired — fetch a new one and send it back
break;
case 'THESYS_IDENTITY_TOKEN_ERROR':
// Identity token error — event.data.code, event.data.message
break;
}
});
```
Always validate `event.origin` against the expected iframe origin to prevent unauthorized windows from triggering actions.
### Message Types
| Type | Payload | Description |
| -------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `THESYS_APP_READY` | *(none)* | The agent has finished loading and is ready for interaction |
| `THESYS_WIDGET_CLOSE` | *(none)* | The agent is requesting to be closed |
| `THESYS_WIDGET_OPEN` | *(none)* | The agent is requesting to be opened |
| `THESYS_WIDGET_TOGGLE` | *(none)* | The agent is requesting to toggle its visibility |
| `THESYS_THREAD_CHANGED` | `{ threadId: string }` | The active conversation thread changed |
| `THESYS_NEW_THREAD` | *(none)* | The user started a new chat (before the thread is created) |
| `THESYS_USER_MESSAGE_SENT` | `{ message: string, threadId: string }` | The user sent a message |
| `THESYS_GENERATION_STARTED` | `{ threadId: string, messageId: string }` | The agent started generating a response |
| `THESYS_GENERATION_ENDED` | `{ threadId: string, messageId: string, message?: AssistantMessage }` | The agent finished generating a response. `message` is the full persisted assistant item, present only for allowlisted origins (see [Allowed Parent Origins](#allowed-parent-origins)). |
| `THESYS_TOOL_EXECUTION_STARTED` | `{ toolName: string, threadId: string }` | A tool started executing |
| `THESYS_TOOL_EXECUTION_ENDED` | `{ toolName: string, threadId: string, error?: string }` | A tool finished executing |
| `THESYS_AGENT_ERROR` | `{ code: string, message: string }` | An error occurred during processing |
| `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED` | *(none)* | The identity token has expired; parent should provide a new one |
| `THESYS_IDENTITY_TOKEN_ERROR` | `{ code: string, message: string }` | An identity token error occurred |
If you're using the **embed widget** npm package, you can use the higher-level [Events API](/agent-builder/embed-widget/events) instead of listening for raw `postMessage` events.
***
## Allowed Parent Origins
For security, the agent splits its outgoing messages into two classes:
* **Protocol events** — the handshake (`THESYS_APP_READY`), widget controls (`THESYS_WIDGET_OPEN/CLOSE/TOGGLE`), and `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED`. These are required for the embed to function and are always sent to `'*'` with their full payload.
* **Data events** — anything that carries information about the user's conversation: `THESYS_THREAD_CHANGED`, `THESYS_NEW_THREAD`, `THESYS_USER_MESSAGE_SENT`, `THESYS_GENERATION_STARTED`, `THESYS_GENERATION_ENDED`, `THESYS_AGENT_ERROR`, `THESYS_TOOL_EXECUTION_STARTED/ENDED`, and `THESYS_IDENTITY_TOKEN_ERROR`.
Data events are only delivered with their full payload to parent origins on the playground's **Allowed Parent Origins** allowlist. When the allowlist is empty, the event still fires, but the payload is stripped to just `{ type }` so non-opted-in embedders cannot read private conversation data.
```javascript theme={null}
// Allowlist empty (default):
// Parent receives: { type: 'THESYS_GENERATION_ENDED' }
//
// Allowlist contains the parent's origin:
// Parent receives: {
// type: 'THESYS_GENERATION_ENDED',
// threadId: '…',
// messageId: '…',
// message: { id, role: 'assistant', content: [...], ... }
// }
```
### Configuring the allowlist
1. Open your playground in the [Thesys Console](https://console.thesys.dev/).
2. Click **Deploy**, expand the **Embed on your website** section.
3. Under **Allowed parent origins**, add each origin (e.g. `https://app.example.com`, `http://localhost:3000`) that should receive full event payloads.
4. Republish the playground — the new allowlist applies to all subsequent loads of the iframe.
Origins must be exact `scheme://host[:port]` matches. No paths, no trailing slashes, no wildcards.
***
## Identity Token Refresh
When a [BYOI](/agent-builder/byoi) identity token expires during an active session, the agent sends `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED` to the parent. The parent should fetch a new token from its backend and reply with `THESYS_IDENTITY_TOKEN_REFRESHED`:
```javascript theme={null}
const iframe = document.getElementById('thesys-agent');
const agentOrigin = new URL(iframe.src).origin;
window.addEventListener('message', async (event) => {
if (event.origin !== agentOrigin) return;
if (event.data?.type === 'THESYS_IDENTITY_TOKEN_REFRESH_NEEDED') {
try {
const res = await fetch('/api/thesys-token');
const { token } = await res.json();
iframe.contentWindow.postMessage(
{ type: 'THESYS_IDENTITY_TOKEN_REFRESHED', identityToken: token },
agentOrigin
);
} catch (error) {
console.error('Failed to refresh identity token:', error);
}
}
});
```
If the parent doesn't respond within **10 seconds**, the agent shows an error modal to the user.
***
## URL Parameters
When embedding via a direct iframe, you can configure the agent through URL query parameters:
| Parameter | Value | Description |
| ------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `IDENTITY_TOKEN` | JWT string | Sets the initial identity token for [BYOI](/agent-builder/byoi) |
| `HIDE_LOGIN` | `"true"` | Hides the Thesys login UI |
| `appRenderContext` | `TRAY_EMBED_WIDGET` \| `FULLSCREEN_EMBED_WIDGET` \| `CHATBAR_EMBED_WIDGET` | Tells the agent which embed layout context it's running in |
```html theme={null}
```
# Bring Your Own Identity (BYOI)
Source: https://docs.thesys.dev/agent-builder/byoi
Pass your own user identities into a published Thesys agent — each user gets isolated conversation history without needing a Thesys login.
## Overview
BYOI lets you identify **your** users inside a Thesys-published agent. Instead of requiring end-users to create a Thesys account, you sign a lightweight JWT on your backend and pass it to the embedded agent. Thesys verifies the signature and scopes every conversation to the user ID you provide.
```mermaid theme={null}
sequenceDiagram
participant User
participant YourBackend as Your Backend
participant Agent as Thesys Agent
User->>YourBackend: Logs in to your app
YourBackend->>YourBackend: Signs JWT with Identity Secret
YourBackend->>Agent: Passes JWT (via widget or iframe)
Agent->>Agent: Verifies JWT signature
Agent->>Agent: Scopes conversations to externalUserId
Agent-->>User: Personalized, isolated chat history
```
**What you get:**
* Per-user conversation history — no Thesys login required
* Full control over user identification
* Works with both the embed widget and direct iframe embedding
* Automatic token refresh when JWTs expire mid-session
***
## Quick Setup
Open your agent in [Agent Builder](https://console.thesys.dev), click **Share** to open the publish side panel, and find the **Bring Your Own Identity** section.
Click **Generate Secret** and copy the value. Store it securely on your backend — treat it like a password.
Never expose the Identity Secret in frontend code. It must only live on your server.
Create an **HS256-signed JWT** containing your user's ID. The token must include:
| Claim | Type | Required | Description |
| ---------------- | -------- | ----------- | ----------------------------------- |
| `externalUserId` | `string` | **Yes** | Your app's unique user identifier |
| `exp` | `number` | Recommended | Expiration timestamp (Unix seconds) |
```javascript Node.js theme={null}
const jwt = require('jsonwebtoken');
function generateIdentityToken(userId) {
return jwt.sign(
{ externalUserId: userId },
process.env.THESYS_IDENTITY_SECRET,
{ algorithm: 'HS256', expiresIn: '1h' }
);
}
// Express endpoint
app.get('/api/thesys-token', (req, res) => {
const token = generateIdentityToken(req.user.id);
res.json({ token });
});
```
```python Python theme={null}
import jwt, time, os
def generate_identity_token(user_id: str) -> str:
return jwt.encode(
{
"externalUserId": user_id,
"exp": int(time.time()) + 3600, # 1 hour
},
os.environ["THESYS_IDENTITY_SECRET"],
algorithm="HS256",
)
# Flask / FastAPI endpoint
@app.get("/api/thesys-token")
def get_token():
token = generate_identity_token(current_user.id)
return {"token": token}
```
Set a reasonable expiration (e.g. 1 hour). If you configure automatic token refresh (see below), expired tokens are silently renewed without interrupting the user.
Choose the integration method that fits your setup:
Install the embed widget:
```bash theme={null}
npm install agent-embed-widget
```
Pass `identityToken` for the initial token and `getIdentityToken` for automatic refresh on expiry:
```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
const { token } = await fetch('/api/thesys-token')
.then(r => r.json());
embedWidget({
url: 'https://console.thesys.dev/app/YOUR_SLUG',
identityToken: token,
getIdentityToken: async () => {
// Called automatically when the token expires
const res = await fetch('/api/thesys-token');
const { token } = await res.json();
return token;
},
hideLogin: true,
});
```
The widget handles the entire refresh cycle for you — when the JWT expires mid-conversation, it calls your `getIdentityToken` callback, gets a fresh token, and retries the failed request seamlessly.
The widget supports three display modes: `tray` (default), `full-screen`, and `chatbar`.
Append `IDENTITY_TOKEN` and `HIDE_LOGIN` as URL query parameters:
```html theme={null}
```
To set the token dynamically:
```javascript theme={null}
const token = await fetchTokenFromYourBackend();
const iframe = document.getElementById('thesys-agent');
iframe.src = `https://console.thesys.dev/app/YOUR_SLUG?IDENTITY_TOKEN=${token}&HIDE_LOGIN=true`;
```
**Automatic token refresh for iframes** requires a `postMessage` listener. See the [Token Refresh](#automatic-token-refresh) section below for the full setup.
***
## Automatic Token Refresh
When a JWT expires during an active conversation, the Thesys agent automatically requests a fresh token from the parent window using `postMessage`. If a new token is provided, the failed request is retried seamlessly — the user never sees an error.
### How it works
```mermaid theme={null}
sequenceDiagram
participant Agent as Thesys Agent (iframe)
participant Parent as Parent Window
participant Backend as Your Backend
Agent->>Agent: API call fails — token expired
Agent->>Parent: postMessage: REFRESH_NEEDED
Parent->>Backend: Fetch new token
Backend-->>Parent: Fresh JWT
Parent->>Agent: postMessage: REFRESHED {token}
Agent->>Agent: Updates token & retries request
Note over Agent: Conversation continues seamlessly
```
If the parent doesn't respond within 10 seconds (e.g. no listener configured), the agent falls back to showing an error modal asking the user to refresh the page.
### Embed Widget
If you're using the embed widget, token refresh is handled automatically — just provide the `getIdentityToken` callback as shown in [Step 3](#quick-setup) above. No additional setup needed.
### Direct iframe
For direct iframe embeds, add a `message` event listener on your parent page:
```javascript theme={null}
const iframe = document.getElementById('thesys-agent');
window.addEventListener('message', async (event) => {
if (event.data?.type === 'THESYS_IDENTITY_TOKEN_REFRESH_NEEDED') {
try {
const res = await fetch('/api/thesys-token');
const { token } = await res.json();
iframe.contentWindow.postMessage({
type: 'THESYS_IDENTITY_TOKEN_REFRESHED',
identityToken: token,
}, '*');
} catch (error) {
console.error('Failed to refresh identity token:', error);
}
}
});
```
### PostMessage Protocol
| Direction | Message Type | Payload |
| --------------- | -------------------------------------- | --------------------------- |
| iframe → parent | `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED` | *(none)* |
| parent → iframe | `THESYS_IDENTITY_TOKEN_REFRESHED` | `{ identityToken: string }` |
***
## How It Works Under the Hood
* Thesys verifies the JWT signature using your Identity Secret (HS256)
* The `externalUserId` claim is extracted and used to scope conversations
* Each unique `externalUserId` gets its own isolated conversation history
* If no identity token is provided, users are anonymous (session-based)
* If a user is logged into Thesys **and** has an identity token, the identity token takes priority
***
## Secret Rotation
If you need to rotate your Identity Secret:
1. Click **Rotate Secret** in the BYOI section of the publish panel
2. Copy the new secret to your backend environment
3. Redeploy your backend
Rotating the secret invalidates **all** previously signed tokens immediately. Users with old tokens will see an "Authentication Failed" error until they get a new token (via refresh or page reload).
***
## Token Errors
| Error | Cause | Resolution |
| --------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **Session Expired** | JWT `exp` has passed | Handled automatically if token refresh is configured; otherwise the user must refresh the page |
| **Authentication Failed** | Signature doesn't match (secret was rotated?) | Ensure your backend uses the current Identity Secret |
| **Invalid Identity Token** | Missing `externalUserId` claim | Add the required claim to your JWT payload |
| **Identity Not Configured** | No secret has been generated | Generate a secret in the publish panel |
***
## FAQ
No. The secret is synced to all published versions automatically.
No. Each agent has its own independent Identity Secret.
Only **HS256** (HMAC-SHA256). Other algorithms will be rejected.
No hard limit, but keep payloads small. Only `externalUserId` and `exp` are read by Thesys.
Yes, but Thesys only reads `externalUserId` and `exp`. Extra claims are ignored.
When the token expires mid-session, the agent shows an error modal. The user must refresh the page to continue. Configuring automatic refresh avoids this interruption entirely.
Yes. BYOI works with all three embed widget display modes: `tray`, `full-screen`, and `chatbar`.
# Customization
Source: https://docs.thesys.dev/agent-builder/embed-widget/customization
Theme the chatbar, set placeholders, and style the widget to match your brand.
## Theme
Set the overall color scheme with the `theme` option:
```javascript theme={null}
embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
theme: 'light', // 'dark' (default) or 'light'
});
```
This affects the floating button, loading screen, and the overall widget chrome. The agent content inside the iframe is styled independently through the Agent Builder style settings.
***
## Chatbar Theming
The chatbar layout supports fine-grained visual customization through the `options.theme` property. Each value maps to a CSS variable on the chatbar container.
```javascript theme={null}
embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
type: 'chatbar',
options: {
theme: {
containerFills: '#1a1a2e',
strokeDefault: '#333366',
roundedL: '24px',
primaryText: '#e0e0ff',
interactiveAccent: '#7c3aed',
accentPrimaryText: '#ffffff',
roundedS: '12px',
},
},
});
```
### Theme Properties
| Property | CSS Variable | Description |
| ------------------- | ------------------------------ | ------------------------------------------ |
| `containerFills` | `--crayon-container-fills` | Background color of the chatbar input area |
| `strokeDefault` | `--crayon-stroke-default` | Border color of the input area |
| `roundedL` | `--crayon-rounded-l` | Border radius of the entire chatbar |
| `primaryText` | `--crayon-primary-text` | Color of the input text |
| `interactiveAccent` | `--crayon-interactive-accent` | Background color of the send button |
| `accentPrimaryText` | `--crayon-accent-primary-text` | Icon/text color on the send button |
| `roundedS` | `--crayon-rounded-s` | Border radius of the send button |
All properties are optional. Unset properties use the defaults from the widget's `dark` or `light` theme.
To match your site's design exactly, inspect the chatbar element in DevTools and override additional CSS variables on the `.thesys--chatbar-widget` selector.
***
## Chatbar Options
Beyond theming, the chatbar accepts functional options:
```javascript theme={null}
embedWidget({
url: '...',
type: 'chatbar',
options: {
placeholder: 'Search our knowledge base...',
keyboardShortcutEnabled: false,
conversationStarters: [
'What features do you offer?',
'How does pricing work?',
],
},
});
```
| Option | Type | Default | Description |
| ------------------------- | ---------- | ------------------- | ------------------------------------------------------------ |
| `placeholder` | `string` | `"Ask me anything"` | Input placeholder text |
| `keyboardShortcutEnabled` | `boolean` | `true` | Allow **Space** to focus the input from anywhere on the page |
| `conversationStarters` | `string[]` | `[]` | Suggested prompts shown when the input is focused |
See [Widget Types — Chatbar](/agent-builder/embed-widget/widget-types#chatbar) for more details on chatbar behavior.
***
## Hiding the Login UI
If you're using [BYOI](/agent-builder/byoi) to identify users, you'll typically want to hide the built-in Thesys login interface:
```javascript theme={null}
embedWidget({
url: '...',
hideLogin: true,
identityToken: yourJwt,
});
```
***
## Preloading
By default, the agent iframe loads when the user first opens the widget. To eliminate that initial load delay, enable preloading:
```javascript theme={null}
// Preload immediately
const widget = embedWidget({
url: '...',
preload: true,
});
// Or preload later based on user behavior
widget.preload();
```
The iframe loads invisibly in the background so the agent is ready instantly when the user opens it.
# Events
Source: https://docs.thesys.dev/agent-builder/embed-widget/events
Listen to lifecycle events from the embedded agent — thread changes, message flow, tool execution, and errors.
The embed widget emits events at key points in the agent's lifecycle, letting you react to user actions, track conversation flow, and handle errors in your application.
There are two ways to listen to events: **inline callbacks** passed to `embedWidget()`, and the **`on()` method** on the widget instance.
**Data events carry payload only when the parent origin is allowlisted.** By default, events that contain conversation data (`generationEnded`, `userMessageSent`, `threadChanged`, etc.) still fire but their payload is stripped to just the event type. To receive the full payload — including the assistant's message body for live rendering — you must add your parent page's origin to the playground's **Allowed Parent Origins** list. See [Receiving Message Data](#receiving-message-data) below.
***
## Quick Start
```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
// Inline callbacks
onGenerationStarted: (threadId, messageId) => {
console.log('Agent is thinking…', { threadId, messageId });
},
onGenerationEnded: (threadId, messageId) => {
console.log('Agent replied', { threadId, messageId });
},
});
// Or subscribe dynamically with on()
const unsub = widget.on('userMessageSent', (message, threadId) => {
analytics.track('user_message', { message, threadId });
});
// Later: stop listening
unsub();
```
***
## Event Reference
### Thread Events
#### `threadChanged`
Fires when the active conversation thread changes — for example, when the user selects a different thread from the thread list, or a new thread is created after the first message is sent.
| Parameter | Type | Description |
| ---------- | -------- | --------------------------------- |
| `threadId` | `string` | The ID of the newly active thread |
```javascript theme={null}
widget.on('threadChanged', (threadId) => {
console.log('Switched to thread:', threadId);
});
```
#### `newThread`
Fires when the user clicks "New Chat" to start a fresh conversation. At this point no thread has been created on the backend yet — the thread is created once the first message is sent (which then fires `threadChanged`).
```javascript theme={null}
widget.on('newThread', () => {
console.log('User started a new chat');
});
```
***
### Message Events
#### `userMessageSent`
Fires when the user sends a message to the agent.
| Parameter | Type | Description |
| ---------- | -------- | ---------------------------------- |
| `message` | `string` | The message content |
| `threadId` | `string` | The thread the message was sent in |
```javascript theme={null}
widget.on('userMessageSent', (message, threadId) => {
console.log('User said:', message, 'in thread:', threadId);
});
```
#### `generationStarted`
Fires when the agent begins generating a response.
| Parameter | Type | Description |
| ----------- | -------- | ----------------------------------------------- |
| `threadId` | `string` | The thread the response is being generated in |
| `messageId` | `string` | The ID of the assistant message being generated |
```javascript theme={null}
widget.on('generationStarted', (threadId, messageId) => {
showTypingIndicator(threadId);
});
```
#### `generationEnded`
Fires when the agent finishes generating a response. The `messageId` is the persisted backend ID that you can use with the Thesys API to fetch the message content. When your parent origin is allowlisted, `message` contains the **full persisted assistant message** so you can render the response live in another panel without an additional API call.
| Parameter | Type | Description |
| ----------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `threadId` | `string \| undefined` | The thread the response was generated in |
| `messageId` | `string \| undefined` | The backend ID of the completed assistant message |
| `message` | `AssistantMessage \| undefined` | The full persisted assistant message item (id, role, content, status, conversation\_id, metadata, …) |
All three arguments are `undefined` unless the embedding page's origin is on the playground's **Allowed Parent Origins** list. See [Receiving Message Data](#receiving-message-data).
```javascript theme={null}
widget.on('generationEnded', (threadId, messageId, message) => {
hideTypingIndicator(threadId);
if (message) {
// Render the assistant's reply in a side panel
renderAssistantMessage(message);
}
});
```
**`AssistantMessage` shape**
```typescript theme={null}
interface AssistantMessage {
id: string;
role: 'assistant';
type: 'message';
content: Array>;
status?: Record;
conversation_id?: string;
created_at?: number;
metadata?: Record;
}
```
***
### Tool Execution Events
These events fire when the agent uses a tool (web search, image search, artifact builder, etc.) during response generation. Internal tools used by the system are excluded — only user-facing tools trigger these events.
#### `toolExecutionStarted`
Fires when the agent starts executing a tool.
| Parameter | Type | Description |
| ---------- | -------- | ----------------------------------- |
| `toolName` | `string` | The name of the tool being executed |
| `threadId` | `string` | The thread the tool is executing in |
```javascript theme={null}
widget.on('toolExecutionStarted', (toolName, threadId) => {
console.log(`Tool "${toolName}" started in thread ${threadId}`);
});
```
#### `toolExecutionEnded`
Fires when a tool finishes executing. If the tool failed, the `error` parameter contains the error message.
| Parameter | Type | Description |
| ---------- | --------------------- | -------------------------------------------------------- |
| `toolName` | `string` | The name of the tool that finished |
| `threadId` | `string` | The thread the tool executed in |
| `error` | `string \| undefined` | Error message if the tool failed, `undefined` on success |
```javascript theme={null}
widget.on('toolExecutionEnded', (toolName, threadId, error) => {
if (error) {
console.error(`Tool "${toolName}" failed:`, error);
} else {
console.log(`Tool "${toolName}" completed successfully`);
}
});
```
***
### Error Events
#### `agentError`
Fires when an error occurs during message processing or response streaming.
| Parameter | Type | Description |
| --------- | -------- | -------------------------------------------------------------------- |
| `code` | `string` | Error code (`MESSAGE_PROCESSING_ERROR` or `STREAM_PROCESSING_ERROR`) |
| `message` | `string` | Human-readable error description |
```javascript theme={null}
widget.on('agentError', (code, message) => {
errorReporter.capture({ code, message });
});
```
#### `identityTokenError`
Fires when a [BYOI](/agent-builder/byoi) identity token is invalid or cannot be refreshed. This is separate from the normal token refresh flow — it indicates a persistent authentication failure.
| Parameter | Type | Description |
| --------- | -------- | --------------------------------------- |
| `code` | `string` | Error code identifying the failure type |
| `message` | `string` | Human-readable error description |
```javascript theme={null}
widget.on('identityTokenError', (code, message) => {
console.error('Identity token error:', code, message);
// Redirect user to re-authenticate
window.location.href = '/login';
});
```
***
## Inline Callbacks
All events can also be passed as callbacks directly to `embedWidget()`. Callback names use the `on` prefix with PascalCase:
```javascript theme={null}
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
onThreadChanged: (threadId) => { /* ... */ },
onNewThread: () => { /* ... */ },
onUserMessageSent: (message, threadId) => { /* ... */ },
onGenerationStarted: (threadId, messageId) => { /* ... */ },
onGenerationEnded: (threadId, messageId, message) => { /* ... */ },
onAgentError: (code, message) => { /* ... */ },
onToolExecutionStarted: (toolName, threadId) => { /* ... */ },
onToolExecutionEnded: (toolName, threadId, error) => { /* ... */ },
onIdentityTokenError: (code, message) => { /* ... */ },
});
```
Both inline callbacks and `on()` listeners fire for the same event — you can use either or both.
***
## `widget.on(event, callback)`
Subscribe to events dynamically after the widget is created. Returns an **unsubscribe function**.
```javascript theme={null}
const unsubscribe = widget.on('generationEnded', (threadId, messageId) => {
console.log('Done:', messageId);
});
// Stop listening
unsubscribe();
```
Multiple listeners can be registered for the same event. Each call to `on()` returns its own independent unsubscribe function.
***
## Receiving Message Data
For security, the published agent does **not** broadcast conversation data (assistant messages, thread IDs, user messages, tool names, error details) to arbitrary embedders. Without explicit opt-in, every data event still fires — but with its payload stripped to just the event type:
```javascript theme={null}
// Default behavior (no allowlisted origins): all data events fire as
// { type: 'THESYS_GENERATION_ENDED' }
// { type: 'THESYS_USER_MESSAGE_SENT' }
// ...
// All payload fields are undefined in your callbacks.
```
To receive the full payload — including the assistant `message` for live rendering — add your parent page's origin to the playground's **Allowed Parent Origins** list:
1. Open your playground in the [Thesys Console](https://console.thesys.dev/).
2. Click **Deploy**, expand the **Embed on your website** section.
3. Under **Allowed parent origins**, add each origin that should receive full event payloads (for example, `https://app.example.com` or `http://localhost:3000`).
4. Republish the playground.
Once allowlisted, your origin will receive the full payload for every data event. Origins not on the list will continue to receive stripped events. The handshake events (`APP_READY`, widget open/close/toggle, identity-token refresh) are unaffected and always work.
### What counts as a "data event"?
| Event | Class | Notes |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------- |
| `threadChanged`, `newThread`, `userMessageSent`, `generationStarted`, `generationEnded`, `agentError`, `toolExecutionStarted`, `toolExecutionEnded`, `identityTokenError` | **Data** | Payload only delivered to allowlisted origins |
| `THESYS_APP_READY`, `THESYS_WIDGET_OPEN/CLOSE/TOGGLE`, `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED` | **Protocol** | Required for widget functionality, always delivered with full payload |
Origins must be exact matches: `scheme://host[:port]` with no path or trailing slash. `https://app.example.com` and `https://app.example.com:443` are treated as different origins.
***
## Typical Event Flow
Here's the sequence of events during a normal conversation turn:
```
newThread // User clicks "New Chat"
→ userMessageSent // User sends first message
→ threadChanged (threadId) // Backend creates the thread
→ generationStarted // Agent begins responding
→ toolExecutionStarted // (if the agent uses a tool)
→ toolExecutionEnded //
→ generationEnded // Agent finishes responding
```
For subsequent messages in the same thread:
```
userMessageSent
→ generationStarted
→ generationEnded
```
***
## Direct Iframe Integration
If you're embedding the agent via a **direct iframe** instead of the embed widget npm package, you can listen for the same events using the browser's `postMessage` API. Each event is sent as a message with a `type` field prefixed with `THESYS_`.
For full details on `postMessage` usage, origin validation, and URL parameters, see the [PostMessage Protocol](/agent-builder/advanced/postmessage-protocol) page.
### Event Mapping
| Embed Widget Event | postMessage `type` | Payload |
| ---------------------- | -------------------------------------- | -------------------------------------------------------------------- |
| `threadChanged` | `THESYS_THREAD_CHANGED` | `{ threadId: string }` |
| `newThread` | `THESYS_NEW_THREAD` | *(none)* |
| `userMessageSent` | `THESYS_USER_MESSAGE_SENT` | `{ message: string, threadId: string }` |
| `generationStarted` | `THESYS_GENERATION_STARTED` | `{ threadId: string, messageId: string }` |
| `generationEnded` | `THESYS_GENERATION_ENDED` | `{ threadId: string, messageId: string, message: AssistantMessage }` |
| `toolExecutionStarted` | `THESYS_TOOL_EXECUTION_STARTED` | `{ toolName: string, threadId: string }` |
| `toolExecutionEnded` | `THESYS_TOOL_EXECUTION_ENDED` | `{ toolName: string, threadId: string, error?: string }` |
| `agentError` | `THESYS_AGENT_ERROR` | `{ code: string, message: string }` |
| `identityTokenError` | `THESYS_IDENTITY_TOKEN_ERROR` | `{ code: string, message: string }` |
| *(widget-only)* | `THESYS_APP_READY` | *(none)* — the agent has finished loading |
| *(widget-only)* | `THESYS_WIDGET_CLOSE` | *(none)* — the agent requested to close |
| *(widget-only)* | `THESYS_WIDGET_OPEN` | *(none)* — the agent requested to open |
| *(widget-only)* | `THESYS_WIDGET_TOGGLE` | *(none)* — the agent requested to toggle visibility |
| *(widget-only)* | `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED` | *(none)* — token expired, parent should refresh |
| *(parent → iframe)* | `THESYS_RESET_THREAD` | *(none)* — start a new conversation thread |
| *(parent → iframe)* | `THESYS_TOGGLE_SIDEBAR` | *(none)* — open or close the conversation history sidebar |
### Example
```javascript theme={null}
const iframe = document.getElementById('thesys-agent');
const expectedOrigin = new URL(iframe.src).origin;
window.addEventListener('message', (event) => {
if (event.origin !== expectedOrigin) return;
if (!event.data?.type) return;
switch (event.data.type) {
case 'THESYS_APP_READY':
console.log('Agent loaded');
break;
case 'THESYS_THREAD_CHANGED':
console.log('Thread:', event.data.threadId);
break;
case 'THESYS_NEW_THREAD':
console.log('New chat started');
break;
case 'THESYS_USER_MESSAGE_SENT':
console.log('User said:', event.data.message);
break;
case 'THESYS_GENERATION_STARTED':
console.log('Generating…', event.data.messageId);
break;
case 'THESYS_GENERATION_ENDED':
console.log('Done:', event.data.messageId);
// event.data.message is the full assistant message — only present
// when this origin is on the playground's allowed parent origins list.
if (event.data.message) {
renderAssistantMessage(event.data.message);
}
break;
case 'THESYS_TOOL_EXECUTION_STARTED':
console.log('Tool started:', event.data.toolName);
break;
case 'THESYS_TOOL_EXECUTION_ENDED':
console.log('Tool ended:', event.data.toolName, event.data.error);
break;
case 'THESYS_AGENT_ERROR':
console.error('Error:', event.data.code, event.data.message);
break;
case 'THESYS_IDENTITY_TOKEN_ERROR':
console.error('Token error:', event.data.code, event.data.message);
break;
case 'THESYS_IDENTITY_TOKEN_REFRESH_NEEDED':
// Fetch a new token and send THESYS_IDENTITY_TOKEN_REFRESHED back
break;
}
});
```
***
## Example: Analytics Integration
```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
});
widget.on('userMessageSent', (message, threadId) => {
analytics.track('agent_message_sent', {
threadId,
messageLength: message.length,
});
});
widget.on('generationEnded', (threadId, messageId, message) => {
analytics.track('agent_response_received', {
threadId,
messageId,
contentLength: message?.content?.length,
});
});
widget.on('toolExecutionStarted', (toolName, threadId) => {
analytics.track('agent_tool_used', { toolName, threadId });
});
widget.on('agentError', (code, message) => {
analytics.track('agent_error', { code, message });
});
```
# Getting Started
Source: https://docs.thesys.dev/agent-builder/embed-widget/getting-started
Install the embed widget and add a Thesys agent to your site in under five minutes.
## Installation
```bash npm theme={null}
npm install agent-embed-widget
```
```bash yarn theme={null}
yarn add agent-embed-widget
```
```bash pnpm theme={null}
pnpm add agent-embed-widget
```
The widget requires **React 18+** or **React 19+** as a peer dependency. If your project doesn't use React, use the [UMD / CDN](#umd--cdn) approach below.
***
## Quick Start
### ES Module
```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
theme: 'dark',
});
```
That's it — a floating chat button appears in the bottom-right corner. Click it to open the agent.
### CommonJS
```javascript theme={null}
const { embedWidget } = require('agent-embed-widget');
require('agent-embed-widget/dist/agent-embed-widget.css');
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
theme: 'dark',
});
```
### UMD / CDN
If you're not using a bundler, load the widget from a CDN:
```html theme={null}
```
***
## Options
| Option | Type | Default | Description |
| ------------------ | -------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `url` | `string` | — | **Required.** The published agent URL |
| `type` | `'tray'` \| `'full-screen'` \| `'chatbar'` | `'tray'` | Widget layout. See [Widget Types](/agent-builder/embed-widget/widget-types) |
| `theme` | `'dark'` \| `'light'` | `'dark'` | Color theme |
| `hideLogin` | `boolean` | `false` | Hide the Thesys login UI inside the agent |
| `identityToken` | `string` | — | Signed HS256 JWT for [BYOI](/agent-builder/byoi) user identity |
| `getIdentityToken` | `() => Promise` | — | Callback for automatic token refresh on expiry |
| `preload` | `boolean` | `false` | Load the agent iframe in the background immediately |
| `options` | `TrayOptions` \| `FullScreenOptions` \| `ChatbarOptions` | — | Type-specific options. See [Customization](/agent-builder/embed-widget/customization) |
***
## Widget Instance
`embedWidget()` returns a `WidgetInstance` with methods for programmatic control:
```javascript theme={null}
const widget = embedWidget({ url: '...', theme: 'dark' });
widget.open(); // Show the widget
widget.close(); // Hide the widget
widget.sendMessage('Hello!'); // Send a message
widget.sendMessage('Start over', { newThread: true }); // New thread
widget.setInput('Draft for the user to review'); // Prefill input
widget.preload(); // Start loading early
widget.destroy(); // Remove from DOM
```
See [Programmatic Control](/agent-builder/embed-widget/programmatic-control) for detailed usage and examples.
***
## Direct iframe (Alternative)
If you prefer not to use the npm package, embed the agent directly with an iframe:
```html theme={null}
```
The embed widget is recommended over a raw iframe because it handles widget chrome, responsive layout, programmatic control, and automatic identity token refresh out of the box.
***
## Next Steps
* [**Widget Types**](/agent-builder/embed-widget/widget-types) — Choose the right layout for your use case
* [**Customization**](/agent-builder/embed-widget/customization) — Theme the chatbar and configure options
* [**User Identity (BYOI)**](/agent-builder/byoi) — Identify users and isolate their conversations
# Mobile Behavior
Source: https://docs.thesys.dev/agent-builder/embed-widget/mobile-behavior
How the embed widget adapts to mobile devices and small viewports.
The embed widget automatically adapts its layout on mobile devices (viewport width under 768px). No configuration is needed — the responsive behavior is built in.
***
## Tray on Mobile
On desktop, the tray opens a compact 583 × 800px panel. On mobile, it switches to a **full-screen overlay** for better usability on small screens.
| | Desktop | Mobile |
| -------------- | ------------------------------ | ------------------------------ |
| **Trigger** | Floating button (bottom-right) | Floating button (bottom-right) |
| **Open state** | Compact panel | Full-screen overlay |
| **Close** | Click outside or close button | Close button |
***
## Full-screen on Mobile
No change — full-screen is already the same layout on both desktop and mobile.
***
## Chatbar on Mobile
The chatbar adapts its layout while keeping the inline input bar:
| | Desktop | Mobile |
| --------------- | ------------------------------ | ------------------------------------- |
| **Input bar** | Fixed at bottom center | Fixed at bottom center |
| **Chat window** | Floating window above input | Full-screen overlay with close handle |
| **Close** | Pill close button above window | Drag handle at top of overlay |
When the chat window is open on mobile, the widget container expands to fill the entire screen. When closed, it shrinks back to just the input bar.
***
## Detection
Mobile detection uses `window.innerWidth < 768`. This check runs when the widget is first created. If the user resizes the browser window after creation, the chatbar re-evaluates on open/close transitions.
The 768px breakpoint is not currently configurable. If you need different behavior at a specific breakpoint, you can conditionally set the `type` option based on your own viewport check before calling `embedWidget()`.
# Programmatic Control
Source: https://docs.thesys.dev/agent-builder/embed-widget/programmatic-control
Open, close, send messages, and prefill input on the embedded agent from your application code.
`embedWidget()` returns a `WidgetInstance` with methods to control the agent programmatically. This lets you trigger conversations from buttons, links, application events, or any other logic in your app.
```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
theme: 'dark',
});
```
***
## Methods
### `widget.open()`
Opens the widget and makes it visible.
```javascript theme={null}
widget.open();
```
### `widget.close()`
Closes the widget. The agent stays loaded in the background — reopening is instant.
```javascript theme={null}
widget.close();
```
### `widget.destroy()`
Removes the widget from the DOM entirely and cleans up all resources. After calling `destroy()`, the instance cannot be reopened.
```javascript theme={null}
widget.destroy();
```
### `widget.sendMessage(message, options?)`
Sends a user message to the agent. If the widget is closed, it opens automatically.
```javascript theme={null}
widget.sendMessage('Tell me about pricing');
widget.sendMessage('Start a new topic', { newThread: true });
```
| Parameter | Type | Description |
| ------------------- | --------- | ---------------------------------------------------------------------------- |
| `message` | `string` | The message text to send |
| `options.newThread` | `boolean` | If `true`, starts a new conversation thread before sending. Default: `false` |
### `widget.setInput(message, options?)`
Prefills the chat input bar **without** sending. The widget opens (if closed) and the user can review, edit, and submit the message themselves.
```javascript theme={null}
widget.setInput('Can you help me understand my recent invoice?');
widget.setInput('New question for a fresh thread', { newThread: true });
```
| Parameter | Type | Description |
| ------------------- | --------- | ------------------------------------------------------------------------------- |
| `message` | `string` | The message text to prefill |
| `options.newThread` | `boolean` | If `true`, starts a new conversation thread before prefilling. Default: `false` |
### `widget.preload()`
Starts loading the agent iframe in the background so it's ready instantly when the user opens the widget. Useful when you didn't set `preload: true` in the initial options but want to trigger it based on user behavior.
```javascript theme={null}
widget.preload();
```
***
## Examples
### Trigger from buttons
```html theme={null}
```
### Preload on scroll
Load the agent in the background as the user scrolls near the bottom of the page:
```javascript theme={null}
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
});
window.addEventListener('scroll', () => {
if (window.scrollY > document.body.scrollHeight - 1000) {
widget.preload();
}
}, { once: true });
```
### Open after a delay
```javascript theme={null}
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
});
setTimeout(() => widget.open(), 5000);
```
### Clean up on navigation (SPA)
In a single-page application, destroy the widget when the user navigates away from the page:
```javascript theme={null}
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
});
// React useEffect cleanup, Vue onUnmounted, etc.
return () => widget.destroy();
```
***
## Pending Messages
If you call `sendMessage()` or `setInput()` before the agent iframe has finished loading, the action is queued and executed automatically once the iframe is ready. You don't need to wait for a "loaded" event.
# Widget Types
Source: https://docs.thesys.dev/agent-builder/embed-widget/widget-types
Choose between tray, full-screen, and chatbar layouts for your embedded agent.
The embed widget ships with three layout types. Each is optimized for a different use case. Set the type when calling `embedWidget()`:
```javascript theme={null}
const widget = embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
type: 'tray', // 'tray' | 'full-screen' | 'chatbar'
});
```
***
## Tray
```javascript theme={null}
embedWidget({ url: '...', type: 'tray' });
```
The default layout. A floating circular button appears in the **bottom-right corner**. Clicking it opens a compact chat panel (583 × 800px, capped at `100vh - 100px`).
**Best for:** Help widgets, customer support, contextual assistants that shouldn't dominate the page.
On mobile devices (viewport under 768px), the tray automatically switches to a full-screen layout for better usability. See [Mobile Behavior](/agent-builder/embed-widget/mobile-behavior).
***
## Full-screen
```javascript theme={null}
embedWidget({ url: '...', type: 'full-screen' });
```
Same floating button as the tray, but clicking it opens the agent as a **full-screen overlay**. The agent fills the entire viewport.
**Best for:** Dedicated AI assistant pages, onboarding flows, or experiences where you want the agent to be the primary focus.
***
## Chatbar
```javascript theme={null}
embedWidget({ url: '...', type: 'chatbar' });
```
An **inline input bar** fixed to the bottom center of the page — no floating button. The user types directly into the bar, and a chat window opens above it.
**Best for:** Search-like experiences, landing pages, AI-native products where the agent is the main interaction point.
### Chatbar-Specific Options
The chatbar type accepts additional configuration through the `options` property:
```javascript theme={null}
embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
type: 'chatbar',
options: {
placeholder: 'Ask me anything about our product...',
keyboardShortcutEnabled: true,
conversationStarters: [
'What can you help me with?',
'Show me pricing plans',
'How do I get started?',
],
},
});
```
| Option | Type | Default | Description |
| ------------------------- | -------------- | ------------------- | ------------------------------------------------------------------------------------------------ |
| `placeholder` | `string` | `"Ask me anything"` | Placeholder text in the input field |
| `keyboardShortcutEnabled` | `boolean` | `true` | Press **Space** anywhere on the page to focus the input (only when no other input is focused) |
| `conversationStarters` | `string[]` | `[]` | Suggested prompts displayed above the input when focused |
| `theme` | `ChatbarTheme` | — | Visual overrides for the chatbar. See [Customization](/agent-builder/embed-widget/customization) |
### Keyboard Shortcuts
When `keyboardShortcutEnabled` is `true` (the default):
* **Space** — Focuses the chatbar input (only when no other text field is active)
* **Escape** — Blurs the chatbar input
### Conversation Starters
When the input is focused and the chat window is closed, the starters appear as clickable chips labeled **A**, **B**, **C**, etc. Clicking one sends it as the first message and opens the chat window.
***
## Choosing a Type
| Criteria | Tray | Full-screen | Chatbar |
| ---------------- | --------------- | ---------------- | ------------------------------------ |
| Presence on page | Floating button | Floating button | Inline input bar |
| Space when open | Compact panel | Full viewport | Window above input |
| Best for | Help / support | Focused AI tasks | Search / main UX |
| Mobile behavior | Full-screen | Full-screen | Input stays; window goes full-screen |
# Agent Builder
Source: https://docs.thesys.dev/agent-builder/introduction
Build, publish, and embed AI agents — then integrate them into any website or app.
## What is Agent Builder?
Agent Builder is the Thesys platform for creating and publishing AI agents. You configure your agent — model, system prompt, knowledge sources, tools, and styling — through a visual editor at [console.thesys.dev](https://console.thesys.dev), then publish it as a shareable URL.
Once published, you embed the agent into your own product using either the **Embed Widget** npm package or a direct **iframe**. Your users interact with the agent without ever leaving your site.
## Key Capabilities
Drop a fully featured chat widget into any website with a few lines of code. Supports tray, full-screen, and chatbar layouts.
Identify your own users with a signed JWT so each person gets isolated conversation history — no Thesys login required.
Open, close, send messages, and prefill input programmatically. Trigger the agent from buttons, links, or application events.
Theme the chatbar to match your brand with CSS variable overrides, custom placeholders, and conversation starters.
## How It Works
Go to [Agent Builder](https://console.thesys.dev) and create a new project. Configure the model, system prompt, knowledge sources, and tools.
Click **Share** in the editor toolbar. Your agent gets a unique URL like `https://console.thesys.dev/app/your-slug`.
Install the embed widget and add it to your site:
```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';
embedWidget({
url: 'https://console.thesys.dev/app/your-slug',
});
```
## Next Steps
* [**Getting Started**](/agent-builder/embed-widget/getting-started) — Install the widget and embed your first agent
* [**Widget Types**](/agent-builder/embed-widget/widget-types) — Choose between tray, full-screen, and chatbar layouts
* [**User Identity (BYOI)**](/agent-builder/byoi) — Give each user their own conversation history
# Embed
Source: https://docs.thesys.dev/api-reference/endpoints/embed
POST /v1/embed/chat/completions
A LLM native endpoint that generates UI.
POST `https://api.thesys.dev/v1/embed/chat/completions`
* Drop in replacement for existing LLM endpoint that can be used
to generate UI rather than text completions.
* Supports tool calling for fetching external data.
* Ability to steer semantic design via system prompts.
* Visual customization via [Crayon](https://crayonai.org)
## Request
Supports both [streaming](/api-reference/objects/streaming#request-body) and [non-streaming](/api-reference/objects/message#request-body) payloads.
## Response
Returns [streaming chunks](/api-reference/objects/streaming#sse-event-stream) in streaming mode
or a [message](/api-reference/objects/message#response) object in non-streaming mode.
# Visualize
Source: https://docs.thesys.dev/api-reference/endpoints/visualize
POST /v1/visualize/chat/completions
Generates a UI for the given data.
* Given a data object, generates the best UI representation for it.
* DOES NOT support tool calling.
* Ability to steer semantic design via system prompts.
* Visual customization via [Crayon](https://crayonai.org)
Learn how to integrate C1 Visualize with your existing LLM infrastructure in our [Two-Step Visualize Pattern guide](/guides/integration-pattern/visualize).
## Request
Supports both [streaming](/api-reference/objects/streaming#request-body) and [non-streaming](/api-reference/objects/message#request-body) payloads.
* Accepts data via the `messages` property.
* Does not accept the `tools` property.
## Response
Returns [streaming chunks](/api-reference/objects/streaming#sse-event-stream) in streaming mode
or a [message](/api-reference/objects/message#response) object in non-streaming mode.
# Errors
Source: https://docs.thesys.dev/api-reference/errors
## HTTP Errors
The following table shows the HTTP errors that can be returned by the C1 API:
* 400 `Bad Request`: The request payload has errors.
* 403 `Forbidden`: The request is missing a valid API key.
* 404 `Not Found`: Likely a result of invalid path.
* 413 `Request Entity Too Large`: Request exceeds the maximum size allowed.
* 429 `Too Many Requests`: Rate limit exceeded.
* 500 `Internal Server Error`: An unexpected error occurred on our side.
## Generated UI Errors
In addition to HTTP errors, the C1 API may return errors within the generated response.
This is handled by the UI components automatically and you do not need to handle them manually.
# API Overview
Source: https://docs.thesys.dev/api-reference/getting-started
## Accessing the API
All API endpoints are available at `https://api.thesys.dev/v1`.
## Authentication
Authentication is done via API keys. You can create API keys from the [keys page](https://console.thesys.dev/keys).
API keys must be sent as a `Authorization` header with the value `Bearer `.
If the API key is not valid, you will receive a 403 error.
## Rate Limits
Please refer to the [Rate Limits](/api-reference/rate-limits) page for more information.
## Errors
Please refer to the [Errors](/api-reference/errors) page for more information.
## Examples
### For Chat Completions API
```python python theme={null}
from openai import OpenAI
client = OpenAI(
base_url="https://api.thesys.dev/v1/embed",
api_key=""
)
completion = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
messages=[
{
"role": "user",
"content": "How did the population of the world grow from 1950 to 2020?"
}
])
```
```js node theme={null}
import OpenAI from "openai";
const client = new OpenAI({
base_url="https://api.thesys.dev/v1/embed",
api_key=""
});
const completion = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
messages: [
{
role: "user",
content: "How did the population of the world grow from 1950 to 2020?",
},
],
});
```
Stream this response via the language specific helper functions to the react
client.
### For Responses API
```python python theme={null}
from openai import OpenAI
client = OpenAI(
base_url="https://api.thesys.dev/v1/embed",
api_key=""
)
response = client.responses.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
input="How did the population of the world grow from 1950 to 2020?"
)
print(response)
```
```js node theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: ""
});
const response = await client.responses.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
input: "How did the population of the world grow from 1950 to 2020?",
});
console.log(response);
```
## Chat Completions API
For C1 Gemini models, the `reasoning_effort` parameter translates to the following max reasoning tokens:
| `reasoning_effort` | 3 Flash - Max reasoning tks | 3 Pro - Max reasoning tks |
| ------------------ | --------------------------- | ------------------------- |
| `none` | 0 | 256 |
| `minimal` | 512 | 512 |
| `low` | 1024 | 1024 |
| `medium` | 2048 | 2048 |
| `high` | 4096 | 4096 |
| `xhigh` | 8192 | 8192 |
| Property | C1 OpenAI models | C1 Anthropic Models |
| ----------------------- | ------------------------------------------- | ------------------------------------------- |
| `messages` | (audio not supported) | (audio not supported) |
| `model` | | |
| `audio` | | |
| `frequency_penalty` | | |
| `function_call` | use `tool_choice` | use `tool_choice` |
| `functions` | use `tools` | use `tools` |
| `logit_bias` | | |
| `logprobs` | | |
| `max_completion_tokens` | | |
| `max_tokens` | | |
| `metadata` | `thesys` metadata only | `thesys` metadata only |
| `modalities` | | |
| `n` | must be `1` | must be `1` |
| `parallel_tool_calls` | `true` by default | `true` by default |
| `prediction` | | |
| `presence_penalty` | | |
| `prompt_cache_key` | | |
| `reasoning_effort` | | |
| `response_format` | | |
| `service_tier` | | |
| `stop` | | |
| `stream` | | |
| `temperature` | | |
| `tool_choice` | | |
| `tools` | | |
| `top_p` | | |
| `verbosity` | | |
| `web_search_options` | | |
## Responses API
All C1 models are supported via the Responses API, including OpenAI and
non-OpenAI models. Our Responses API implementation is [Open
Responses](https://www.openresponses.org/) compliant.
There are three ways to maintain chat history in Responses API:
1. **Pass the full conversation history** — Include all previous messages in the `input` array with each request.
2. **Use `previous_response_id`** — Reference a prior response by its ID to automatically chain conversations (requires `store: true`).
3. **Use `conversation`** — Group related responses into a named conversation for persistent multi-turn context (requires `store: true`).
To create a conversation using `previous_response_id`:
```python python theme={null}
from openai import OpenAI
client = OpenAI(
base_url="https://api.thesys.dev/v1/embed",
api_key=""
)
response = client.responses.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
input="What is the capital of France?"
)
follow_up = client.responses.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
input="And what is its population?",
previous_response_id=response.id
)
print(follow_up)
```
```js node theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: ""
});
const response = await client.responses.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
input: "What is the capital of France?",
});
const followUp = await client.responses.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
input: "And what is its population?",
previous_response_id: response.id,
});
console.log(followUp);
```
To create a conversation and use it across multiple requests:
```python python theme={null}
from openai import OpenAI
# Client for conversation management
conv_client = OpenAI(
base_url="https://api.thesys.dev",
api_key=""
)
# Client for generation
embed_client = OpenAI(
base_url="https://api.thesys.dev/v1/embed",
api_key=""
)
conversation = conv_client.conversations.create()
response = embed_client.responses.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
input="What is the capital of France?",
store=True,
conversation={"id": conversation.id}
)
follow_up = embed_client.responses.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
input="And what is its population?",
store=True,
conversation={"id": conversation.id}
)
print(follow_up)
```
```js node theme={null}
import OpenAI from "openai";
// Client for conversation management
const convClient = new OpenAI({
baseURL: "https://api.thesys.dev",
apiKey: ""
});
// Client for generation
const embedClient = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: ""
});
const conversation = await convClient.conversations.create();
const response = await embedClient.responses.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
input: "What is the capital of France?",
store: true,
conversation: { id: conversation.id },
});
const followUp = await embedClient.responses.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
input: "And what is its population?",
store: true,
conversation: { id: conversation.id },
});
console.log(followUp);
```
Built-in tools (`web_search`, `file_search`, `code_interpreter`,
`computer_use`, `mcp`) will be supported soon.
| Property | Supported |
| ------------------------ | ----------------------------------------------------------------- |
| `background` | Always `false` |
| `conversation` | with `store: true` |
| `include` | |
| `input` | |
| `instructions` | |
| `max_output_tokens` | |
| `max_tool_calls` | |
| `metadata` | `thesys` metadata only |
| `model` | |
| `parallel_tool_calls` | `true` by default |
| `previous_response_id` | with `store: true` |
| `prompt` | |
| `prompt_cache_key` | |
| `prompt_cache_retention` | |
| `reasoning` | `effort` only |
| `safety_identifier` | |
| `service_tier` | |
| `store` | `true` by default |
| `stream` | |
| `temperature` | |
| `text` | `verbosity` only |
| `tool_choice` | |
| `tools` | `function` only; built-in tools coming soon |
| `top_logprobs` | |
| `top_p` | |
### `input` item types
| Input Item Type | Supported |
| -------------------------------------------------------------------------------- | --------------------------------- |
| `message` (user/system/developer/assistant) | |
| `function_call` | |
| `function_call_output` | |
| `item_reference` | |
| `reasoning` | |
| `compaction` | coming soon |
| `web_search_call` | coming soon |
| `file_search_call` | coming soon |
| `computer_call` / `computer_call_output` | coming soon |
| `code_interpreter_call` | coming soon |
| `image_generation_call` | coming soon |
| `local_shell_call` / `local_shell_call_output` | |
| `shell_call` / `shell_call_output` | |
| `apply_patch_call` / `apply_patch_call_output` | |
| `mcp_list_tools` / `mcp_approval_request` / `mcp_approval_response` / `mcp_call` | coming soon |
| `custom_tool_call` / `custom_tool_call_output` | |
### `text` sub-fields
| Property | Supported |
| ---------------- | ------------------------------------------ |
| `text.verbosity` | |
| `text.format` | Always Thesys format |
# API Changelog
Source: https://docs.thesys.dev/api-reference/model-changelog
View changelogs for each API version of C1.
Embed model versions:
* `c1///v-20260331`
For example, `c1/anthropic/claude-sonnet-4.6/v-20260331`
Minimum SDK version:
* @thesysai/genui-sdk: `0.9.0`
* @crayonai/react-ui: `0.9.16`
* @crayonai/react-core: `0.7.7`
Upgrading to `v-20260331` is a **backwards incompatible** change due to a new OpenUI response format and component-level changes.
**Existing threads are not affected.** Threads started on `v-20251230` or earlier will continue to function on those versions even if you change the model. The new API version is automatically routed to `v-20251230` for old threads. OpenUI responses are only supported for new threads created with `v-20260331` or later.
## OpenUI Support
C1 now supports the [OpenUI Framework](https://www.openui.com/) - upto 3x faster generations compared to the JSON implementation, with upto 66% less token consumption.
## New Components
This release brings in a whole set of new components. The entire library has also ben shifted to OpenUI CSS from Crayon CSS.
A full list of all the new components and changes will be documented soon, with guides on CSS migration and generation prompts.
## Custom Component Migration
The custom component declaration, schema declaration, and passing to the API stays the same. The only change is an additional step of passing the
component schema to the GenUI SDK. Basing the instructions off the [existing guide](/guides/custom-components), the change is as follows:
### `v-20260331` version onwards
Previous version [here](/guides/custom-components). Steps 1, 2, 3 stay the same.
`zod` moves from version `3` to version `4`, i.e. `zodToJsonSchema` is now `z.toJSONSchema`.
```ts src/app/api/chat/route.tsx theme={null}
const CUSTOM_COMPONENT_SCHEMAS = {
FlightList: zodToJsonSchema(FlightListSchema), // [!code --]
FlightList: z.toJSONSchema(FlightListSchema), // [!code ++]
};
```
```ts src/app/page.tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import { FlightList } from "./components";
// ...
export default function Home() {
return (
);
}
```
Artifact model version:
* `c1/artifact/v-20260130`
Minimum SDK version:
* @thesysai/genui-sdk: `0.8.3`
* @crayonai/react-ui: `0.9.16`
## New Features
* **Welcome Screen** - The `C1Chat` component now supports a configurable welcome screen displayed when a thread is empty. You can customize the title, description, and image to greet users when they start a new conversation. See the [`welcomeMessage` prop](/react-reference/c1-chat#props) for details.
* **Conversation Starters** - The `C1Chat` component now supports conversation starters — clickable prompt buttons shown on the welcome screen to help users quickly begin a conversation with predefined options. Supports both `short` (pill) and `long` (list) variants. See the [`conversationStarters` prop](/react-reference/c1-chat#props) for details.
* **Artifact Editing** - Users can now edit artifacts inline directly within the chat interface, enabling a more interactive workflow. For setup instructions, see [Editing Artifacts](/guides/artifacts/editing).
* **Pinch Zoom in Reports** - Reports now support pinch-to-zoom functionality, making it easier to inspect details in generated reports.
* **New Slide Templates** - Added new slide templates, giving more design options for generated slide presentations. See the full [Slide Templates (v-20260130)](/guides/artifacts/slides/slide-templates/v-20260130) reference for all available templates and parameters.
* **PPTX Export for Slides** - Slide artifacts can now be exported as `.pptx` files, enabling seamless use in PowerPoint and other presentation tools. See the [Exporting Slides to PPTX](/guides/artifacts/pptx-export) guide for setup instructions.
* **New Report Blocks** - Added new block types for reports, expanding the variety of content that can be generated within report artifacts.
Stable embed model versions:
* `c1/anthropic/claude-sonnet-4/v-20251230`
* `c1/openai/gpt-5.2/v-20251230`
* `c1/openai/gpt-5/v-20251230`
Experimental embed model versions:
* `c1-exp/anthropic/claude-sonnet-4.5/v-20251230`
* `c1-exp/anthropic/claude-haiku-4.5/v-20251230`
Visualise model version:
* `c1/anthropic/claude-sonnet-4/v-20251230`
Artifact model version:
* `c1/artifact/v-20251230`
Minimum SDK version:
* @crayonai/react-ui: `0.9.9`
* @thesysai/genui-sdk: `0.7.15`
## New Model Support
C1 now supports `Claude Sonnet 4.5` and `Claude Haiku 4.5` models. These are enabled via the C1 API as **experimental** models. Production use of these models is not encouraged.
## New Features
* **Tray Widget Form Factor** - The `C1Chat` component now supports a new `bottom-tray` form factor for tray widget layouts. This enables a variety of use cases, including widget-based chatbot interactions and minimizable chat interfaces.
* **Mermaid Diagram Generation** - C1 now supports generating mermaid diagrams. This is **not** enabled by default. To use mermaid generation, users should enter a supporting system prompt:
```
You can generate mermaid diagram code in TextContent.
```
* **Enhanced Components**
* **Tag variants** - C1 tags now support standard color variants: `neutral`, `info`, `success`, `warning`, `danger`. These variants can be used to suppliment semantic information with colors, and can optionally be guided via system prompts.
* **Image improvements** - We improved the way images are displayed, especially in wide layouts. This ensures all images are rendered with balanced aspect ratios.
* **C1 Artifact icon generation** - We have significantly improved our icon generation process. Embed models already support this, and support has been extended to the C1 Artifact model as well.
Model versions:
* `c1/anthropic/claude-sonnet-4/v-20251130`
* `c1/openai/gpt-5/v-20251130`
Minimum SDK version:
* @crayonai/react-ui: `0.9.6`
* @thesysai/genui-sdk: `0.7.5`
## New Features
* **Citations and Source Links** - C1 can now include citations and links to the original sources for the facts and data it presents. This adds credibility to the data and allows the user to verify the information.
* **Enhanced Interactivity**
* **Action Column for Tables** - C1 can now generate a dedicated action column in tables, with buttons and actions that are specific to each row. This makes it easier than ever for users to act on data directly where they see it, from viewing details to deleting an entry.
* **Expanded Support for Actions** - C1 can now attach continue conversation, custom actions and open\_url actions to Icon Buttons and List Items. This provides more design flexibility to place actions in more intuitive, contextual locations within your UI.
* **Carousel Footer for Actions** - Carousel items can now have a footer for action buttons. The buttons are aligned across the bottom of the carousel item.
* **'Explore' Button** - C1 can now add an optional "explore" button next to choices in checkboxes, radio groups, and switches. This allows a user to click for more information about a specific option, perfect for complex forms or when choices require more explanation.
Not included by default, but can be added by using the `c1_included_components` metadata.
```ts theme={null}
const runToolResponse = openAIClient.chat.completions.runTools({
// ... other options
metadata: {
thesys: JSON.stringify({
c1_included_components: ["ExploreButton"],
}),
},
});
```
* **New Chart Type: Donut Chart** - We have added Donut Chart to the library of charts in C1. This is a variant of the Pie Chart that is displayed as a donut shape.
* **New: Condensed Charts** - Perfect for dashboards and tight spaces, C1 can now generate condensed versions of Bar, Area, and Line charts. These minimalist charts are designed to convey trends and data at a glance without taking up too much screen real estate.
* **Smarter Icon Generation** : We've significantly improved our icon generation logic to be more reliable. This fix completely eliminates cases of missing icons in the UI.
Model versions:
* `c1/anthropic/claude-sonnet-4/v-20250930`
* `c1/openai/gpt-5/v-20250930`
Minimum SDK version:
* @crayonai/react-ui: `0.8.41`
* @thesysai/genui-sdk: `0.6.40`
## New Features
* **Custom Actions** - We now allow C1 users to define custom actions that can be triggered from the C1 UI.
For more details and examples, refer to [Custom Actions](/guides/custom-actions).
* **New Components** - We have added the following new components:
* **Single stacked bar chart** - A horizontal bar chart that represents a whole divided into proportional segments. Each segment (or “slice”) corresponds to a specific category and visually depicts its percentage contribution to the total.
* **Scatter chart** - A chart that plots data points to show the relationship between two variables. It helps identify trends, correlations, and outliers in the data.
* **Redesigned Component** - We have redesigned the Carousel, Table and Slider components to be more responsive and user-friendly.
Model versions:
* `c1/anthropic/claude-sonnet-4/v-20250915`
* `c1/openai/gpt-5/v-20250915`
Minimum SDK version:
* @crayonai/react-ui: `0.8.31`
* @thesysai/genui-sdk: `0.6.34`
## New Features
* **Bring Your Own Components** - We now allow C1 users to provide their own custom React components for generation as a part of the C1 API and GenUI SDK.
For more details and examples, refer to [Custom Components](/guides/custom-components).
* **New Stable Models** - With `v-20250915` we are introducing GPT-5 as a stable offering.
Older model strings for GPT-5 experimental (`c1-exp/openai/gpt-5/`) will no longer be maintained, and all new features and bug fixes will now be released for the stable versions.
* **Foldable Sections** - We updated our sections to now allow foldable sections. The C1 API decides whether to use foldable sections or not at runtime based on the content and length of the section.
* **Inline Headers** - Components now have an optional inline header, allowing for more context to be given with the response.
## Bug Fixes / Improvements
* Form inputs are now disabled until the assistant response is fully generated.
* Improved component usage of `c1/anthropic/claude-sonnet-4/v-20250915` and `c1/openai/gpt-5/v-20250915`
* Improved internal error handling of the C1 API
Model versions:
* `c1/anthropic/claude-sonnet-4/v-20250831`
* `c1-exp/openai/gpt-5/v-20250831`
Minimum SDK version:
* @crayonai/react-ui:"0.8.27"
* @thesysai/genui-sdk: "0.6.32"
## New Features
* **Editable Table** - Added support for an inline multi edit component that allows changing multiple records at once.
Not included by default, but can be added by using the `c1_included_components` metadata.
```ts {3-6} [expandable] theme={null}
const runToolResponse = openAIClient.chat.completions.runTools({
// ... other options
metadata: {
thesys: JSON.stringify({
c1_included_components: ["EditableTable"],
}),
},
});
```
* **Improved List** - We refined the list component by removing unclear interactive behavior, improving
visual clarity, and adding support for multiple types: images, icons, and numbers.
* **Improved Callout** - We updated the callout visually to feel more contextual and natural specifically
within conversation-based interfaces, improving clarity and highlighting key information more effectively.
* **Form validations** - Generated forms can optionally have validation rules applied to them.
* **Improved Error Handling** - SDK now allows passing an `onError` prop to the `C1Component` to handle errors.
```tsx {3-5} [expandable] theme={null}
import { C1Component } from "@thesysai/genui-sdk";
...
{
console.error(`Generated UI error: ${code}, ${c1Response}`);
}} />
```
## Bug Fixes / Improvements
* Improved carousel responsiveness to adjust for different screen sizes.
Deprecation Notice: From this version onwards Anthropic Sonnet 3.5 and Sonnet 3.7 will not be supported since Anthropic has announced EOL for these models and actively suggests using Sonnet 4 instead.
***
Model versions:
* `c1/anthropic/claude-sonnet-4/v-20250815`
* `c1/anthropic/claude-3.7-sonnet/v-20250815`
* `c1/anthropic/claude-3.5-sonnet/v-20250815`
## UI
* Added support for advance layouting options
* Added new mini-card component
* Advanced layouting options can be disabled by using metadata. Pass `c1_excluded_components` in the metadata to exclude the component. This might be useful
if you are building a copilot or width-constrained agent.
```ts focus={3-7} theme={null}
const runToolResponse = openAIClient.chat.completions.runTools({
// ... other options
metadata: {
thesys: JSON.stringify({
c1_excluded_components: ["Layout"],
}),
},
});
```
* Added optional description for switch, radio and checkbox components - this is helpful for explaining each option to the user before they make a choice.
## Bug Fixes / Improvements
* Fixed a bug where carousels appeared visually cluttered.
***
***
Model versions:
* `c1/anthropic/claude-3.5-sonnet/v-20250709`
* Added new 'Section' and 'Horizontal Stacked Bar Chart' components
* Updated graph schema to significantly reduce token usage
* Various fixes:
* Improved KaTeX rendering in markdown
* Fixed chart issues with negative and very large numbers
* Corrected Y-axis label display in charts
* Fixed datepicker value saving and submission
***
# Models & Compatibility
Source: https://docs.thesys.dev/api-reference/models-and-compatibility
## Models & Pricing
C1 is family of API endpoints that use different underlying LLM models.
The pricing is based on the model family and the amount of tokens used.
Pricing is based on model name irrespective of the kind of endpoint you use.
For example, using the [embed](/api-reference/endpoints/embed) endpoint with
`c1/anthropic/claude-sonnet-4/v-*` is priced the same as using the [visualize](/api-reference/endpoints/visualize)
endpoint with the same model.
Generally speaking, a model name is more than just the model of the LLM it uses.
It also includes the version of the model, the set of components available for use in the UI,
and any other metadata.
This guide is called "Models" and not "API Versions" because typically you'd pass
the API version to the `model` parameter in the request to the LLM compatible
endpoints of C1.
## Model Versions & SDK Requirements
Thesys supports all major models — including every model from OpenAI, Anthropic, and Google, as well as the full catalog of models available via OpenRouter. All models use the same model string format:
`c1/provider/model/v-*`
* `c1` prefix: marks the request as a C1 Generative UI request
* `provider/model`: standard provider/model path (follows the OpenRouter naming convention)
* `v-*`: the Thesys API version — see [API Changelog](/api-reference/model-changelog) for details on each release
Only `v-20251230` is currently supported for OpenRouter models.
### Model Coverage
**OpenAI, Anthropic & Google** are accessed through first-party APIs, giving you full compliance coverage, zero data retention guarantees, and rigorous quality assurance. These are the primary recommended providers and the ones we actively test and optimise against. See the [Recommended Models](#recommended-models) section for a curated list. For example, to use Claude Opus 4.6: `c1/anthropic/claude-opus-4.6/v-20251230`.
**All other models** are available via OpenRouter — any model in the OpenRouter catalog works with the same `c1/provider/model/v-*` format. You can configure provider constraints such as ZDR and data residency via the `provider` parameter in your API request — see [OpenRouter Provider Routing](https://openrouter.ai/docs/guides/routing/provider-selection). For example, to use Qwen 3.5: `c1/qwen/qwen3.5-397b-a17b/v-20251230`.
Compliance and availability are not guaranteed for OpenRouter-routed models.
OpenRouter may route requests across multiple underlying providers, each with
their own data retention and availability policies, which are outside of
Thesys's control. You can use the `provider` parameter to restrict routing to
specific providers or enforce constraints such as ZDR — see [OpenRouter
Provider
Routing](https://openrouter.ai/docs/guides/routing/provider-selection) for
configuration options. For compliance-sensitive workloads, use OpenAI,
Anthropic, or Google models via the Thesys API.
### Recommended Models
#### Embed Endpoint Models
**Model:** `c1/anthropic/claude-sonnet-4.6/v-20260331`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.9.0 @crayonai/react-ui@~0.9.16 @crayonai/react-core@~0.7.7
```
**Model:** `c1/openai/gpt-5.2/v-20260331`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.9.0 @crayonai/react-ui@~0.9.16 @crayonai/react-core@~0.7.7
```
**Model:** `c1/anthropic/claude-sonnet-4/v-20251230`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.7.15 @crayonai/react-ui@~0.9.9 @crayonai/react-core@~0.7.6
```
**Model:** `c1/openai/gpt-5.2/v-20251230`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.7.15 @crayonai/react-ui@~0.9.9 @crayonai/react-core@~0.7.6
```
**Model:** `c1/openai/gpt-5/v-20251230`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.7.15 @crayonai/react-ui@~0.9.9 @crayonai/react-core@~0.7.6
```
**Model:** `c1/google/gemini-3-pro/v-20251230`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.7.15 @crayonai/react-ui@~0.9.9 @crayonai/react-core@~0.7.6
```
Reasoning for Gemini 3 Pro cannot be disabled, which increases the Time-to-First-Bite (TTFB). This might lead to high user-perceived latency.
**Model:** `c1/google/gemini-3-flash/v-20251230`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.7.15 @crayonai/react-ui@~0.9.9 @crayonai/react-core@~0.7.6
```
#### Visualize Endpoint Model
**Model:** `c1/anthropic/claude-sonnet-4/v-20251230`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.7.15 @crayonai/react-ui@~0.9.9 @crayonai/react-core@~0.7.6
```
#### Artifact Endpoint Model
**Model:** `c1/artifact/v-20260130`
**Install Dependencies:**
```bash theme={null}
npm install @thesysai/genui-sdk@~0.8.3 @crayonai/react-ui@~0.9.16 @crayonai/react-core@~0.7.6
```
#### Free Models
Requests and responses for free models will be stored and might be used to
improve services and offerings.
**Model:** `c1/google/gemini-3.1-pro-free/v-20260331`
**Model:** `c1/google/gemini-3.1-flash-lite-free/v-20260331`
### Other Model Versions
| C1 Model (Latest Version) | genui-sdk | react-ui | react-core |
| :------------------------------------------ | :-------- | :------- | :--------- |
| `c1/anthropic/claude-3.5-sonnet/v-20250709` | \~0.6.27 | \~0.8.14 | \~0.7.6 |
| `c1/anthropic/claude-3.7-sonnet/v-20250709` | \~0.6.27 | \~0.8.14 | \~0.7.6 |
| `c1-latest` | -- | -- | -- |
| `c1-202504XX*` | -- | -- | -- |
## Model Specifications
These specifications apply to all versions of each model family.
| C1 Model | LLM | Input Price / M tokens | Cached Input (Read) | Cached Input (Write) | Output Price / M tokens | Context Window | Max Output |
| :----------------------------------- | :--------- | :--------------------- | :------------------ | :------------------- | :---------------------- | :------------- | :--------- |
| `c1/anthropic/claude-sonnet-4` | Sonnet 4 | \$3.00 | \$0.30 | \$3.75 | \$15.00 | 200K | 64K |
| `c1/openai/gpt-5.2` | GPT 5.2 | \$1.75 | \$0.175 | -- | \$14.00 | 400K | 128K |
| `c1/openai/gpt-5` | GPT 5 | \$1.25 | \$0.125 | -- | \$10.50 | 400K | 128K |
| `c1-exp/anthropic/claude-sonnet-4.5` | Sonnet 4.5 | \$3.00 | \$0.30 | \$3.75 | \$15.00 | 200K | 64K |
| `c1-exp/anthropic/claude-haiku-4.5` | Haiku 4.5 | \$1.00 | \$0.10 | \$1.25 | \$5.00 | 200K | 64K |
| `c1-exp/anthropic/claude-3.5-haiku` | 3.5 Haiku | \$0.80 | \$0.80 | \$1.00 | \$4.00 | 200K | 8192 |
| `c1-exp/openai/gpt-4.1` | GPT 4.1 | \$2.00 | \$0.50 | -- | \$8.00 | 1M | 32K |
# Messages
Source: https://docs.thesys.dev/api-reference/objects/message
UI completions are returned by the API when `stream=false` is set in the request.
## Request Headers
The API key to use for the request.
The content type of the request. Should be `application/json`.
## Request Body
An array of messages of the conversation so far.
The role of the message. Can be either `user`, `assistant`, or `tool`.
The content of the message.
The ID of the tool call. Only present if the role is `tool`.
The model to use for the UI completion. Should be one of the models listed in the [Models](/api-reference/models-and-compatibility) page.
The temperature to use for the UI completion. Should be between 0 and 1.
The top-p value to use for the UI completion. Should be between 0 and 1.
The maximum number of tokens to use for the UI completion.
The number of completions to generate.
Should be unset or set to 1.
Whether to stream the response. Should be unset or `false`.
The parameters above are commonly used with the Messages API. For a
comprehensive list of all supported parameters and their compatibility across
different model providers (OpenAI vs Anthropic), see the [Supported API
Parameters](/api-reference/getting-started#supported-api-parameters) table.
```sh curl theme={null}
curl -X POST \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"model": "c1/anthropic/claude-sonnet-4/v-20251230", "messages": [{"role": "user", "content": "Hello, world!"}]}' \
https://api.thesys.dev/v1/embed/chat/completions
```
```json theme={null}
{
"id": "chatcmpl-1743157633416-cffw7tgswx",
"object": "chat.completion",
"created": 1743157633,
"model": "c1/anthropic/claude-sonnet-4/v-20251230",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": {
...
}
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 8,
"completion_tokens": 439,
"total_tokens": 447
}
}
```
## Response
A unique identifier for the UI completion
An array of completions. Will always contain exactly one object.
The reason the generation stopped. Can be either `stop` or `tool_calls`.
Completion message generated by the model.
The role of the message. Will always be `assistant` for generations.
A JSON object containing the UI completion.
The timestamp when the UI completion was created
The model used to generate the UI completion
The object type, which is always `chat.completion`
An object containing the usage statistics for the UI completion
Number of tokens in the prompt
Number of tokens in the generated UI completion
Total number of tokens: `prompt_tokens + completion_tokens`
# Streaming Chunks
Source: https://docs.thesys.dev/api-reference/objects/streaming
Streaming message chunks are returned by the API when `stream=true` is set in the request.
## Request Headers
The API key to use for the request.
The content type of the request. Should be `application/json`.
## Request Body
An array of messages of the conversation so far.
The role of the message. Can be either `user`, `assistant`, or `tool`.
The content of the message.
The ID of the tool call. Only present if the role is `tool`.
The model to use for the UI completion. Should be one of the models listed in the [Models](/api-reference/models-and-compatibility) page.
The temperature to use for the UI completion. Should be between 0 and 1.
The top-p value to use for the UI completion. Should be between 0 and 1.
The maximum number of tokens to use for the UI completion.
The number of completions to generate.
Should be unset or set to 1.
Whether to stream the response. Should be `true` for streaming.
The parameters above are commonly used with the Messages API. For a
comprehensive list of all supported parameters and their compatibility across
different model providers (OpenAI vs Anthropic), see the [Supported API
Parameters](/api-reference/getting-started#supported-api-parameters) table.
```sh curl theme={null}
curl -X POST \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"model": "c1/anthropic/claude-sonnet-4/v-20251230", "stream": true, "messages": [{"role": "user", "content": "Hello, world!"}]}' \
https://api.thesys.dev/v1/embed/chat/completions
```
```
id: 1
data: {"id":"chatcmpl-1743157601318-pzx9ougakm","object":"chat.completion.chunk","created":1743157601,"model":"c1/anthropic/claude-sonnet-4/v-20251230","choices":[{"index":0,"delta":{"role":"assistant","content":{...}}]}
id: 2
data: {"id":"chatcmpl-1743157601318-pzx9ougakm","object":"chat.completion.chunk","created":1743157601,"model":"c1/anthropic/claude-sonnet-4/v-20251230","choices":[{"index":0,"delta":{"content":{...}}]}
```
## SSE Event Stream
A identifier for this event.
One of the following objects:
* [Completion Chunk](#completion-chunk)
## Completion Chunk
A unique identifier for the UI completion. Will be the same in all events for a given completion.
The object type, which is always `chat.completion.chunk`
Timestamp for when the chunk was created.
The model used to generate the UI completion.
An object containing the usage statistics for the UI completion
Only present if the generation is complete.
Number of tokens in the prompt
Number of tokens in the generated UI completion
Total number of tokens: `prompt_tokens + completion_tokens`
An array of completions. Will always contain exactly one object.
The reason the generation stopped. Can be either `stop` or `tool_calls`.
Present only if the generation is complete.
A JSON object containing the UI completion.
# Rate Limits
Source: https://docs.thesys.dev/api-reference/rate-limits
Like most other LLM API providers, we have 2 sets of rate limits to prevent abuse and
manage our capacity.
* Spend limits: We limit the max amount of money you can spend in a month.
* Usage limits: We limit the number of requests you can make within a time window.
## Spend Limits
Spend limits are set per organization per month. We are working on adding support
for setting spend limits automatically from the developer console but for now,
you can contact us at `support@thesys.dev` to set a spend limit for your organization.
Billing is done on a monthly basis or a cumulative monthly spend of \$100, whichever comes first.
## Usage Limits
* Usage limits are also set per organization irrespective of the number of API key used.
* Limits are enforced at both `ITPM` (Input Tokens Per Minute) and `OTPM` (Output Tokens Per Minute)
* If you hit the rate limit, you will receive a 429 error.
If you need higher limits, please contact us at `support@thesys.dev`
# Build an AI Financial Data Copilot
Source: https://docs.thesys.dev/examples/analytics-with-c1
Learn how to build an AI-powered data copilot with generative UI and real-time financial data
Build an intelligent financial data copilot that generates custom visualizations and insights based on natural language queries. This guide teaches you how to combine financial APIs with C1 to create dynamic charts, tables, and insights.
**Try it above** or visit [analytics-with-c1.vercel.app](https://analytics-with-c1.vercel.app)
## What You'll Learn
* Connecting C1 with financial data APIs using tool calling
* Building a suite of analytics tools (stocks, crypto, financials)
* Designing system prompts for data visualization
* Automatic chart generation based on data type
* Adding web search for market context
* Thread management for multi-turn conversations
* Setting up C1Chat for financial analysis UI
## Architecture Overview
AI data copilots work differently from traditional dashboards:
```
User Query → LLM (with data tools) → Financial APIs → LLM analyzes data → C1 generates visualizations → Stream to user
```
Instead of pre-built dashboards, the copilot generates the perfect visualization for each query - charts for trends, tables for comparisons, cards for summaries.
## Setup
### Prerequisites
* Node.js 18+
* Thesys API key from [console.thesys.dev](https://console.thesys.dev)
* Financial Datasets API key from [financialdatasets.ai](https://financialdatasets.ai)
* (Optional) Exa API key from [exa.ai](https://exa.ai) for web search
### Create Next.js Project
```bash npm theme={null}
npx create-next-app@latest analytics-with-c1
cd analytics-with-c1
```
```bash pnpm theme={null}
pnpm create next-app analytics-with-c1
cd analytics-with-c1
```
```bash yarn theme={null}
yarn create next-app analytics-with-c1
cd analytics-with-c1
```
When prompted, select:
* TypeScript: Yes
* ESLint: Yes
* Tailwind CSS: Yes
* App Router: Yes
* Customize default import alias: No
### Install Dependencies
```bash npm theme={null}
npm install @thesysai/genui-sdk @crayonai/react-ui openai financial-datasets exa-js zod zod-to-json-schema
```
```bash pnpm theme={null}
pnpm add @thesysai/genui-sdk @crayonai/react-ui openai financial-datasets exa-js zod zod-to-json-schema
```
```bash yarn theme={null}
yarn add @thesysai/genui-sdk @crayonai/react-ui openai financial-datasets exa-js zod zod-to-json-schema
```
### Environment Variables
Create a `.env.local` file:
```bash theme={null}
THESYS_API_KEY=your_thesys_api_key
FINANCIAL_DATASETS_API_KEY=your_financial_datasets_key
# Optional: For web search context
EXA_API_KEY=your_exa_api_key
```
Sign up for Financial Datasets at [financialdatasets.ai](https://financialdatasets.ai) to get real-time stock, crypto, and financial data.
## Step 1: Set Up Financial Data Tools
Create tools that fetch financial data. C1 will call these automatically based on user queries:
```typescript tools/financial.ts theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
// Define your financial API client
const financialAPI = new FinancialDatasets({
apiKey: process.env.FINANCIAL_DATASETS_API_KEY,
});
export const getFinancialTools = (writeThinkingState) => {
const createTool = (name, description, schema, fn, thinkingState) => ({
type: "function",
function: {
name,
description,
parameters: zodToJsonSchema(schema),
function: async (args: string) => {
const parsedArgs = JSON.parse(args);
// Show user what's happening
writeThinkingState(
typeof thinkingState === "function"
? thinkingState(parsedArgs)
: thinkingState
);
const result = await fn(parsedArgs);
return JSON.stringify(result);
},
},
});
return [
// Stock price tool
createTool(
"get_current_stock_price",
"Get the current/latest price of a company",
z.object({ ticker: z.string() }),
async ({ ticker }) => {
return await financialAPI.getStockPrice(ticker);
},
(args) => ({
title: `Fetching Stock Price for ${args.ticker}`,
description: "Getting the latest market price",
})
),
// Historical stock data tool
createTool(
"get_historical_stock_prices",
"Gets historical stock prices for charting trends",
z.object({
ticker: z.string(),
start_date: z.string(),
end_date: z.string(),
}),
async ({ ticker, start_date, end_date }) => {
return await financialAPI.getHistoricalPrices(
ticker,
start_date,
end_date
);
},
(args) => ({
title: `Charting Historical Prices for ${args.ticker}`,
description: "Plotting past stock performance to identify trends",
})
),
// Company financials tool
createTool(
"get_income_statements",
"Get income statements to analyze profitability",
z.object({ ticker: z.string() }),
async ({ ticker }) => {
return await financialAPI.getIncomeStatements(ticker);
},
(args) => ({
title: `Analyzing Income Statements for ${args.ticker}`,
description: "Reviewing profitability and financial performance",
})
),
// Crypto price tool
createTool(
"get_current_crypto_price",
"Get current cryptocurrency price",
z.object({ ticker: z.string() }),
async ({ ticker }) => {
return await financialAPI.getCryptoPrice(ticker);
},
(args) => ({
title: `Getting Crypto Price for ${args.ticker}`,
description: "Fetching current market value",
})
),
// Company news tool
createTool(
"get_company_news",
"Get latest news for a company",
z.object({ ticker: z.string() }),
async ({ ticker }) => {
return await financialAPI.getCompanyNews(ticker);
},
(args) => ({
title: `Scanning News for ${args.ticker}`,
description: "Catching up on latest announcements",
})
),
];
};
```
```python tools/financial.py theme={null}
from typing import Callable, Dict, Any
from pydantic import BaseModel
import json
# Define your financial API client
financial_api = FinancialDatasets(
api_key=os.environ["FINANCIAL_DATASETS_API_KEY"]
)
def get_financial_tools(write_thinking_state: Callable):
def create_tool(name: str, description: str, schema: Dict, fn: Callable, thinking_state):
async def tool_function(args: str) -> str:
parsed_args = json.loads(args)
# Show user what's happening
state = (
thinking_state(parsed_args)
if callable(thinking_state)
else thinking_state
)
write_thinking_state(state)
result = await fn(parsed_args)
return json.dumps(result)
return {
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": schema,
},
}
return [
create_tool(
"get_current_stock_price",
"Get the current/latest price of a company",
{
"type": "object",
"properties": {
"ticker": {"type": "string"}
},
"required": ["ticker"],
},
lambda args: financial_api.get_stock_price(args["ticker"]),
lambda args: {
"title": f"Fetching Stock Price for {args['ticker']}",
"description": "Getting the latest market price",
},
),
# Add more tools similarly...
]
```
The `writeThinkingState` calls create a better UX by showing users which data sources are being queried in real-time.
## Step 2: Add Web Search for Context
Financial data is more useful with context. Add web search to explain market movements:
```typescript tools/web-search.ts theme={null}
import Exa from "exa-js";
const exa = new Exa(process.env.EXA_API_KEY);
export const createWebSearchTool = (writeThinkingState) => ({
type: "function",
function: {
name: "webSearch",
description: "Search the web for latest market information and context",
parameters: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
function: async ({ query }: { query: string }) => {
writeThinkingState({
title: "Searching the web",
description: "Collecting live insights for broader context",
});
// Exa's answer mode returns direct answers with citations
const results = await exa.answer(query);
return JSON.stringify({
answer: results.answer,
citations: results.citations.map(({ title, text }) => ({
text: text ?? title,
})),
});
},
},
});
```
```python tools/web_search.py theme={null}
from exa_py import Exa
exa = Exa(api_key=os.environ["EXA_API_KEY"])
def create_web_search_tool(write_thinking_state):
async def web_search(query: str) -> str:
write_thinking_state({
"title": "Searching the web",
"description": "Collecting live insights for broader context",
})
# Exa's answer mode returns direct answers
results = await exa.answer(query)
return json.dumps({
"answer": results.answer,
"citations": [
{"text": c.text or c.title}
for c in results.citations
],
})
return {
"type": "function",
"function": {
"name": "webSearch",
"description": "Search the web for latest market information",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
},
"required": ["query"],
},
},
}
```
## Step 3: Create the Analytics Endpoint
Connect your tools with C1 to create the main analytics endpoint:
```typescript app/api/chat/route.ts theme={null}
import { makeC1Response } from "@thesysai/genui-sdk/server";
import OpenAI from "openai";
import { getFinancialTools } from "@/tools/financial";
import { createWebSearchTool } from "@/tools/web-search";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
export async function POST(req: NextRequest) {
const { prompt, threadId } = await req.json();
const c1Response = makeC1Response();
c1Response.writeThinkItem({
title: "Analyzing Prompt",
description: "Interpreting your query and preparing data sources",
});
// Combine all tools
const financialTools = getFinancialTools(c1Response.writeThinkItem);
const webSearchTool = createWebSearchTool(c1Response.writeThinkItem);
const allTools = [...financialTools, webSearchTool];
// Call C1 with automatic tool execution
const runToolsResponse = client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
...(await getThreadHistory(threadId)),
{
role: "user",
content: prompt,
},
],
stream: true,
tools: allTools,
});
// Stream content as it's generated
runToolsResponse.on("content", c1Response.writeContent);
// Clean up when done
runToolsResponse.on("end", async () => {
c1Response.end();
await saveToThread(threadId, prompt, c1Response.getAssistantMessage());
});
return new Response(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```python main.py theme={null}
from fastapi import FastAPI
from openai import OpenAI
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, write_think_item
client = OpenAI(
api_key=os.environ["THESYS_API_KEY"],
base_url="https://api.thesys.dev/v1/embed",
)
@app.post("/api/chat")
@with_c1_response()
async def chat(request: ChatRequest):
await write_think_item({
"title": "Analyzing Prompt",
"description": "Interpreting your query and preparing data sources",
})
# Combine all tools
financial_tools = get_financial_tools(write_think_item)
web_search_tool = create_web_search_tool(write_think_item)
all_tools = [*financial_tools, web_search_tool]
# Call C1 with automatic tool execution
stream = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
*await get_thread_history(request.threadId),
{"role": "user", "content": request.prompt},
],
stream=True,
tools=all_tools,
)
# Stream response
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
await save_to_thread(
request.threadId,
request.prompt,
get_assistant_message()
)
```
## Step 4: Craft the System Prompt for Analytics
The system prompt guides C1 to create appropriate visualizations:
```typescript theme={null}
const SYSTEM_PROMPT = `You are a financial data copilot. Given the user's prompt, generate visualizations and insights to answer their financial questions.
**Visualization Rules:**
1. **Use charts for trends and time series:**
- Line charts for stock prices over time
- Bar charts for comparing metrics across periods
- Area charts for cumulative values
2. **Use tables for detailed financial data:**
- Income statements, balance sheets, cash flows
- Show key metrics with proper formatting (currency, percentages)
3. **Use cards for summaries and key metrics:**
- Current stock price with change indicators
- Key financial ratios
- Quick facts and highlights
4. **Add context with web search:**
- If stock price dropped, search for "why did [TICKER] stock fall"
- For earnings data, search for latest analyst opinions
- Provide market context for significant changes
5. **Structure responses clearly:**
- Start with the answer to the user's question
- Show relevant visualizations
- Add supporting data and context
- No follow-up questions in responses
Current date: ${new Date().toISOString()}
`;
```
Being specific about **when** to use each visualization type (charts vs tables vs cards) leads to consistent, professional-looking outputs.
## Step 5: Thread Management
Enable multi-turn analysis by maintaining conversation context:
```typescript threads.ts theme={null}
interface Message {
role: "user" | "assistant";
content: string;
}
const threads = new Map();
export const getThreadHistory = async (threadId: string) => {
return threads.get(threadId) || [];
};
export const saveToThread = async (
threadId: string,
userMessage: string,
assistantMessage: string
) => {
const history = threads.get(threadId) || [];
history.push(
{ role: "user", content: userMessage },
{ role: "assistant", content: assistantMessage }
);
threads.set(threadId, history);
};
```
```python threads.py theme={null}
from typing import List, Dict
threads: Dict[str, List[Dict[str, str]]] = {}
async def get_thread_history(thread_id: str) -> List[Dict[str, str]]:
return threads.get(thread_id, [])
async def save_to_thread(
thread_id: str,
user_message: str,
assistant_message: str
):
history = threads.get(thread_id, [])
history.extend([
{"role": "user", "content": user_message},
{"role": "assistant", "content": assistant_message},
])
threads[thread_id] = history
```
**Why threads matter:**
```
User: "Show me Apple's stock price"
AI: [Shows current price chart]
User: "How does that compare to last quarter?"
AI: [References Apple from context, shows quarterly comparison]
```
## Step 6: Set Up the Frontend UI
Create the conversational analytics interface using C1Chat:
```typescript app/page.tsx theme={null}
"use client";
import { C1Chat } from "@crayonai/react-ui";
import "@crayonai/react-ui/styles/index.css";
import { useState } from "react";
export default function Page() {
const [threadId] = useState(() => crypto.randomUUID());
return (
);
}
```
C1Chat provides the complete conversational analytics UI with:
* Message history
* Streaming responses with live chart generation
* Thinking states showing which APIs are being queried
* Automatic thread management for follow-ups
* Responsive design for charts and tables
## Step 7: Run Your Analytics Copilot
Start the development server:
```bash npm theme={null}
npm run dev
```
```bash pnpm theme={null}
pnpm dev
```
```bash yarn theme={null}
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) and try these queries:
1. **"Show me Tesla's stock price"** - See current price with change indicators
2. **"What's Apple's revenue trend over the last 4 quarters?"** - Get financial statements with charts
3. **"Compare Bitcoin and Ethereum prices"** - See multi-asset comparison
4. Follow up: **"Which one performed better this month?"** - Test thread continuity
The first query might take a few seconds as financial data is fetched. The copilot will show thinking states like "Fetching Stock Price for TSLA" so users know what's happening.
## Key Concepts
Separate tools let the LLM understand what data is needed. Instead of you parsing "show me Apple's revenue", the LLM sees this needs `get_income_statements` with ticker "AAPL". The LLM becomes your query planner.
C1 analyzes the data structure and your system prompt. If data has timestamps, it generates time-series charts. If comparing multiple values, it creates comparison charts. Your prompt guides these decisions.
Use C1 (`c1/anthropic/claude-sonnet-4/v-20250930`) instead of direct Claude API. C1 adds UI generation capabilities on top of Claude's analysis. Direct Claude only returns text.
Yes! Use [Custom Components](/guides/custom-components) to define domain-specific charts like candlestick charts, correlation matrices, or portfolio allocations.
## Going to Production
Before deploying:
1. **Add rate limiting** to prevent API quota exhaustion
2. **Implement error handling** for failed API calls
3. **Monitor API costs** - financial data APIs can get expensive
4. **Add authentication** if serving multiple users
## Full Example & Source Code
Experience the full data copilot. Try asking about stocks, crypto, or company financials to see C1 generate custom visualizations.
**Try it now →**
Complete implementation with all financial tools, thread management, and deployment config.
**Star on GitHub →**
# Build an AI-Powered Infinite Canvas
Source: https://docs.thesys.dev/examples/canvas-with-c1
Learn how to build an infinite canvas with AI-generated visual cards for ideation, planning, and research
Build an intelligent infinite canvas where AI generates rich visual cards based on your prompts. This guide teaches you how to integrate tldraw with C1 to create context-aware cards that understand existing canvas content.
**Try it above** or visit [canvas-with-c1.vercel.app](https://canvas-with-c1.vercel.app)
## What You'll Learn
* Integrating tldraw infinite canvas with C1
* Creating custom shape utils for AI-generated content
* Building context-aware systems that understand selected shapes
* Real-time streaming content into canvas cards
* Implementing keyboard shortcuts for quick access
* Adding image search to enrich visual content
## Architecture Overview
AI canvas apps combine visual collaboration tools with generative UI:
```
User Prompt → Extract Context from Selected Cards → LLM + C1 → Generate Visual Card → Stream to Canvas Shape
```
When you select existing cards and create a new one, the AI sees the content of selected cards and generates contextually relevant responses. Each card is a resizable, repositionable C1 component.
## Setup
### Prerequisites
* Node.js 18+
* Thesys API key from [console.thesys.dev](https://console.thesys.dev)
* (Optional) Google Custom Search API key and CSE ID for image search
### Create Next.js Project
```bash npm theme={null}
npx create-next-app@latest canvas-with-c1
cd canvas-with-c1
```
```bash pnpm theme={null}
pnpm create next-app canvas-with-c1
cd canvas-with-c1
```
```bash yarn theme={null}
yarn create next-app canvas-with-c1
cd canvas-with-c1
```
When prompted, select:
* TypeScript: Yes
* ESLint: Yes
* Tailwind CSS: Yes
* App Router: Yes
* Customize default import alias: No
### Install Dependencies
```bash npm theme={null}
npm install @thesysai/genui-sdk @crayonai/react-ui openai tldraw zod zod-to-json-schema
```
```bash pnpm theme={null}
pnpm add @thesysai/genui-sdk @crayonai/react-ui openai tldraw zod zod-to-json-schema
```
```bash yarn theme={null}
yarn add @thesysai/genui-sdk @crayonai/react-ui openai tldraw zod zod-to-json-schema
```
Optional (for image search):
```bash npm theme={null}
npm install google-images
```
```bash pnpm theme={null}
pnpm add google-images
```
```bash yarn theme={null}
yarn add google-images
```
### Environment Variables
Create a `.env.local` file:
```bash theme={null}
THESYS_API_KEY=your_thesys_api_key
# Optional: For image search
GOOGLE_API_KEY=your_google_api_key
GOOGLE_CSE_ID=your_custom_search_engine_id
```
Get your Thesys API key from [console.thesys.dev](https://console.thesys.dev). For Google image search, follow the [Custom Search API guide](https://developers.google.com/custom-search/v1/introduction).
## Step 1: Set Up tldraw Canvas
Start by creating the infinite canvas workspace with tldraw:
```typescript app/page.tsx theme={null}
"use client";
import "@crayonai/react-ui/styles/index.css";
import "tldraw/tldraw.css";
import { Tldraw } from "tldraw";
import { shapeUtils } from "./shapeUtils";
import { PromptInput } from "./components/PromptInput";
import { FOCUS_PROMPT_EVENT } from "./events";
export default function Page() {
return (
);
}
```
The `persistenceKey` saves canvas state to localStorage so users don't lose their work on refresh.
## Step 2: Create C1 Component Shape
Define a custom tldraw shape that wraps C1 components:
```typescript shapes/C1ComponentShape.tsx theme={null}
import type { TLBaseShape } from "tldraw";
export type C1ComponentShapeProps = {
w: number;
h: number;
c1Response?: string;
isStreaming?: boolean;
prompt?: string;
};
export type C1ComponentShape = TLBaseShape<
"c1-component",
C1ComponentShapeProps
>;
```
```typescript shapeUtils/C1ComponentShapeUtil.tsx theme={null}
import { HTMLContainer, ShapeUtil } from "tldraw";
import { C1Component } from "@crayonai/react-ui";
import type { C1ComponentShape } from "../shapes/C1ComponentShape";
export class C1ComponentShapeUtil extends ShapeUtil {
static override type = "c1-component" as const;
getDefaultProps(): C1ComponentShape["props"] {
return {
w: 600,
h: 300,
c1Response: "",
isStreaming: false,
};
}
component(shape: C1ComponentShape) {
return (
);
}
indicator(shape: C1ComponentShape) {
return (
);
}
}
```
This creates a resizable, repositionable shape that renders C1 components on the canvas.
## Step 3: Extract Context from Selected Shapes
When users select existing cards, extract their content to provide context to the AI:
```typescript utils/shapeContext.ts theme={null}
import type { Editor, TLShape } from "tldraw";
import type { C1ComponentShapeProps } from "@/app/shapes/C1ComponentShape";
export function extractC1ShapeContext(editor: Editor): string {
const selectedShapes = editor.getSelectedShapes();
const c1Shapes = selectedShapes.filter(
(shape): shape is TLShape => shape.type === "c1-component"
);
const c1Responses = c1Shapes
.map((shape) => (shape.props as C1ComponentShapeProps).c1Response)
.filter((response) => response)
.join("\n");
return JSON.stringify(c1Responses);
}
```
**Why this matters:** When a user selects "Tesla Q3 earnings" and "TSLA stock price" cards, then asks "Compare these", the AI sees both cards' content and generates a comparison.
## Step 4: Create the API Endpoint
Build the backend endpoint that generates C1 responses for canvas cards:
```typescript app/api/ask/route.ts theme={null}
import { NextRequest } from "next/server";
import OpenAI from "openai";
import { makeC1Response } from "@thesysai/genui-sdk/server";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
const SYSTEM_PROMPT = `You are a helpful assistant that generates cards for a visual canvas.
- Generate short, focused cards - don't pack everything into one card
- Create visually rich layouts with charts, images, and mini-components
- For comparisons, use tables and side-by-side layouts
- Integrate relevant images to make cards engaging
- Do not use accordions or add follow-up questions
`;
export async function POST(req: NextRequest) {
const { prompt, context } = await req.json();
const c1Response = makeC1Response();
c1Response.writeThinkItem({
title: "Processing your request...",
description: "Analyzing input and preparing visual content.",
});
const messages = [];
// If context exists, combine it with the prompt
if (context) {
messages.push({
role: "user",
content: `{prompt: ${prompt}, context: ${context}}`,
});
} else {
messages.push({
role: "user",
content: prompt,
});
}
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
...messages
],
stream: true,
tools: [], // We'll add image search tool in Step 6
});
llmStream.on("content", c1Response.writeContent);
llmStream.on("end", () => c1Response.end());
return new Response(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```python main.py theme={null}
from fastapi import FastAPI
from openai import OpenAI
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, write_think_item
client = OpenAI(
api_key=os.environ["THESYS_API_KEY"],
base_url="https://api.thesys.dev/v1/embed",
)
SYSTEM_PROMPT = """You are a helpful assistant that generates cards for a visual canvas.
- Generate short, focused cards - don't pack everything into one card
- Create visually rich layouts with charts, images, and mini-components
- For comparisons, use tables and side-by-side layouts
- Integrate relevant images to make cards engaging
- Do not use accordions or add follow-up questions
"""
@app.post("/api/ask")
@with_c1_response()
async def ask(request: AskRequest):
await write_think_item({
"title": "Processing your request...",
"description": "Analyzing input and preparing visual content.",
})
messages = []
# If context exists, combine it with the prompt
if request.context:
content = f"{{prompt: {request.prompt}, context: {request.context}}}"
messages.append({"role": "user", "content": content})
else:
messages.append({"role": "user", "content": request.prompt})
stream = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
*messages
],
stream=True,
tools=[], # Add image search or other tools here
)
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
```
The system prompt is critical for canvas apps. Emphasize short, visually rich cards rather than long text blocks.
## Step 5: Shape Creation and Management
Create a manager to handle the lifecycle of canvas shapes:
```typescript utils/c1ShapeManager.ts theme={null}
import type { Editor, TLShapeId } from "tldraw";
import { createShapeId } from "tldraw";
import { extractC1ShapeContext } from "./shapeContext";
export async function createC1ComponentShape(
editor: Editor,
options: {
searchQuery: string;
width?: number;
height?: number;
}
): Promise {
const { searchQuery, width = 600, height = 300 } = options;
// Generate unique shape ID
const shapeId = createShapeId();
// Extract context from selected shapes
const context = extractC1ShapeContext(editor);
// Calculate optimal position (center of viewport)
const viewport = editor.getViewportPageBounds();
const position = {
x: viewport.center.x - width / 2,
y: viewport.center.y - height / 2,
};
// Create the shape
editor.createShape({
id: shapeId,
type: "c1-component",
x: position.x,
y: position.y,
props: {
w: width,
h: height,
prompt: searchQuery,
},
});
// Call API and stream updates
const response = await fetch("/api/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: searchQuery,
context,
}),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let fullResponse = "";
// Mark shape as streaming
editor.updateShape({
id: shapeId,
type: "c1-component",
props: { isStreaming: true },
});
// Stream updates
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
fullResponse += chunk;
editor.updateShape({
id: shapeId,
type: "c1-component",
props: { c1Response: fullResponse, isStreaming: true },
});
}
// Mark streaming complete
editor.updateShape({
id: shapeId,
type: "c1-component",
props: { isStreaming: false },
});
return shapeId;
}
```
This handles:
1. Extracting context from selected cards
2. Positioning the new card in the viewport
3. Creating the shape
4. Streaming C1 content as it generates
5. Marking completion
## Step 6: Add Image Search Tool (Optional)
Enhance cards with relevant images using tool calling. This step is optional.
Create the image search tool:
```typescript app/api/ask/tools.ts theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import GoogleImages from "google-images";
const client = new GoogleImages(
process.env.GOOGLE_CSE_ID!,
process.env.GOOGLE_API_KEY!
);
export function getImageSearchTool(writeThinkItem?: Function) {
return {
type: "function",
function: {
name: "getImageSrc",
description: "Get image src for given alt text",
parse: JSON.parse,
parameters: zodToJsonSchema(
z.object({
altText: z.string().describe("The alt text of the image"),
})
),
function: async ({ altText }: { altText: string }) => {
if (writeThinkItem) {
writeThinkItem(
"Searching for images...",
"Finding the perfect image for your canvas."
);
}
const results = await client.search(altText, { size: "huge" });
return results[0].url;
},
},
};
}
```
Update `app/api/ask/route.ts` to use the tool:
```typescript theme={null}
import { getImageSearchTool } from "./tools";
export async function POST(req: NextRequest) {
// ... existing code ...
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
...messages
],
stream: true,
tools: [
getImageSearchTool((title: string, desc: string) => {
c1Response.writeThinkItem({ title, description: desc });
})
],
});
// ... rest of the code ...
}
```
The LLM will automatically call this tool when generating cards that need images. Skip this step if you don't have Google API credentials.
## Step 7: Keyboard Shortcuts
Add keyboard shortcut support. Update your `app/page.tsx` to include overrides:
```typescript app/page.tsx theme={null}
"use client";
import "@crayonai/react-ui/styles/index.css";
import "tldraw/tldraw.css";
import { Tldraw, type TLUiOverrides } from "tldraw";
import { shapeUtils } from "./shapeUtils";
import { PromptInput } from "./components/PromptInput";
import { FOCUS_PROMPT_EVENT } from "./events";
const overrides: TLUiOverrides = {
actions: (_editor, actions) => {
return {
...actions,
"focus-prompt-input": {
id: "focus-prompt-input",
label: "Focus Prompt Input",
kbd: "$k", // Cmd/Ctrl + K
onSelect: () => {
window.dispatchEvent(new CustomEvent(FOCUS_PROMPT_EVENT));
},
},
};
},
};
export default function Page() {
return (
);
}
```
The `$k` syntax in tldraw means Cmd+K on Mac, Ctrl+K on Windows/Linux.
## Step 8: Create Prompt Input Component
Create the UI component that users interact with:
```typescript app/components/PromptInput.tsx theme={null}
"use client";
import { useState, useRef, useEffect } from "react";
import { useEditor } from "tldraw";
import { createC1ComponentShape } from "../utils/c1ShapeManager";
interface PromptInputProps {
focusEventName: string;
}
export function PromptInput({ focusEventName }: PromptInputProps) {
const editor = useEditor();
const [isFocused, setIsFocused] = useState(false);
const [prompt, setPrompt] = useState("");
const inputRef = useRef(null);
const isCanvasZeroState = editor.getCurrentPageShapes().length === 0;
// Listen for keyboard shortcut event
useEffect(() => {
const handleFocusEvent = () => {
inputRef.current?.focus();
setIsFocused(true);
};
window.addEventListener(focusEventName, handleFocusEvent);
return () => window.removeEventListener(focusEventName, handleFocusEvent);
}, [focusEventName]);
const onInputSubmit = async (prompt: string) => {
setPrompt("");
try {
await createC1ComponentShape(editor, {
searchQuery: prompt,
width: 600,
height: 300,
});
} catch (error) {
console.error("Failed to create shape:", error);
}
};
return (
);
}
```
Create an event constant:
```typescript app/events/index.ts theme={null}
export const FOCUS_PROMPT_EVENT = "focus-prompt-input";
```
## Step 9: Register Shape Utils and Run
Create the shape utils registry:
```typescript app/shapeUtils/index.ts theme={null}
import { C1ComponentShapeUtil } from "./C1ComponentShapeUtil";
export const shapeUtils = [C1ComponentShapeUtil];
```
Now run your development server:
```bash npm theme={null}
npm run dev
```
```bash pnpm theme={null}
pnpm dev
```
```bash yarn theme={null}
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) and:
1. Press **Cmd/Ctrl + K** to open the prompt input
2. Type "Create a product launch plan" and press Enter
3. Watch as an AI-generated card appears on the canvas
4. Select the card and press Cmd/Ctrl + K again
5. Ask "What are the risks?" - the AI will see the first card's context
6. Experiment with multiple cards and selections
If you skipped Step 6 (image search), the canvas will work perfectly without it - you just won't get automatic image embedding.
## Key Concepts
When you select existing cards on the canvas and create a new one, the `extractC1ShapeContext` function reads the C1 responses from selected shapes and sends them as context to the API. The LLM sees this context and generates responses that reference or build upon the selected cards.
tldraw's shape system provides built-in features: selection, resizing, repositioning, undo/redo, persistence, and export. Using custom shapes means your AI cards integrate seamlessly with tldraw's native editing capabilities.
Currently, cards have fixed dimensions. For dynamic sizing, calculate content height in the shape component and call `editor.updateShape()` with new height. Monitor the C1Component's rendered size and update the shape's `h` prop accordingly.
Yes! tldraw has built-in collaboration support. Use their sync server or implement your own using the `store` prop. Each user's shapes, selections, and camera positions sync in real-time.
## Testing Your Canvas
Try these workflows to test context awareness:
* **Single card**: "Create a product roadmap for Q1"
* **Context-aware**: Create "Tesla stock analysis" card, select it, then ask "What's the outlook?"
* **Multi-select**: Create "Revenue data" and "Cost data" cards, select both, ask "Create a comparison chart"
* **Follow-up**: After any card, select it and ask "Expand on this with more details"
## Going to Production
Before deploying:
1. **Add authentication** to prevent unauthorized API usage
2. **Implement rate limiting** on the `/api/ask` endpoint
3. **Set up canvas sharing** if you want users to share their canvases
4. **Add export functionality** - tldraw can export to PNG/SVG
5. **Consider collaboration** - tldraw supports multiplayer mode
## Full Example & Source Code
Experience the full infinite canvas. Try creating cards, selecting them for context, and building visual layouts with AI assistance.
**Try it now →**
Complete implementation with tldraw integration, custom shape utils, context extraction, and streaming updates.
**Star on GitHub →**
# Company Research Agent
Source: https://docs.thesys.dev/examples/company-research
Build a Generative UI Company Research agent using Thesys
For the purpose of demonstration, this guide will build a Generative UI search application where the user can search for information related to companies. The guide can be broken down into
2 sections:
1. Implementing the backend
2. Implementing the frontend
A complete example implementation of the search application demonstrating
`C1Component` usage can be found
[here](https://github.com/thesysdev/examples/tree/main/standalone-c1-component).
This guide is much easier to follow if you've already completed the
[Quickstart](/guides/setup). You can simply replace code in existing files and
create new files as required!
## Implementing the backend
For the search application, the following system prompt can be used to tailor the UI output to enable the user to search companies ask follow up questions:
```ts app/api/chat/systemPrompt.ts theme={null}
export const systemPrompt = `
You are a business research assistant just like crunchbase. You answer questions about a company or domain.
given a company name or domain, you will search the web for the latest information.
At the end of your response, add a form with single input field to ask for follow up questions.
`;
```
To learn more about system prompts and how you can use them to tailor the output of the LLM to your specific use-case, check out the [Using System Prompts](/guides/guiding-outputs) guide.
In a search application, the agent may need a tool to search the web for up-to-date information. This guide adds a web search tool powered by [Tavily](https://tavily.com) to search the web
and the packages - `zod` along with `zod-to-json-schema` to provide the schema for the tool.
To learn more about tool-calling and how you can use tools to extend the capabilities of your agent, check out the [Tool Calling](/guides/integrate-data/tool-calling) guide.
```ts app/api/chat/tools.ts [expandable] theme={null}
import { JSONSchema } from "openai/lib/jsonschema.mjs";
import { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { tavily } from "@tavily/core";
const tavilyClient = tavily({ apiKey: process.env.TAVILY_API_KEY });
export const tools: [
RunnableToolFunctionWithParse<{
searchQuery: string;
}>
] = [
{
type: "function",
function: {
name: "web_search",
description:
"Search the web for a given query, will return details about anything including business",
parse: (input) => {
return JSON.parse(input) as { searchQuery: string };
},
parameters: zodToJsonSchema(
z.object({
searchQuery: z.string().describe("search query"),
})
) as JSONSchema,
function: async ({ searchQuery }: { searchQuery: string }) => {
const results = await tavilyClient.search(searchQuery, {
maxResults: 5,
});
return JSON.stringify(results);
},
strict: true,
},
},
];
```
```ts app/api/chat/tools.js [expandable] theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { tavily } from "@tavily/core";
const tavilyClient = tavily({ apiKey: process.env.TAVILY_API_KEY });
export const tools = [
{
type: "function",
function: {
name: "web_search",
description:
"Search the web for a given query, will return details about anything including business",
parse: (input) => {
return JSON.parse(input);
},
parameters: zodToJsonSchema(
z.object({
searchQuery: z.string().describe("search query"),
})
),
function: async ({ searchQuery }) => {
const results = await tavilyClient.search(searchQuery, {
maxResults: 5,
});
return JSON.stringify(results);
},
strict: true,
},
},
];
```
For the purpose of the search application, the API may only need the search query and the previous agent response (if any) so that the LLM can generate an appropriate response.
Since the entire conversation history is not required, the API endpoint may look somewhat as follows:
```ts app/api/chat/route.ts [expandable] theme={null}
import { NextRequest } from "next/server";
import OpenAI from "openai";
import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { transformStream } from "@crayonai/stream";
import { tools } from "./tools";
import { systemPrompt } from "./systemPrompt";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
export async function POST(req: NextRequest) {
const { prompt, previousC1Response } = (await req.json()) as {
prompt: string;
previousC1Response?: string;
};
const runToolsResponse = client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
{
// Add the system prompt to provide appropriate instructions to the agent on how to generate the response and what UI constraints to consider.
role: "system",
content: systemPrompt,
},
// If there was a previous agent response, the user prompt may be a follow up question. Add the previous response
// to the messages sent to the LLM so that the agent can generate an appropriate response.
...((previousC1Response
? [{ role: "assistant", content: previousC1Response }]
: []) as ChatCompletionMessageParam[]),
{ role: "user", content: prompt },
],
stream: true,
tools: tools,
});
const llmStream = await runToolsResponse;
const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0]?.delta?.content || "";
});
return new Response(responseStream as ReadableStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```ts app/api/chat/route.js [expandable] theme={null}
import { NextRequest } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { tools } from "./tools";
import { systemPrompt } from "./systemPrompt";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
export async function POST(req) {
const { prompt, previousC1Response } = await req.json();
const runToolsResponse = client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
// Add the system prompt to provide appropriate instructions to the agent on how to generate the response and what UI constraints to consider.
{
role: "system",
content: systemPrompt,
},
// If there was a previous agent response, the user prompt may be a follow up question. Add the previous response
// to the messages sent to the LLM so that the agent can generate an appropriate response.
...(previousC1Response
? [{ role: "assistant", content: previousC1Response }]
: []),
{ role: "user", content: prompt },
],
stream: true,
tools: tools,
});
const llmStream = await runToolsResponse;
const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0]?.delta?.content || "";
});
return new Response(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
## Implementing the frontend
Unlike the `C1Chat` component, the `C1Component` leaves it up to the user to manage state to provide greater flexibility. Therefore, the frontend implementation can be broken
down into the following parts:
1. Implementing state management
2. Making the API call
3. Implementing the UI
For the search application, the following states should suffice:
* `query` - The search query entered by the user
* `c1Response` - For storing the response sent by the C1 API
* `isLoading` - For tracking when a request is in progress
* `abortController` - For managing request cancellation
The states required can depend on individual application requirements. Feel
free to make changes to the states as per your use case.
A `useUIState` hook can be used to manage the state and provide a clean interface to UI code for accessing and modifying the state. Here's an example implementation:
```ts app/home/uiState.ts [expandable] theme={null}
import { useState } from "react";
import { makeApiCall } from "./api";
/**
* Type definition for the UI state.
* Contains all the state variables needed for the application's UI.
*/
export type UIState = {
/** The current search query input */
query: string;
/** The current response from the C1 API */
c1Response: string;
/** Whether an API request is currently in progress */
isLoading: boolean;
};
/**
* Custom hook for managing the application's UI state.
* Provides a centralized way to manage state and API interactions.
*
* @returns An object containing:
* - state: Current UI state
* - actions: Functions to update state and make API calls
*/
export const useUIState = () => {
// State for managing the search query input
const [query, setQuery] = useState("");
// State for storing the API response
const [c1Response, setC1Response] = useState("");
// State for tracking if a request is in progress
const [isLoading, setIsLoading] = useState(false);
// State for managing request cancellation
const [abortController, setAbortController] =
useState(null);
/**
* Wrapper function around makeApiCall that provides necessary state handlers.
* This keeps the component interface simple while handling all state management internally.
*/
const handleApiCall = async (
searchQuery: string,
previousC1Response?: string
) => {
// makeApiCall will be implemented in the next step
await makeApiCall({
searchQuery,
previousC1Response,
setC1Response,
setIsLoading,
abortController,
setAbortController,
});
};
// Return the state and actions in a structured format
return {
state: {
query,
c1Response,
isLoading,
},
actions: {
setQuery,
setC1Response,
makeApiCall: handleApiCall,
},
};
};
```
```js app/home/uiState.js [expandable] theme={null}
import { useState } from "react";
import { makeApiCall } from "./api";
/**
* Custom hook for managing the application's UI state.
* Provides a centralized way to manage state and API interactions.
*
* @returns An object containing:
* - state: Current UI state
* - actions: Functions to update state and make API calls
*/
export const useUIState = () => {
// State for managing the search query input
const [query, setQuery] = useState("");
// State for storing the API response
const [c1Response, setC1Response] = useState("");
// State for tracking if a request is in progress
const [isLoading, setIsLoading] = useState(false);
// State for managing request cancellation
const [abortController, setAbortController] = useState(null);
/**
* Wrapper function around makeApiCall that provides necessary state handlers.
* This keeps the component interface simple while handling all state management internally.
*/
const handleApiCall = async (searchQuery, previousC1Response) => {
// makeApiCall will be implemented in the next step
await makeApiCall({
searchQuery,
previousC1Response,
setC1Response,
setIsLoading,
abortController,
setAbortController,
});
};
// Return the state and actions in a structured format
return {
state: {
query,
c1Response,
isLoading,
},
actions: {
setQuery,
setC1Response,
makeApiCall: handleApiCall,
},
};
};
```
Since the search application does not need the entire conversation history to function, it is sufficient to send the current user search query and the previous agent response (if any) to
the backend for generating a response. This ensures that if the current query is a follow up question based on the previous search result, the LLM can generate an appropriate response.
A function `makeApiCall` can be implemented as follows:
```ts app/home/api.ts [expandable] theme={null}
/**
* Type definition for parameters required by the makeApiCall function.
* This includes both the API request parameters and state management callbacks.
*/
export type ApiCallParams = {
/** The search query to be sent to the API */
searchQuery: string;
/** Optional previous response for context in follow-up queries */
previousC1Response?: string;
/** Callback to update the response state */
setC1Response: (response: string) => void;
/** Callback to update the loading state */
setIsLoading: (isLoading: boolean) => void;
/** Current abort controller for cancelling ongoing requests */
abortController: AbortController | null;
/** Callback to update the abort controller state */
setAbortController: (controller: AbortController | null) => void;
};
/**
* Makes an API call to the /api/chat endpoint with streaming response handling.
* Supports request cancellation and manages loading states.
*
* @param params - Object containing all necessary parameters and callbacks
*/
export const makeApiCall = async ({
searchQuery,
previousC1Response,
setC1Response,
setIsLoading,
abortController,
setAbortController,
}: ApiCallParams) => {
try {
// Cancel any ongoing request before starting a new one
if (abortController) {
abortController.abort();
}
// Create and set up a new abort controller for this request
const newAbortController = new AbortController();
setAbortController(newAbortController);
setIsLoading(true);
// Make the API request with the abort signal
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: searchQuery,
previousC1Response,
}),
signal: newAbortController.signal,
});
// Set up stream reading utilities
const decoder = new TextDecoder();
const stream = response.body?.getReader();
if (!stream) {
throw new Error("response.body not found");
}
// Initialize accumulator for streamed response
let streamResponse = "";
// Read the stream chunk by chunk
while (true) {
const { done, value } = await stream.read();
// Decode the chunk, considering if it's the final chunk
const chunk = decoder.decode(value, { stream: !done });
// Accumulate response and update state
streamResponse += chunk;
setC1Response(streamResponse);
// Break the loop when stream is complete
if (done) {
break;
}
}
} catch (error) {
console.error("Error in makeApiCall:", error);
} finally {
// Clean up: reset loading state and abort controller
setIsLoading(false);
setAbortController(null);
}
};
```
```js app/home/api.js [expandable] theme={null}
/**
* Makes an API call to the /api/chat endpoint with streaming response handling.
* Supports request cancellation and manages loading states.
*
* @param {Object} params - Object containing all necessary parameters and callbacks
* @param {string} params.searchQuery - The search query to be sent to the API
* @param {string} [params.previousC1Response] - Optional previous response for context in follow-up queries
* @param {Function} params.setC1Response - Callback to update the response state
* @param {Function} params.setIsLoading - Callback to update the loading state
* @param {AbortController} params.abortController - Current abort controller for cancelling ongoing requests
* @param {Function} params.setAbortController - Callback to update the abort controller state
*/
export const makeApiCall = async ({
searchQuery,
previousC1Response,
setC1Response,
setIsLoading,
abortController,
setAbortController,
}) => {
try {
// Cancel any ongoing request before starting a new one
if (abortController) {
abortController.abort();
}
// Create and set up a new abort controller for this request
const newAbortController = new AbortController();
setAbortController(newAbortController);
setIsLoading(true);
// Make the API request with the abort signal
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: searchQuery,
previousC1Response,
}),
signal: newAbortController.signal,
});
// Set up stream reading utilities
const decoder = new TextDecoder();
const stream = response.body?.getReader();
if (!stream) {
throw new Error("response.body not found");
}
// Initialize accumulator for streamed response
let streamResponse = "";
// Read the stream chunk by chunk
while (true) {
const { done, value } = await stream.read();
// Decode the chunk, considering if it's the final chunk
const chunk = decoder.decode(value, { stream: !done });
// Accumulate response and update state
streamResponse += chunk;
setC1Response(streamResponse);
// Break the loop when stream is complete
if (done) {
break;
}
}
} catch (error) {
console.error("Error in makeApiCall:", error);
} finally {
// Clean up: reset loading state and abort controller
setIsLoading(false);
setAbortController(null);
}
};
```
For a search application, the following components are required:
* A search query input box
* A "Submit" button to initiate the search
* `C1Component` for rendering the Generative UI response.
You will also need to wrap the `C1Component` in a `ThemeProvider` to ensure that the rendered components are styled correctly.
Here's an example code block implementing the entire UI, with the important sections highlighted:
```tsx app/home/page.tsx [expandable] {42-53} theme={null}
"use client";
import "@crayonai/react-ui/styles/index.css";
import { ThemeProvider, C1Component } from "@thesysai/genui-sdk";
import { useUIState } from "./uiState";
import { Loader } from "./Loader";
export const HomePage = () => {
const { state, actions } = useUIState();
return (
actions.setQuery(value)}
onKeyDown={(e) => {
// make api call only when response loading is not in progress
if (e.key === "Enter" && !state.isLoading) {
actions.makeApiCall(state.query);
}
}}
/>
);
};
```
```ts app/home/Loader.tsx theme={null}
export const Loader = () => {
return (
);
};
```
That's it! Your search app with `C1Component` is now complete. Try running it and search for a few companies on the `/home` route!
# Example Applications
Source: https://docs.thesys.dev/examples/index
End-to-end tutorials showing how to build production-ready AI applications with Thesys C1
Learn by building. Each tutorial walks you through creating a complete, production-ready application from scratch. Follow the setup instructions to get an MVP running locally, then [deploy to production](/guides/deployment/introduction).
New to Thesys? Start with [What is Thesys C1](/guides/what-is-thesys-c1) to understand the core concepts before diving into examples.
## Featured Examples
Build a Perplexity-style search app with Exa or Google Gemini. Generate rich, visual UI instead of plain text results. Includes multi-provider search, image integration, and conversation threads.
**What you'll learn:** [Tool calling](/guides/integrate-data/tool-calling), search APIs, [streaming](/guides/streaming), [C1Chat](/react-reference/c1-chat)
Create an AI copilot for financial analysis. Query stocks, crypto, and company financials in natural language. Get automatic charts, tables, and insights.
**What you'll learn:** Financial APIs, [data visualization](/guides/styling/charts), [tool calling](/guides/integrate-data/tool-calling), [thread management](/guides/conversational/persistence)
Build an infinite canvas with AI-generated visual cards. Select cards for context-aware follow-ups. Perfect for brainstorming, planning, and research.
**What you'll learn:** tldraw integration, [custom components](/guides/custom-components), context extraction, keyboard shortcuts
Create a conversational AI app with rich UI responses. The simplest way to get started with C1Chat and generative UI.
**What you'll learn:** [C1Chat basics](/react-reference/c1-chat), [streaming](/guides/streaming), [message history](/guides/conversational/persistence)
## More Examples
Multi-step research agent that gathers company information from multiple sources and synthesizes it into comprehensive reports.
**What you'll learn:** Multi-step workflows, web scraping, data synthesis
## What Makes These Examples Different
These aren't code snippets or minimal demos. Each tutorial:
* **Starts from zero**: Create a new Next.js project and build everything from scratch
* **Runs locally**: Follow [setup steps](/guides/setup) to get a working MVP on your machine
* **Production-ready**: Includes error handling, rate limiting, and [deployment guidance](/guides/deployment/introduction)
* **Teaches architecture**: Understand why decisions are made, not just how to copy code
* **Full source code**: Every example has a GitHub repo you can clone and explore
## Choose Your Path
Start with [AI Search Engine](/examples/search-with-c1). You'll learn how to integrate search APIs, process results, and generate contextual UI. Then check out [Company Research](/examples/company-research) for multi-step workflows.
Start with [Financial Data Copilot](/examples/analytics-with-c1). You'll learn how to connect APIs, visualize data, and create conversational interfaces for data exploration.
Start with [AI Canvas](/examples/canvas-with-c1). You'll learn how to integrate tldraw, create custom shapes, and build context-aware systems.
Start with [ChatGPT-like Interface](/examples/like-chatgpt). It's the simplest example that introduces core concepts like C1Chat, streaming, and message history.
## Common Patterns Across Examples
All examples demonstrate these core concepts:
* **[Tool Calling](/guides/integrate-data/tool-calling)**: Let LLMs decide when and how to call external APIs
* **[Streaming](/guides/streaming)**: Real-time response generation with progress indicators
* **[C1Chat](/react-reference/c1-chat)**: Pre-built conversational UI that handles complexity for you
* **[Thread Management](/guides/conversational/persistence)**: Maintain conversation context for follow-up questions
* **[System Prompts](/guides/guiding-outputs)**: Guide LLMs to generate specific types of UI
## Need Help?
* Read [How C1 Works](/guides/how-c1-works) to understand the fundamentals
* Follow the [Getting Started guide](/guides/setup) to set up your environment
* Join our [Discord community](https://discord.gg/Pbv5PsqUSv) to ask questions
* Check the [full documentation](/guides/what-is-thesys-c1) for detailed guides
* Explore [GitHub repos](https://github.com/thesysdev) for complete source code
## Contributing
Have an idea for a new example? Open an issue or PR on [GitHub](https://github.com/thesysdev/examples). We're especially interested in:
* [Integration with other frameworks](/guides/frameworks/index) (LangChain, CrewAI, AutoGen)
* Domain-specific applications (legal, medical, education)
* Novel UI patterns ([collaborative editing](/guides/conversational/sharing/thread), real-time updates)
* Different tech stacks (Python/FastAPI, Go, Rust)
# Build your own ChatGPT-like application
Source: https://docs.thesys.dev/examples/like-chatgpt
Integrate images via tool calling to bring your chatbot to life
The [quickstart guide](/guides/setup) walks you through building a functional agent. However, we can go further and add more features to it. This guide demonstrates how you can
utilize tool calling to add advanced features to the agent.
Let's assume you want to build an agent that can also integrate images into its responses.
If you have followed the quickstart guide with C1Chat, your backend endpoint may look somewhat like this:
```ts app/api/chat/route.ts [expandable] theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
export async function POST(req: NextRequest) {
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam & { id: string };
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
const responseStream = transformStream(
llmStream,
(chunk) => {
return chunk.choices[0].delta.content;
},
{
onEnd: ({ accumulated }) => {
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream;
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```js app/api/chat/route.js [expandable] theme={null}
import { NextResponse } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
export async function POST(req) {
const { prompt, threadId, responseId } = await req.json();
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
const responseStream = transformStream(
llmStream,
(chunk) => {
return chunk.choices[0].delta.content;
},
{
onEnd: ({ accumulated }) => {
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
);
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
To make the agent integrate images into its responses, you would need to:
* Tell the agent to use images in its responses
* Tell the agent how to get the images to be used in the response
The first step is to tell the agent to use images in its responses, since C1 does not do this by default. You can do this by adding a [system / developer prompt](https://platform.openai.com/docs/guides/prompt-engineering#messages-and-roles).
This is also how you can customize the tone and behaviour of the agent.
```ts app/api/chat/systemPrompt.ts theme={null}
export const systemPrompt = `
You are a helpful and friendly AI assistant. Here are some rules you must follow:
Rules:
- Include images in your responses wherever they can make the responses more visually appealing or helpful.
- The images must be from the 'getImageSrc' tool. Pass the alt text of the image to the 'getImageSrc' tool to get an image src.
`;
```
If you have followed the quickstart guide, you would have a message history store that persists the conversation state. You can add a system prompt to
each new thread as follows:
```ts app/api/chat/messageStore.ts {14} theme={null}
import OpenAI from "openai";
import { systemPrompt } from "./systemPrompt";
export type DBMessage = OpenAI.Chat.ChatCompletionMessageParam & {
id?: string;
};
const messagesStore: {
[threadId: string]: DBMessage[];
} = {};
export const getMessageStore = (id: string) => {
if (!messagesStore[id]) {
messagesStore[id] = [{ role: "system", content: systemPrompt }];
}
const messageList = messagesStore[id];
return {
addMessage: (message: DBMessage) => {
messageList.push(message);
},
messageList,
getOpenAICompatibleMessageList: () => {
return messageList.map((m) => {
const message = {
...m,
};
delete message.id;
return message;
});
},
};
};
```
```js app/api/chat/messageStore.js {7} theme={null}
import OpenAI from "openai";
import { systemPrompt } from "./systemPrompt";
const messagesStore = {};
export const getMessageStore = (id) => {
if (!messagesStore[id]) {
messagesStore[id] = [{ role: "system", content: systemPrompt }];
}
const messageList = messagesStore[id];
return {
addMessage: (message) => {
messageList.push(message);
},
messageList,
getOpenAICompatibleMessageList: () => {
return messageList.map((m) => {
const message = {
...m,
};
delete message.id;
return message;
});
},
};
};
```
Next, add a tool to the agent that it can call to fetch an image url for the response. This example uses the Google Custom Search API to fetch an image url. See
[google-images](https://www.npmjs.com/package/google-images) package documentation and [Google Custom Search](https://developers.google.com/custom-search/v1/overview) documentation
for more details.
For detailed information on how to use tools, see [Function
Calling](https://platform.openai.com/docs/guides/function-calling).
First, define the tool:
```ts app/api/chat/tools.ts theme={null}
import type { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
import type { RunnableToolFunctionWithoutParse } from "openai/lib/RunnableFunction.mjs";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import GoogleImages from "google-images";
const client = new GoogleImages(
process.env.GOOGLE_CSE_ID,
process.env.GOOGLE_API_KEY
);
export const tools: (
| RunnableToolFunctionWithoutParse
| RunnableToolFunctionWithParse
)[] = [
{
type: "function",
function: {
name: "getImageSrc",
description: "Get the image src for the given alt text",
parse: JSON.parse,
parameters: zodToJsonSchema(
z.object({
altText: z.string().describe("The alt text of the image"),
})
) as any,
function: async ({ altText }: { altText: string }) => {
const results = await client.search(altText, {
size: "medium",
});
return results[0].url;
},
strict: true,
},
},
];
```
```ts app/api/chat/tools.js theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import GoogleImages from "google-images";
const client = new GoogleImages(
process.env.GOOGLE_CSE_ID,
process.env.GOOGLE_API_KEY
);
export const tools = [
{
type: "function",
function: {
name: "getImageSrc",
description: "Get the image src for the given alt text",
parse: JSON.parse,
parameters: zodToJsonSchema(
z.object({
altText: z.string().describe("The alt text of the image"),
})
),
function: async ({ altText }) => {
const results = await client.search(altText, {
size: "medium",
});
return results[0].url;
},
strict: true,
},
},
];
```
Now you can add the tool to the agent and handle the tool call. The OpenAI SDK provides a `runTools` method for convenient implementation of tool calling.
Additionally, you can use the `message` event to add the tool call messages along with the assistant response to the message history:
```ts app/api/chat/route.ts {11, 14-16} theme={null}
// ... other imports
import { tools } from "./tools";
export async function POST(req: NextRequest) {
// ... rest of your endpoint code
const runToolsResponse = client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
tools,
});
runToolsResponse.on("message", (event) => {
messageStore.addMessage(event);
});
const llmStream = await runToolsResponse;
// Unwrap the OpenAI stream to a C1 stream
const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0].delta.content;
}) as ReadableStream;
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```js app/api/chat/route.js {11, 14-16} theme={null}
// ... other imports
import { tools } from "./tools";
export async function POST(req) {
// ... rest of your endpoint code
const runToolsResponse = client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
tools,
});
runToolsResponse.on("message", (event) => {
messageStore.addMessage(event);
});
const llmStream = await runToolsResponse;
// Unwrap the OpenAI stream to a C1 stream
const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0].delta.content;
});
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
C1 should now integrate images into its responses:
# Build an AI Search Engine
Source: https://docs.thesys.dev/examples/search-with-c1
Learn how to build a Perplexity-style search app with generative UI using Thesys C1
Build your own AI-powered search engine that generates beautiful, interactive UI instead of just returning links. This guide teaches you the architecture and techniques behind apps like Perplexity and Google AI Search.
**Try it above** or visit [search-with-c1.vercel.app](https://search-with-c1.vercel.app)
## What You'll Learn
* How to connect C1 with search APIs using tool calling
* Building multi-provider search (Exa neural search + Google Gemini)
* Crafting system prompts for rich visual outputs
* Streaming search results in real-time
* Setting up C1Chat for conversational search UI
* Thread management for follow-up questions
## Architecture Overview
Modern AI search apps follow this pattern:
```
User Query → LLM (with search tools) → Search APIs → LLM processes results → C1 generates UI → Stream to user
```
The key innovation: instead of returning raw search results, we let C1 generate contextual, visual UI based on what the user searched for.
## Setup
### Prerequisites
* Node.js 18+
* Thesys API key from [console.thesys.dev](https://console.thesys.dev)
* Choose one search provider:
* **Exa API key** from [exa.ai](https://exa.ai) (recommended for neural search)
* **Google Gemini API key** from [ai.google.dev](https://ai.google.dev)
* (Optional) Google Custom Search API key and CSE ID for image search
### Create Next.js Project
```bash npm theme={null}
npx create-next-app@latest search-with-c1
cd search-with-c1
```
```bash pnpm theme={null}
pnpm create next-app search-with-c1
cd search-with-c1
```
```bash yarn theme={null}
yarn create next-app search-with-c1
cd search-with-c1
```
When prompted, select:
* TypeScript: Yes
* ESLint: Yes
* Tailwind CSS: Yes
* App Router: Yes
* Customize default import alias: No
### Install Dependencies
```bash npm theme={null}
npm install @thesysai/genui-sdk @crayonai/react-ui openai exa-js
```
```bash pnpm theme={null}
pnpm add @thesysai/genui-sdk @crayonai/react-ui openai exa-js
```
```bash yarn theme={null}
yarn add @thesysai/genui-sdk @crayonai/react-ui openai exa-js
```
If using Google Gemini instead of Exa:
```bash npm theme={null}
npm install @google/generative-ai
```
```bash pnpm theme={null}
pnpm add @google/generative-ai
```
```bash yarn theme={null}
yarn add @google/generative-ai
```
Optional (for image search):
```bash npm theme={null}
npm install google-images
```
```bash pnpm theme={null}
pnpm add google-images
```
```bash yarn theme={null}
yarn add google-images
```
### Environment Variables
Create a `.env.local` file:
```bash theme={null}
THESYS_API_KEY=your_thesys_api_key
# Choose one search provider
EXA_API_KEY=your_exa_api_key
# OR
GOOGLE_GEMINI_API_KEY=your_gemini_api_key
# Optional: For image search
GOOGLE_API_KEY=your_google_api_key
GOOGLE_CSE_ID=your_custom_search_engine_id
```
We recommend Exa for better neural search results. Get your API key from [exa.ai](https://exa.ai).
## Step 1: Set Up Search Tools
C1 uses OpenAI's tool calling to trigger searches. Here's how to define a search tool:
```typescript Next.js theme={null}
const createWebSearchTool = (searchProvider, c1Response) => [{
type: "function",
function: {
name: "webSearch",
description: "Search the web to get high-quality results with full content",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query to perform",
},
},
required: ["query"],
},
function: async (args: { query: string }) => {
// Update user on search progress
c1Response.writeThinkItem({
title: "Searching the web",
description: "Retrieving relevant information...",
});
// Call your search provider (Exa or Gemini)
const searchResult = await exaSearch(args.query);
return JSON.stringify(searchResult);
},
},
}];
```
```python Python/FastAPI theme={null}
from typing import Dict, Any
import json
def create_web_search_tool(search_provider, c1_response):
async def web_search(query: str) -> str:
# Update user on search progress
await c1_response.write_think_item({
"title": "Searching the web",
"description": "Retrieving relevant information...",
})
# Call your search provider
search_result = await exa_search(query)
return json.dumps(search_result)
return [{
"type": "function",
"function": {
"name": "webSearch",
"description": "Search the web to get high-quality results",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to perform",
},
},
"required": ["query"],
},
},
}]
```
The `writeThinkItem` calls show users what's happening while search runs in the background. This creates a better UX than silent loading.
## Step 2: Implement Search Providers
You have two options for search:
### Option A: Exa Neural Search
Exa provides AI-powered search with full page content extraction:
```typescript exa-search.ts theme={null}
import Exa from "exa-js";
const exa = new Exa(process.env.EXA_API_KEY);
export const exaSearch = async (query: string) => {
// Search and get full content in one call
const response = await exa.searchAndContents(query, {
numResults: 5,
text: true, // Get full page content
highlights: true, // Get key snippets
type: "auto", // Let Exa choose search mode
});
return {
results: response.results.map(r => ({
title: r.title,
url: r.url,
content: r.text, // Full page text for LLM
snippet: r.highlights?.[0] || r.text?.slice(0, 200),
})),
searchQuery: query,
};
};
```
```python exa_search.py theme={null}
from exa_py import Exa
exa = Exa(api_key=os.environ["EXA_API_KEY"])
async def exa_search(query: str):
# Search and get full content
response = await exa.search_and_contents(
query,
num_results=5,
text=True, # Get full page content
highlights=True, # Get key snippets
type="auto", # Let Exa choose search mode
)
return {
"results": [
{
"title": r.title,
"url": r.url,
"content": r.text,
"snippet": r.highlights[0] if r.highlights else r.text[:200],
}
for r in response.results
],
"searchQuery": query,
}
```
### Option B: Google Gemini with Grounding
Gemini 2.5 has built-in Google Search grounding:
```typescript gemini-search.ts theme={null}
import { GoogleGenAI } from "@google/genai";
const genAI = new GoogleGenAI({
apiKey: process.env.GEMINI_API_KEY,
});
export const googleGenAISearch = async (query: string) => {
const response = await genAI.models.generateContentStream({
model: "gemini-2.5-flash",
contents: [{ role: "user", parts: [{ text: query }] }],
config: {
tools: [{ googleSearch: {} }], // Enable search grounding
thinkingConfig: {
includeThoughts: true, // Stream thinking process
},
},
});
let result = "";
for await (const chunk of response) {
if (chunk.text) result += chunk.text;
}
return result;
};
```
```python gemini_search.py theme={null}
import google.generativeai as genai
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
async def google_genai_search(query: str):
model = genai.GenerativeModel("gemini-2.5-flash")
response = await model.generate_content_async(
query,
tools=["google_search_retrieval"], # Enable search grounding
)
return response.text
```
## Step 3: Create the Main API Endpoint
Now connect everything with C1:
```typescript app/api/ask/route.ts theme={null}
import { makeC1Response } from "@thesysai/genui-sdk/server";
import { transformStream } from "@crayonai/stream";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
export async function POST(req: NextRequest) {
const { prompt, searchProvider } = await req.json();
const c1Response = makeC1Response();
// Create search tool
const tools = createWebSearchTool(searchProvider, c1Response);
// Call C1 with automatic tool execution
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20250915",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: prompt },
],
tools,
stream: true,
});
// Stream response to user
transformStream(llmStream, (chunk) => {
const content = chunk.choices[0]?.delta?.content || "";
if (content) c1Response.writeContent(content);
return content;
}, {
onEnd: () => c1Response.end(),
});
return new Response(c1Response.responseStream, {
headers: { "Content-Type": "text/event-stream" },
});
}
```
```python main.py theme={null}
from fastapi import FastAPI
from openai import OpenAI
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content
client = OpenAI(
api_key=os.environ["THESYS_API_KEY"],
base_url="https://api.thesys.dev/v1/embed",
)
@app.post("/api/ask")
@with_c1_response()
async def ask(request: AskRequest):
# Create search tool
tools = create_web_search_tool(request.searchProvider, c1_response)
# Call C1 with tool execution
stream = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250915",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": request.prompt},
],
tools=tools,
stream=True,
)
# Stream response
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
```
## Step 4: Craft the Perfect System Prompt
The system prompt determines how C1 generates UI. Here's a proven pattern for search apps:
```typescript theme={null}
const SYSTEM_PROMPT = `You are a visual search AI assistant. Your mission is to provide visually-rich answers.
Today is ${new Date().toLocaleDateString()}.
**Core Directives:**
1. **Always Search First:** Use your web search tool for every query to get current information.
2. **Be Visual:** For any visual topic (places, products, people), MUST use images.
- Use Image component for single images
- Use ImageGallery for multiple related images
- Add images to every ListItem when showing visual things
3. **Visualize Data:** Use tables and charts for statistics, comparisons, and numbers.
4. **Structure Content:** Use headings, lists, and sections to organize information clearly.
5. **Add Follow-ups:** Always include 2-4 follow-up questions to continue the conversation.
**Image Rules:**
- Leave src/imagesSrc/imageSrc fields EMPTY (images are added automatically)
- Provide detailed alt text describing what should be shown
- Examples: "Eiffel Tower illuminated at night", "Graph showing Tesla stock price"
Remember: A great search response is a visual response.`;
```
The key is being specific about **when** to use visual components and **how** to structure them. Vague prompts lead to inconsistent results.
## Step 5: Enable Image Search
C1 generates image components, but needs actual image URLs to display them. Create an image search endpoint that C1 can call:
```typescript app/api/search/image/route.ts theme={null}
import axios from "axios";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { query } = await req.json();
try {
// Use Google Custom Search API for images
const response = await axios.get(
"https://www.googleapis.com/customsearch/v1",
{
params: {
q: query,
searchType: "image",
key: process.env.GOOGLE_API_KEY,
cx: process.env.GOOGLE_CX, // Custom Search Engine ID
num: 1, // Just need one image
safe: "active", // Safe search
imgSize: "large", // High quality
},
}
);
if (!response.data.items || response.data.items.length === 0) {
return NextResponse.json({ url: null, thumbnailUrl: null });
}
return NextResponse.json({
url: response.data.items[0].link,
thumbnailUrl: response.data.items[0].image.thumbnailLink,
});
} catch (error) {
// Return placeholder on error
return NextResponse.json({
url: "https://via.placeholder.com/360x240",
thumbnailUrl: "https://via.placeholder.com/360x240",
});
}
}
```
```python app/api/search/image.py theme={null}
import os
import httpx
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
class ImageSearchRequest(BaseModel):
query: str
@router.post("/api/search/image")
async def search_image(request: ImageSearchRequest):
try:
async with httpx.AsyncClient() as client:
# Use Google Custom Search API for images
response = await client.get(
"https://www.googleapis.com/customsearch/v1",
params={
"q": request.query,
"searchType": "image",
"key": os.environ["GOOGLE_API_KEY"],
"cx": os.environ["GOOGLE_CX"],
"num": 1,
"safe": "active",
"imgSize": "large",
},
)
data = response.json()
if not data.get("items"):
return {"url": None, "thumbnailUrl": None}
item = data["items"][0]
return {
"url": item["link"],
"thumbnailUrl": item["image"]["thumbnailLink"],
}
except Exception:
return {
"url": "https://via.placeholder.com/360x240",
"thumbnailUrl": "https://via.placeholder.com/360x240",
}
```
### How Image Search Works
The C1 SDK handles images automatically through a `searchImage` callback:
```tsx Frontend (React) theme={null}
import { C1Component } from "@thesysai/genui-sdk";
// Your image search function (calls your backend)
const imageSearch = async (query: string) => {
const response = await fetch("/api/search/image", {
method: "POST",
body: JSON.stringify({ query }),
});
return response.json(); // { url: "...", thumbnailUrl: "..." }
};
// Pass searchImage to C1Component
```
```python Frontend Integration theme={null}
# For Python backends, the frontend still uses React
# The searchImage prop works the same way regardless of backend language
```
**The flow:**
1. **Backend C1 generates** image components with descriptive alt text (e.g., `alt="Eiffel Tower at sunset"`)
2. **C1 SDK detects** images with empty `src` attributes
3. **SDK automatically calls** your `searchImage(altText)` function
4. **Your function fetches** the actual image URL from your `/api/search/image` endpoint
5. **SDK updates** the component with the real image URL
You don't need to manually handle image fetching - just provide the `searchImage` callback to `C1Component`.
The `searchImage` callback gives you flexibility: use Google Images, Unsplash, Pexels, or your own image CDN. The SDK just needs a function that takes a query string and returns `{ url, thumbnailUrl }`.
## Step 6: Add Caching (Optional but Recommended)
Cache responses to avoid re-searching identical queries:
```typescript cache.ts theme={null}
const cache = new Map();
const CACHE_TTL = 1000 * 60 * 60; // 1 hour
export const findCachedResponse = (query: string) => {
const cached = cache.get(query);
if (!cached) return null;
// Check if cache expired
if (Date.now() - cached.timestamp > CACHE_TTL) {
cache.delete(query);
return null;
}
return cached.response;
};
export const cacheResponse = (query: string, response: string) => {
cache.set(query, { response, timestamp: Date.now() });
};
```
```python cache.py theme={null}
from datetime import datetime, timedelta
from typing import Dict, Optional
cache: Dict[str, dict] = {}
CACHE_TTL = timedelta(hours=1)
def find_cached_response(query: str) -> Optional[str]:
cached = cache.get(query)
if not cached:
return None
# Check if expired
if datetime.now() - cached["timestamp"] > CACHE_TTL:
del cache[query]
return None
return cached["response"]
def cache_response(query: str, response: str):
cache[query] = {
"response": response,
"timestamp": datetime.now(),
}
```
## Step 7: Add Thread Management for Follow-ups (Optional)
To enable follow-up questions that reference previous searches, implement thread management:
```typescript thread-cache.ts theme={null}
interface ThreadMessage {
role: "user" | "assistant";
prompt?: string; // For user messages
c1Response?: string; // For assistant messages
timestamp: string;
}
// In-memory thread storage (use Redis for production)
const threads = new Map();
export const getThread = async (threadId: string) => {
return threads.get(threadId) || [];
};
export const addUserMessage = async (threadId: string, prompt: string) => {
const thread = threads.get(threadId) || [];
thread.push({
role: "user",
prompt,
timestamp: new Date().toISOString(),
});
threads.set(threadId, thread);
};
export const addAssistantMessage = async (
threadId: string,
c1Response: string
) => {
const thread = threads.get(threadId) || [];
thread.push({
role: "assistant",
c1Response,
timestamp: new Date().toISOString(),
});
threads.set(threadId, thread);
};
```
```python thread_cache.py theme={null}
from datetime import datetime
from typing import List, Dict, Any, Optional
# In-memory thread storage (use Redis for production)
threads: Dict[str, List[Dict[str, Any]]] = {}
async def get_thread(thread_id: str) -> List[Dict[str, Any]]:
return threads.get(thread_id, [])
async def add_user_message(thread_id: str, prompt: str):
thread = threads.get(thread_id, [])
thread.append({
"role": "user",
"prompt": prompt,
"timestamp": datetime.now().isoformat(),
})
threads[thread_id] = thread
async def add_assistant_message(thread_id: str, c1_response: str):
thread = threads.get(thread_id, [])
thread.append({
"role": "assistant",
"c1Response": c1_response,
"timestamp": datetime.now().isoformat(),
})
threads[thread_id] = thread
```
### Using Thread History
Update your main endpoint to include thread history:
```typescript With thread context theme={null}
export async function POST(req: NextRequest) {
const { prompt, threadId } = await req.json();
const c1Response = makeC1Response();
// Get conversation history
const threadHistory = await getThread(threadId);
// Save user message
await addUserMessage(threadId, prompt);
// Build messages with history
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
// Add previous conversation
...threadHistory.map(msg => ({
role: msg.role,
content: msg.role === "user" ? msg.prompt : msg.c1Response,
})),
// Add current prompt
{ role: "user", content: prompt },
];
// Call C1 with context
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20250915",
messages,
tools,
stream: true,
});
let finalResponse = "";
transformStream(llmStream, (chunk) => {
const content = chunk.choices[0]?.delta?.content || "";
if (content) {
finalResponse += content;
c1Response.writeContent(content);
}
return content;
}, {
onEnd: async () => {
// Save assistant response
await addAssistantMessage(threadId, finalResponse);
c1Response.end();
},
});
return new Response(c1Response.responseStream, {
headers: { "Content-Type": "text/event-stream" },
});
}
```
```python With thread context theme={null}
@app.post("/api/ask")
@with_c1_response()
async def ask(request: AskRequest):
# Get conversation history
thread_history = await get_thread(request.threadId)
# Save user message
await add_user_message(request.threadId, request.prompt)
# Build messages with history
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
# Add previous conversation
*[
{
"role": msg["role"],
"content": msg.get("prompt") if msg["role"] == "user" else msg.get("c1Response"),
}
for msg in thread_history
],
# Add current prompt
{"role": "user", "content": request.prompt},
]
# Call C1 with context
stream = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250915",
messages=messages,
tools=tools,
stream=True,
)
final_response = ""
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
final_response += content
await write_content(content)
# Save assistant response
await add_assistant_message(request.threadId, final_response)
```
### Why Thread Management Matters
With threads, users can ask follow-up questions:
```
User: "What are the best restaurants in Tokyo?"
AI: [Shows image gallery + list of restaurants]
User: "Which one has the best sushi?"
AI: [References previous results, shows specific sushi restaurants]
```
Without threads, the second question would fail because the AI has no context from the first search.
**Production tip:** Use Redis or a database for thread storage instead of in-memory. In-memory storage is lost when your server restarts. The search-with-c1 repo includes Redis integration examples.
## Step 8: Set Up the Frontend UI
Now create the conversational search interface using C1Chat:
```typescript app/page.tsx theme={null}
"use client";
import { C1Chat } from "@crayonai/react-ui";
import "@crayonai/react-ui/styles/index.css";
import { useState } from "react";
import { searchImage } from "./utils/searchImage";
export default function Page() {
const [threadId] = useState(() => crypto.randomUUID());
return (
);
}
```
C1Chat provides the complete conversational UI out of the box, including:
* Message history
* Streaming responses
* Thinking states
* Automatic thread management
* Image search integration
### Image Search Integration
Create the image search handler (if you set up image search in Step 5):
```typescript app/utils/searchImage.ts theme={null}
export async function searchImage(query: string): Promise {
const response = await fetch("/api/search-image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const data = await response.json();
return data.imageUrl;
}
```
Create the image search API endpoint:
```typescript app/api/search-image/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import GoogleImages from "google-images";
const client = new GoogleImages(
process.env.GOOGLE_CSE_ID!,
process.env.GOOGLE_API_KEY!
);
export async function POST(req: NextRequest) {
const { query } = await req.json();
try {
const results = await client.search(query, { size: "huge" });
return NextResponse.json({ imageUrl: results[0]?.url || "" });
} catch (error) {
console.error("Image search failed:", error);
return NextResponse.json({ imageUrl: "" });
}
}
```
If you skipped Step 5 (image search), simply omit the `searchImage` prop from C1Chat.
## Step 9: Run Your Search App
Start the development server:
```bash npm theme={null}
npm run dev
```
```bash pnpm theme={null}
pnpm dev
```
```bash yarn theme={null}
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) and try these searches:
1. **"Best restaurants in Tokyo"** - See visual results with images
2. **"How does quantum computing work?"** - Get structured explanations
3. Follow up with **"What are the main applications?"** - Test thread continuity
If you're using Exa, the first search might be slower as it fetches full page content. Subsequent searches will be faster with caching enabled.
## Key Concepts
Tool calling lets the LLM decide **when** to search and **what** to search for. The LLM might reformulate the query, do multiple searches, or skip searching if it has enough context from conversation history.
* **Exa**: Best for deep content analysis. Returns full page text for the LLM to process.
* **Gemini**: Faster and cheaper. Built-in search grounding with automatic result synthesis.
Many apps let users choose (see the live demo).
C1 analyzes the content + your system prompt. If content contains images, lists, or data, and your prompt encourages visual components, C1 will generate appropriate UI. The better your prompt, the better the UI.
Yes! Create additional tools for databases, APIs, or documents. C1 can combine web search with your private data.
## Going to Production
Before deploying:
1. **Add rate limiting** to prevent API abuse
2. **Implement proper error handling** for failed searches
3. **Set up monitoring** for API costs and performance
4. **Add user authentication** if needed
5. **Enable caching** to reduce API calls
## Full Example & Source Code
Experience the complete AI search app in action. Search for anything and see C1 generate beautiful, contextual UI in real-time.
**Try it now →**
Complete implementation with thread management, error handling, caching, and deployment config. Everything from this guide and more.
**Star on GitHub →**
# Editing an Artifact in a Conversation
Source: https://docs.thesys.dev/guides/artifacts/artifacts-in-chat/editing
A step-by-step tutorial on using a tool call to edit an existing Artifact and embed it in an assistant's response.
A key feature of conversational artifacts is the ability for users to modify them with follow-up prompts. This guide explains how to handle a user request like "add a slide about our competitors" by using a tool to edit an existing artifact.
This guide assumes you have already completed the **[Generating an Artifact in a Conversation](/guides/artifacts/artifacts-in-chat/generating)** guide.
### Step 1: Define the `edit_artifact` Tool
First, define the schema for a tool that allows the assistant to edit an artifact. The schema must include parameters to identify which artifact to edit (`artifactId` and `version`) and what changes to make (`instructions`).
```typescript theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
const editPresentationTool = {
type: "function",
function: {
name: "edit_presentation",
description: "Edits an existing slide presentation.",
parameters: zodToJsonSchema(
z.object({
artifactId: z.string().describe("The ID of the artifact to edit."),
version: z.string().describe("The version of the artifact to edit"), // this corresponds to the messageId of the assistant's response that contains the artifact
instructions: z.string().describe("The user's instructions for what to change."),
})
),
},
};
```
```python theme={null}
from pydantic import BaseModel, Field
class EditPresentationParams(BaseModel):
artifactId: str = Field(..., description="The ID of the artifact to edit.")
version: str = Field(..., description="The version of the artifact to edit") # this corresponds to the messageId of the assistant's response that contains the artifact
instructions: str = Field(..., description="The user's instructions for what to change.")
edit_presentation_tool = {
"type": "function",
"function": {
"name": "edit_presentation",
"description": "Edits an existing slide presentation.",
"parameters": EditPresentationParams.model_json_schema(),
},
}
```
### Step 2: The Role of the `version`
In our workflow, the `version` is the unique `messageId` of the assistant's response that contains the artifact.
Using the `messageId` as a version provides a **stable reference** to a specific, point-in-time snapshot of the artifact as it exists in the conversation history. When the LLM requests an edit, your backend can use this `version` to reliably retrieve the exact content that needs to be modified.
### Step 3: Handle the Tool Call in Your Backend
When a user asks to make a change, the LLM will use your `edit_presentation` tool. Your backend must handle this tool call by retrieving the old content, calling the C1 Artifacts API in "edit mode," and streaming back the updated result.
The key steps are:
1. Use the `version` from the tool call to fetch the previous assistant message content from your database.
2. Generate a new `messageId` which will serve as the **new version** of the artifact.
3. Call the C1 Artifacts API, providing the old content and the new editing instructions.
4. Stream the updated artifact into a C1 Response object.
5. Return a `tool_result` to the main LLM with the new version.
6. Stream the LLM's final text confirmation into the same C1 Response object.
```typescript theme={null}
// (Inside your Next.js API route)
import { makeC1Response } from "@thesysai/genui-sdk/server";
// When your edit tool handler is invoked...
async function handleEditPresentation(
{ artifactId, version, instructions }: { artifactId: string, version: string, instructions: string },
{ messageId, c1Response }: { messageId: string, c1Response: ReturnType }
) {
// 1. Retrieve the old artifact content from your database using the version (messageId).
const oldMessageContent = await getMessageFromDb(version);
// 3. Call the Artifacts API in "edit mode".
const updatedArtifactStream = await c1ArtifactsClient.chat.completions.create({
model: "c1/artifact/v-20251030",
messages: [
{ role: "assistant", content: oldMessageContent }, // Old content
{ role: "user", content: instructions }, // New instructions
],
metadata: { thesys: JSON.stringify({ c1_artifact_type: "slides", id: artifactId }) },
stream: true,
});
// 4. Pipe the updated artifact stream into the C1 Response object.
for await (const delta of updatedArtifactStream) {
const content = delta.choices[0]?.delta?.content;
if (content) {
c1Response.writeContent(content);
}
}
// 5. Return the result to the main LLM with the new version.
return `Presentation edited successfully. New version: ${messageId}`
}
```
```python theme={null}
# (Inside your FastAPI route)
from thesys_genui_sdk.context import write_content
# for more details on how to use thesys_genui_sdk, see https://pypi.org/project/thesys-genui-sdk/.
# When your edit tool handler is invoked...
async def handle_edit_presentation(artifactId: str, version: str, instructions: str, message_id: str):
# 1. Retrieve the old artifact content from your database using the version (messageId).
old_message_content = await get_message_from_db(version)
# 2. Call the Artifacts API in "edit mode".
updated_artifact_stream = await c1_artifacts_client.chat.completions.create(
model="c1/artifact/v-20251030",
messages=[
{"role": "assistant", "content": old_message_content}, # Old content
{"role": "user", "content": instructions}, # New instructions
],
metadata={"thesys": json.dumps({"c1_artifact_type": "slides", "id": artifactId})},
stream=True,
)
# 3. Pipe the updated artifact stream.
async for delta in updated_artifact_stream:
content = delta.choices[0].delta.content
if content:
await write_content(content)
# 4. Return the result to the main LLM with the new version.
return f"Presentation edited successfully. New version: {message_id}"
```
### Step 4: The Final Updated Response
As in the creation step, the main LLM will receive the successful `tool_result` and generate a final confirmation (e.g., "I've updated the presentation with the new slide."). Your backend streams this text into the same C1 Response object, completing the request. The frontend then receives and renders the new assistant message containing the fully updated artifact.
Find more examples and complete code on our GitHub repository.
# Generating an Artifact in a Conversation
Source: https://docs.thesys.dev/guides/artifacts/artifacts-in-chat/generating
A step-by-step tutorial on using a tool call to create a new Artifact and embed it in an assistant's response.
To enable an assistant to create an artifact, you must provide it with a tool that it can call in response to a user's request. Your backend will then handle this tool call by invoking the C1 Artifacts API and streaming the result back to the user.
This guide covers the entire creation workflow, from defining the tool to handling the API calls.
### Step 1: Define the `create_artifact` Tool
First, define the schema for a tool that the assistant can use to create an artifact. This schema outlines the parameters the LLM needs to extract from the user's instructions. In this example, we'll create a `create_presentation` tool.
```typescript theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
const createPresentationTool = {
type: "function",
function: {
name: "create_presentation",
description: "Creates a slide presentation based on a topic.",
parameters: zodToJsonSchema(
z.object({
instructions: z.string().describe("The instructions to generate the presentation."),
})
),
},
};
```
```python theme={null}
from pydantic import BaseModel, Field
class CreatePresentationParams(BaseModel):
instructions: str = Field(..., description="The instructions to generate the presentation.")
create_presentation_tool = {
"type": "function",
"function": {
"name": "create_presentation",
"description": "Creates a slide presentation based on a instructions.",
"parameters": CreatePresentationParams.model_json_schema(),
},
}
```
### Step 2: Handle the Tool Call in Your Backend
When the LLM decides to use your `create_presentation` tool, your backend will receive the tool call. Your code must then orchestrate the process of generating the artifact and streaming it back as part of the assistant's final message.
The key steps are:
1. Generate a unique `artifactId` and `messageId`.
2. Call the C1 Artifacts API, passing the `artifactId`.
3. Stream the artifact content into a [C1 Response](/sdk-reference/c1-response).
4. Return a `tool_result` to the main LLM, including the `artifactId` and `messageId` (as the `version`).
5. Stream the LLM's final text confirmation into the same C1 Response.
6. Store the response in the database as the assistant message.
```typescript theme={null}
// (Inside your Next.js API route)
import { nanoid } from "nanoid";
import { makeC1Response } from "@thesysai/genui-sdk/server";
// When your tool handler is invoked by the LLM...
async function handleCreatePresentation(
// following parameters are passed by the LLM
{ instructions }: { instructions: string },
// following parameters are passed by your backend
{ messageId, c1Response }: { messageId: string, c1Response: ReturnType }
) {
// 2. Call the Artifacts API and stream the result.
const artifactStream = await c1ArtifactsClient.chat.completions.create({
model: "c1/artifact/v-20251030",
messages: [{ role: "user", content: instructions }],
metadata: { thesys: JSON.stringify({ c1_artifact_type: "slides", id: artifactId }) },
stream: true,
});
// 3. Pipe the artifact stream into the C1 Response object.
for await (const delta of artifactStream) {
const content = delta.choices[0]?.delta?.content;
if (content) {
c1Response.writeContent(content);
}
}
// 4. Return the result to the main LLM so it knows the tool succeeded.
return `Presentation created with artifact_id: ${artifactId}, version: ${messageId}`,
}
// After the tool result is sent, the main LLM will generate its final text response.
// You will then pipe that final text stream into the same `c1Response` object
// before returning `c1Response.responseStream` from your API route.
```
```python theme={null}
# (Inside your FastAPI route)
import nanoid
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content
# for more details on how to use thesys_genui_sdk, see https://pypi.org/project/thesys-genui-sdk/.
# When your tool handler is invoked by the LLM...
async def handle_create_presentation(instructions: str, message_id: str):
# 1. Developer generates unique IDs.
artifact_id = nanoid.generate(size=10)
message_id = nanoid.generate(size=10) # This will serve as the first version.
# 2. Call the Artifacts API and stream the result.
artifact_stream = await c1_artifacts_client.chat.completions.create(
model="c1/artifact/v-20251030",
messages=[{"role": "user", "content": f"Create a presentation about {topic}"}],
metadata={"thesys": json.dumps({"c1_artifact_type": "slides", "id": artifact_id})},
stream=True,
)
# 3. Pipe the artifact stream using the write_content helper.
async for delta in artifact_stream:
content = delta.choices[0].delta.content
if content:
await write_content(content)
# 4. Return the result to the main LLM.
return f"Presentation created with artifact_id: {artifact_id}, version: {message_id}"
# Your main logic will then take this tool result, send it back to the LLM,
# and stream the final text confirmation using the same `write_content` helper.
```
### Step 3: The Final Assistant Response
After your backend returns the successful `tool_result`, the main LLM generates a final, user-facing response (e.g., "I've created the presentation for you. You can see it below.").
Your backend code must stream this final text into the *same* C1 Response object. The result is a single, composite assistant message that contains both the confirmation text and the fully rendered artifact, which is then sent to the frontend.
#### A Note on the `version`
In this guide, we use the unique `messageId` of the assistant's response as the `version`. This creates a clear link between a specific version of the artifact and the message that contains it. The importance of this `version` will become clear in the next guide, where we use it to edit the artifact.
Find more examples and complete code on our GitHub repository.
# Artifacts in Chat
Source: https://docs.thesys.dev/guides/artifacts/artifacts-in-chat/index
Integrate rich, editable documents like reports and slides directly into your C1-powered assistants.
You can enhance your C1-powered assistants by giving them the ability to generate and edit long-form content, like reports and slides, directly within a conversation. This is achieved using a tool-calling workflow where your backend orchestrates the creation and modification of these documents, known as Artifacts.
### Workflow
The process relies on your assistant using tools to interact with the C1 Artifacts API. Your backend receives the tool call from the LLM, executes the logic by calling the C1 artifacts API, and then returns the result to the LLM to formulate a final response.
This creates a powerful loop where the assistant can work with documents on the user's behalf.
```mermaid theme={null}
sequenceDiagram
participant User
participant Assistant (Frontend)
participant Your Backend
participant LLM
participant C1 Artifacts API
User->>Assistant (Frontend): "Create a report for me"
Assistant (Frontend)->>Your Backend: Sends prompt
Your Backend->>LLM: "User wants a report"
LLM->>Your Backend: Use `create_report` tool
Your Backend->>C1 Artifacts API: Generate artifact
C1 Artifacts API-->>Your Backend: Returns artifact content
Your Backend->>LLM: Tool success, here is artifact_id & version
LLM->>Your Backend: "I have generate the report"
Your Backend-->>Assistant (Frontend): Assembled C1 Response (text + artifact)
```
The core of this pattern relies on two main tools you will define:
* **`create_artifact`**: A tool that calls the C1 Artifacts API to generate a new document.
* **`edit_artifact`**: A tool that calls the C1 Artifacts API with existing artifact content to make modifications.
### Guides in This Section
* **[Generating an Artifact in a Conversation](/guides/artifacts/artifacts-in-chat/generating)**
A step-by-step tutorial on using a tool call to create a new Artifact and embed it in an assistant's response.
* **[Editing an Artifact in a Conversation](/guides/artifacts/artifacts-in-chat/editing)**
Learn how to implement the edit loop for artifacts using a tool call.
# Editing Artifacts
Source: https://docs.thesys.dev/guides/artifacts/editing/index
Modify existing artifacts through prompt-based instructions or direct manual editing
Artifacts are designed to be refined iteratively. Once an artifact has been generated, there are two ways to edit it:
**1. Prompt-Based Editing:**
Send the existing artifact content back to the API along with a new prompt describing the desired changes. The model processes the instructions and returns a modified version of the artifact—no need to regenerate from scratch.
**2. Manual Editing:**
Allow users to directly edit the content of a rendered artifact on the frontend. By enabling this feature, an "Edit" button appears on the artifact, letting users modify text, titles, and images as if editing a document.
### Guides in This Section
* **[Prompt-Based Editing](/guides/artifacts/editing/prompt-based):**
Learn how to send existing artifact content back to the API with a new prompt to make modifications programmatically.
* **[Manual Editing](/guides/artifacts/editing/manual):**
Learn how to enable direct, in-place editing of artifacts on the frontend using C1 component props.
# Manual Editing
Source: https://docs.thesys.dev/guides/artifacts/editing/manual
Allow users to directly and manually edit the content of generated slides and reports
You can make generated artifacts, such as slides and reports, directly editable by the user. By enabling this feature, an "Edit" button appears on the artifact. When clicked, it allows the user to modify the content—like changing text, updating titles, or correcting data—as if they were editing a document.
This is an opt-in feature that is enabled with a single prop on your C1 components.
### Implementation
To enable artifact editing, you need to pass the `enableArtifactEdit: true` flag to the C1 component you are using.
#### Using ``
Pass the `enableArtifactEdit` prop directly to the component.
```tsx theme={null}
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
function MyArtifactViewer({ c1Response, updateMessage }) {
return (
);
}
```
#### Using ``
Pass `enableArtifactEdit: true` inside the `customizeC1` prop object.
```tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
function MyChat() {
return (
);
}
```
### How It Works
When `enableArtifactEdit` is set to `true`, the C1 UI renders an "Edit" button on the artifact. Clicking this button makes the artifact's content fields editable, allowing the user to make direct changes.
#### Persisting the Changes
After a user finishes editing and saves their changes, the component's internal state is updated. To ensure these changes are not lost, a callback is triggered with the new, modified content.
* For **``**, the `updateMessage` callback fires.
* For **``** (or when using `useThreadManager`), the `onUpdateMessage` callback fires.
You must implement these callbacks to save the updated message content to your database. This keeps your backend state consistent with the manual edits the user sees on the frontend. Without this step, any manual changes will be lost on the next page load.
# Prompt-Based Editing
Source: https://docs.thesys.dev/guides/artifacts/editing/prompt-based
Modify existing artifacts by sending existing artifact back to the API with a new prompt
Prompt-based editing allows you to modify an artifact by making a new API call that includes the **existing artifact content** as context, along with a new prompt describing the desired modifications. Instead of regenerating a document from scratch, you provide editing instructions in a follow-up prompt.
### Editing Pattern
To edit an artifact, you call the same Artifact API endpoint used for generation. The key difference is the structure of the `messages` array, which must contain two messages in this specific order:
1. **An `assistant` message:** The `content` of this message must be the full **artifact content string** of the document you want to edit.
2. **A `user` message:** The `content` of this message is your new prompt with the editing instructions, for example, "Add a slide about our key competitors" or "Change the title to 'Q4 Financial Report'".
The `metadata` object (with the `c1_artifact_type`) should be included just as it was in the original generation call to ensure the context is maintained.
### Full Example: Adding a Slide to a Presentation
This example shows the complete workflow for adding a new slide to an existing presentation.
```typescript src/app/api/edit-slides/route.ts theme={null}
import { NextRequest, NextResponse } from "next";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/artifact",
apiKey: process.env.THESYS_API_KEY,
});
export async function POST(req: NextRequest): Promise {
{ existingArtifactContent: string } = await req.json()
const editPrompt = "Add a new slide at the end summarizing the key takeaways.";
const updatedArtifact = await client.chat.completions.create({
model: "c1/artifact/v-20251030",
messages: [
// 1. Provide the existing artifact content as the assistant's message.
{
role: 'assistant',
content: existingArtifactContent,
},
// 2. Provide the new editing instruction as the user's message.
{
role: 'user',
content: editPrompt,
},
],
metadata: {
thesys: JSON.stringify({
c1_artifact_type: 'slides',
id: 'unique-id', // previously generated unique id for the artifact
}),
},
});
// The response contains the new, modified artifact content string.
return { content: updatedArtifact.choices[0].message.content }
}
```
```python theme={null}
import os
import json
import openai
from pydantic import BaseModel
client = openai.OpenAI(
base_url="https://api.thesys.dev/v1/artifact",
api_key=os.environ.get("THESYS_API_KEY"),
)
class EditSlidesRequest(BaseModel):
existing_artifact_content: str
@app.post('/edit-slides')
def add_slide_to_artifact(req: EditSlidesRequest):
edit_prompt = "Add a new slide at the end summarizing the key takeaways."
updated_artifact = client.chat.completions.create(
model="c1/artifact/v-20251030",
messages=[
# 1. Provide the existing artifact content as the assistant's message.
{
"role": "assistant",
"content": req.existing_artifact_content,
},
# 2. Provide the new editing instruction as the user's message.
{
"role": "user",
"content": edit_prompt,
},
],
metadata={
"thesys": json.dumps({
"c1_artifact_type": "slides"
"id": "unique-id", # previously generated unique id for the artifact
})
}
)
# The response contains the new, modified artifact content string.
return { "content": updated_artifact.choices[0].message.content }
```
# Generating Artifacts
Source: https://docs.thesys.dev/guides/artifacts/generating
Call the Artifact API to create new reports and slides from a prompt
Generating an artifact involves making an API call to a dedicated C1 endpoint. This is done using a standard OpenAI client library, configured to connect to the C1 Artifacts service.
### Configuring the API Client
To begin, initialize your OpenAI client to use the C1 Artifacts endpoint and your Thesys API key.
**Artifact API Endpoint:** `https://api.thesys.dev/v1/artifact`
You can create a new API key from [Developer Console](https://console.thesys.dev/keys).
```typescript theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/artifact",
apiKey: process.env.THESYS_API_KEY,
});
```
```python theme={null}
import os
import openai
client = openai.OpenAI(
base_url="https://api.thesys.dev/v1/artifact",
api_key=os.environ.get("THESYS_API_KEY"),
)
```
### Structuring the Request
The API call's payload has two main parts:
* `messages` array for your prompt
* `metadata` object for C1-specific instructions.
#### The `messages` Array
This is where you provide the prompt to generate the artifact. The `content` of the `user` message can include rich context, like data or a detailed description of the desired output.
You can also include a `system` prompt to provide high-level instructions.
#### The `metadata` Object
This object provides C1-specific instructions for the generation. It must contain a `thesys` key, whose value is a stringified JSON object.
Inside the `thesys` object, the `c1_artifact_type` property tells C1 what kind of artifact to generate, for example `'slides'` or `'report'`.
```json theme={null}
{
"c1_artifact_type": "slides", // or 'report' for generating a report
"id": "unique-id-for-the-artifact"
}
```
### Full Example: Generating Slides
This example brings the concepts together to generate a slide deck from a prompt. The `artifact` variable in the response will contain the C1 DSL string, which is ready to be sent to your frontend for rendering.
```typescript src/app/api/generate-slides/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/artifact",
apiKey: process.env.THESYS_API_KEY,
});
export async function POST(req: NextRequest): Promise {
const artifact = await client.chat.completions.create({
model: "c1/artifact/v-20251030", // Specify the model
messages: [
{
role: 'system',
content: 'system prompt here: it can include high-level instructions or context for the generation.',
},
{
role: 'user',
content: 'Generate a presentation showcasing the top 10 AI tools that can transform business operations, with implementation timelines and ROI calculations.',
},
],
metadata: {
thesys: JSON.stringify({
c1_artifact_type: 'slides',
id: 'unique-id-for-the-artifact', // this is required for editing the artifact in future
}),
}
});
// artifact.choices[0].message.content contains the C1 DSL string for the generated artifact
return { "content": artifact.choices[0].message.content };
}
```
```python theme={null}
import os
import json
import openai
client = openai.OpenAI(
base_url="https://api.thesys.dev/v1/artifact",
api_key=os.environ.get("THESYS_API_KEY"),
)
@app.post("/generate-slides")
def generate_slides():
artifact = client.chat.completions.create(
model="c1/artifact/v-20251030", # Specify the model
messages=[{
"role": "user",
"content": "Generate a presentation showcasing the top 10 AI tools that can transform business operations, with implementation timelines and ROI calculations.",
}],
metadata={
"thesys": json.dumps({
"c1_artifact_type": "slides",
"id": "unique-id-for-the-artifact", // this is required for editing the artifact in future
})
}
)
# artifact.choices[0].message.content contains the C1 DSL string for the generated artifact
return { "content": artifact.choices[0].message.content }
```
# Artifacts
Source: https://docs.thesys.dev/guides/artifacts/index
Generate, render, and edit rich, document-style content like reports and presentations
Artifacts are structured, document-style content that C1 can generate, such as reports and presentations.
They can be created as standalone documents or generated dynamically within a conversational application.
Artifacts feature can be used to build applications that can generate:
* Business reports from a conversation.
* Presentations created dynamically from data.
To see Artifacts in action, try our MCP in tools like ChatGPT or Claude at [render.thesys.dev](https://render.thesys.dev).
### Working with Artifacts
The workflow has three main steps:
**1. Generate:**
You start by sending a prompt to the dedicated Artifacts API endpoint. This initial call creates the content based on your prompt, data and/or system instructions you provide.
**2. Rendering and streaming:**
The generated artifact is then rendered on your frontend using the [C1Component](/guides/rendering-ui) ``. The content can be streamed in real-time, allowing users to see the report or presentation appear as it's being created.
**3. Edit:**
To modify an existing artifact, you send its current content back to the same API endpoint along with a new prompt describing the changes. This enables an iterative editing workflow.
### Guides in This Section
* **[Generating Artifacts](/guides/artifacts/generating):**
Learn how to call the Artifact API, structure your metadata, and write effective prompts to create new reports and presentations.
* **[Rendering & Streaming Artifacts](/guides/artifacts/rendering):**
A guide on fetching and displaying artifacts on your frontend with ``, including how to handle the live stream.
* **[Editing Artifacts](/guides/artifacts/editing):**
Learn about the two ways to edit artifacts: prompt-based editing via the API and manual editing on the frontend.
* **[Artifacts in Chat](/guides/artifacts/artifacts-in-chat):**
Learn how to integrate artifacts into your conversational application using tool calls. You can find the complete example code in our [GitHub repository](https://github.com/thesysdev/examples/tree/main/c1-chat-artifact).
# Exporting Artifacts to PDF
Source: https://docs.thesys.dev/guides/artifacts/pdf-export
Enable users to download generated reports and slides as PDF files
C1 provides a dedicated API endpoint to convert any generated artifact into a downloadable PDF file. This guide covers the end-to-end process, which involves creating an API route on your backend and connecting it to the C1 frontend components.
### How it Works
The PDF export process involves two main steps and is designed to keep your API keys secure:
1. **Frontend Trigger:** The user initiates the export from the UI. The `` provides a `exportParams` string to a callback function in your code.
2. **Backend Endpoint:** Your frontend sends these `exportParams` to a dedicated endpoint on your own backend. Your backend then securely calls the C1 PDF Export API with your API key and streams the resulting PDF file back to the user's browser for download.
You must create a backend endpoint. The C1 PDF Export API cannot be called directly from the frontend as it would expose your secret API key.
### Implement the Backend Endpoint
Your backend needs an API route that will receive the request from your frontend, call the C1 API, and forward the PDF response.
**PDF Export API Endpoint:** `POST /v1/artifact/pdf/export`
```typescript app/api/export-pdf/route.ts (Next.js App Router) theme={null}
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { exportParams } = await req.json();
try {
const pdfResponse = await fetch('https://api.thesys.dev/v1/artifact/pdf/export', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.THESYS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ exportParams }),
});
if (!pdfResponse.ok) {
throw new Error(`Failed to export PDF: ${pdfResponse.statusText}`);
}
// Stream the PDF back to the client
return new NextResponse(pdfResponse.body, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="artifact.pdf"',
},
});
} catch (error) {
const message = error instanceof Error ? error.message : 'An unknown error occurred';
return NextResponse.json({ error: message }, { status: 500 });
}
}
```
```python main.py (FastAPI) theme={null}
import os
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/api/export-pdf")
async def export_artifact_as_pdf(request: Request):
data = await request.json()
export_params = data.get("exportParams")
if not export_params:
raise HTTPException(status_code=400, detail="exportParams not provided")
headers = {
"Authorization": f"Bearer {os.getenv('THESYS_API_KEY')}",
"Content-Type": "application/json",
}
async with httpx.AsyncClient() as client:
# Use a streaming request to handle large files
async with client.stream(
"POST",
"https://api.thesys.dev/v1/artifact/pdf/export",
headers=headers,
json={"exportParams": export_params},
) as response:
response.raise_for_status()
# Stream the PDF content back to the client
return StreamingResponse(
response.aiter_bytes(),
media_type="application/pdf",
headers={"Content-Disposition": "attachment; filename=artifact.pdf"}
)
```
### Implement the Frontend Handler
The `` provides an `exportAsPdf` prop, which accepts a function. This function is called when the user clicks the export button in the artifact's UI. It receives the `exportParams` and a suggested `title` for the file.
Your implementation should call your backend endpoint and use the response to trigger a file download in the browser.
```tsx theme={null}
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
function ArtifactWithExport({ c1Response }: { c1Response: string }) {
return (
);
}
const handleExport = async ({ exportParams, title }: { exportParams: string, title: string }) => {
try {
// 1. Call your backend endpoint with the exportParams
const response = await fetch("/api/export-pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ exportParams }),
});
if (!response.ok) {
throw new Error("Failed to download PDF.");
}
// 2. Get the PDF data as a blob
const blob = await response.blob();
// 3. Create a temporary URL and trigger the download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const filename = (title || 'artifact').replace(/\.pdf$/i, '');
a.download = `${filename}.pdf`;
document.body.appendChild(a);
a.click();
// 4. Clean up the temporary URL
window.URL.revokeObjectURL(url);
a.remove();
} catch (error) {
console.error("Export failed:", error);
// Handle error, e.g., show a notification to the user
}
};
```
#### Usage with ``
You can also enable PDF export in a conversational context. The `exportAsPdf` function can be passed via the `customizeC1` prop to either `` or the `useThreadManager` hook.
```tsx theme={null}
```
# Exporting Slides to PPTX
Source: https://docs.thesys.dev/guides/artifacts/pptx-export
Enable users to download generated slides as PPTX files
C1 provides a dedicated API endpoint to convert any generated slide into a downloadable PPTX file. This guide covers the end-to-end process, which involves creating an API route on your backend and connecting it to the C1 frontend components.
### How it Works
The PPTX export process involves two main steps and is designed to keep your API keys secure:
1. **Frontend Trigger:** The user initiates the export from the UI. The `` provides a `exportParams` string to a callback function in your code.
2. **Backend Endpoint:** Your frontend sends these `exportParams` to a dedicated endpoint on your own backend. Your backend then securely calls the C1 PPTX Export API with your API key and streams the resulting PPTX file back to the user's browser for download.
You must create a backend endpoint. The C1 PPTX Export API should not be called directly from the frontend as it would expose your secret API key.
### Implement the Backend Endpoint
Your backend needs an API route that will receive the request from your frontend, call the C1 API, and forward the PPTX response.
**PPTX Export API Endpoint:** `POST /v1/artifact/pptx/export`
```typescript app/api/export-pptx/route.ts (Next.js App Router) theme={null}
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { exportParams } = await req.json();
try {
const pptxResponse = await fetch('https://api.thesys.dev/v1/artifact/pptx/export', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.THESYS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ exportParams }),
});
if (!pptxResponse.ok) {
throw new Error(`Failed to export PPTX: ${pptxResponse.statusText}`);
}
// Stream the PPTX back to the client
return new NextResponse(pptxResponse.body, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'Content-Disposition': 'attachment; filename="slide.pptx"',
},
});
} catch (error) {
const message = error instanceof Error ? error.message : 'An unknown error occurred';
return NextResponse.json({ error: message }, { status: 500 });
}
}
```
```python main.py (FastAPI) theme={null}
import os
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/api/export-pptx")
async def export_artifact_as_pptx(request: Request):
data = await request.json()
export_params = data.get("exportParams")
if not export_params:
raise HTTPException(status_code=400, detail="exportParams not provided")
headers = {
"Authorization": f"Bearer {os.getenv('THESYS_API_KEY')}",
"Content-Type": "application/json",
}
async with httpx.AsyncClient() as client:
# Use a streaming request to handle large files
async with client.stream(
"POST",
"https://api.thesys.dev/v1/artifact/pptx/export",
headers=headers,
json={"exportParams": export_params},
) as response:
response.raise_for_status()
# Stream the PPTX content back to the client
return StreamingResponse(
response.aiter_bytes(),
media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation",
headers={"Content-Disposition": "attachment; filename=artifact.pptx"}
)
```
### Implement the Frontend Handler
The `` provides an `exportAsPPTX` prop, which accepts a function. This function is called when the user clicks the export button in the artifact's UI. It receives the `exportParams` and a suggested `title` for the file.
Your implementation should call your backend endpoint and use the response to trigger a file download in the browser.
```tsx theme={null}
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
function ArtifactWithExport({ c1Response }: { c1Response: string }) {
return (
);
}
const handlePptxExport = async ({ exportParams, title }: { exportParams: string, title: string }) => {
try {
// 1. Call your backend endpoint with the exportParams
const response = await fetch("/api/export-pptx", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ exportParams }),
});
if (!response.ok) {
throw new Error("Failed to download PPTX.");
}
// 2. Get the PPTX data as a blob
const blob = await response.blob();
// 3. Create a temporary URL and trigger the download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const filename = (title || 'artifact').replace(/\.pptx$/i, '');
a.download = `${filename}.pptx`;
document.body.appendChild(a);
a.click();
// 4. Clean up the temporary URL
window.URL.revokeObjectURL(url);
a.remove();
} catch (error) {
console.error("Export failed:", error);
// Handle error, e.g., show a notification to the user
}
};
```
#### Usage with ``
You can also enable PPTX export in a conversational context. The `exportAsPPTX` function can be passed via the `customizeC1` prop to either `` or the `useThreadManager` hook.
```tsx theme={null}
```
# Rendering & Streaming Artifacts
Source: https://docs.thesys.dev/guides/artifacts/rendering
Fetch and display artifacts on your frontend with ``, including how to handle a live stream
Fetch and display artifacts on your frontend with ``, including how to handle a live stream.
Artifacts, like other C1-generated content, are rendered using the ``. Your frontend application is responsible for fetching the artifact's content string from your backend.
Streaming the response is the recommended approach for the best user experience. It allows the artifact's content to appear on the screen in real-time as it's being generated, rather than forcing the user to wait for the entire document to be ready.
### The Frontend-Backend Interaction
Your frontend application should not call the C1 Artifact API directly. Instead, it should call an endpoint on your backend server. Your backend is then responsible for securely calling the C1 API and forwarding the response to the client.
```mermaid theme={null}
sequenceDiagram
participant Frontend
participant Your Backend
participant C1 Artifact API
Frontend->>+Your Backend: POST /api/generate-artifact
Your Backend->>+C1 Artifact API: Calls C1 with prompt & metadata
C1 Artifact API-->>-Your Backend: Streams back artifact content string
Your Backend-->>-Frontend: Forwards the stream
```
### Rendering a Artifact
If your backend endpoint returns the entire artifact at once instead of streaming, the fetching logic on the frontend becomes simpler.
```tsx theme={null}
async function fetchStaticArtifact() {
setIsLoading(true);
try {
const response = await fetch("/api/generate-slides", { method: "POST" });
const data = await response.json(); // Assuming the backend returns { content: "..." }
setC1Response(data.content);
} catch (error) {
console.error("Error fetching artifact:", error);
} finally {
setIsLoading(false);
}
}
```
### Rendering a Streamed Artifact
This is the standard approach for rendering artifacts. The following example shows a React component that fetches and renders a streamed artifact from a backend endpoint.
```tsx theme={null}
import { useState } from "react";
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
function ArtifactViewer() {
const [c1Response, setC1Response] = useState("");
const [isLoading, setIsLoading] = useState(false);
const generateArtifact = async () => {
setIsLoading(true);
setC1Response(""); // Clear previous artifact
try {
// 1. Call your backend endpoint that streams the response.
const response = await fetch("/api/generate-artifact", { method: "POST" });
if (!response.body) {
throw new Error("Response body is empty.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedResponse = "";
// 2. Read the stream chunk by chunk.
while (true) {
const { done, value } = await reader.read();
if (done) break; // Exit loop when stream is finished
const chunk = decoder.decode(value);
accumulatedResponse += chunk;
// 3. Update state to re-render the component with the new content.
setC1Response(accumulatedResponse);
}
} catch (error) {
console.error("Error fetching or reading stream:", error);
} finally {
setIsLoading(false);
}
};
return (
{/* 4. Pass the streaming response and loading state to the component. */}
);
}
```
# Slide Templates (v-20251230)
Source: https://docs.thesys.dev/guides/artifacts/slides/slide-templates/v-20251230
Reference for the available slide templates that can be generated by C1.
You can use the details and parameters listed here to instruct the LLM in your prompts to generate specific and consistently formatted slides.
## TitleWithImage
A versatile title slide that combines text with a prominent image.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :-------------------------------------------- |
| `title` | Yes | The main title of the slide. |
| `subtitle` | No | Optional text appearing below the title. |
| `helperText` | No | Optional smaller text for additional context. |
| `imageSrc` | Yes | The URL or source path for the image. |
#### **Layouts**
The `layout` parameter controls the position of the image relative to the text.
| `image_right` | `image_bottom` |
| :------------------------------------ | :-------------------------------- |
| | |
***
## TitleOnly
A clean, text-focused slide for section headers or primary titles.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :-------------------------------------------- |
| `title` | Yes | The main title of the slide. |
| `subtitle` | No | Optional text appearing below the title. |
| `helperText` | No | Optional smaller text for additional context. |
#### **Layouts**
The `layout` parameter controls the alignment of the text.
| `title_center` | `title_top_left` |
| :-------------------------------- | :--------------------------------------- |
| | |
***
## KeyStatement
A slide designed to emphasize a single, powerful quote or statement.
#### **Parameters**
| Name | Required | Description |
| :------------ | :------- | :------------------------------------------ |
| `text` | Yes | The main statement or quote. |
| `attribution` | No | Optional source or author of the statement. |
***
## BigFact
Use this slide to highlight a single, impactful data point or statistic.
#### **Parameters**
| Name | Required | Description |
| :-------- | :------- | :-------------------------------------------------------- |
| `value` | Yes | The statistic or data point (e.g., "85%", "\$2.3M"). |
| `caption` | Yes | The descriptive text that provides context for the value. |
***
## TwoColumnText
A layout for presenting two blocks of text side-by-side, ideal for comparisons.
#### **Parameters**
| Name | Required | Description |
| :------------- | :------- | :------------------------------------- |
| `leftContent` | Yes | The text content for the left column. |
| `rightContent` | Yes | The text content for the right column. |
***
## TextWithImage
A standard content slide that combines a title, a body of text, and a supporting image.
#### **Parameters**
| Name | Required | Description |
| :--------- | :------- | :------------------------------------ |
| `title` | Yes | The title of the slide. |
| `content` | Yes | The main body text. |
| `imageSrc` | Yes | The URL or source path for the image. |
#### **Layouts**
The `layout` parameter controls the image position.
| `image_right` | `image_top` | `image_middle` |
| :------------------------------- | :----------------------------- | :-------------------------------- |
| | | |
***
## TextWithChartVertical
A content slide that displays a chart below the main text content.
#### **Parameters**
| Name | Required | Description |
| :-------- | :------- | :---------------------- |
| `title` | Yes | The title of the slide. |
| `content` | Yes | The main body text. |
| `chart` | Yes | Chart data. |
***
## TextWithChartHorizontal
A content slide that displays a chart to the side of the main text content.
#### **Parameters**
| Name | Required | Description |
| :-------- | :------- | :---------------------- |
| `title` | Yes | The title of the slide. |
| `content` | Yes | The main body text. |
| `chart` | Yes | Chart data. |
***
## TextWithTwoChartsHorizontal
A data-heavy slide for comparing two charts side-by-side, each with its own text.
#### **Parameters**
| Name | Required | Description |
| :------------- | :------- | :------------------------------------ |
| `leftTitle` | Yes | The title for the left-side content. |
| `leftContent` | Yes | The body text for the left side. |
| `leftChart` | Yes | Chart data for the left side. |
| `rightTitle` | Yes | The title for the right-side content. |
| `rightContent` | Yes | The body text for the right side. |
| `rightChart` | Yes | Chart data for the right side. |
***
## TextWithTwoChartsStacked
A data-heavy slide for presenting two charts vertically, with a single title and body of text.
#### **Parameters**
| Name | Required | Description |
| :------------ | :------- | :---------------------------------- |
| `title` | Yes | The main title for the slide. |
| `content` | Yes | The main body text. |
| `topChart` | Yes | Chart data for the top position. |
| `bottomChart` | Yes | Chart data for the bottom position. |
# Slide Templates (v-20260130)
Source: https://docs.thesys.dev/guides/artifacts/slides/slide-templates/v-20260130/index
Reference for the slide templates that can be generated by C1
You can use the details and parameters listed here to instruct the LLM in your prompts to generate specific and consistently formatted slides.
***
## Title Slide
The title slide introduces your topic and sets the tone for the entire presentation. It creates a strong first impression, frames your narrative, and signals what's coming next. Use it to establish context and build anticipation.
### Minimal Variant
Combines text and imagery to support professional storytelling. Perfect for case studies and overviews which need some textual context.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :-------------------------------------------- |
| `title` | Yes | The main title of the slide. |
| `subtitle` | No | Optional text appearing below the title. |
| `helperText` | No | Optional smaller text for additional context. |
| `bgImageSrc` | No | URL or source path for the background image. |
#### **Layouts**
The `layout` parameter controls the position of the title.
| `title-bottom` | `title-top` |
| :-------------------------------- | :----------------------------- |
| | |
***
### Standard Variant
Combines text and imagery to support professional storytelling. Perfect for case studies and overviews which need some textual context.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :-------------------------------------------- |
| `title` | Yes | The main title of the slide. |
| `subtitle` | No | Optional text appearing below the title. |
| `helperText` | No | Optional smaller text for additional context. |
| `image` | Yes | URL or source path for the image. |
#### **Layouts**
The `layout` parameter controls the position of the title relative to the image.
| `title-left` | `title-top` |
| :------------------------------ | :----------------------------- |
| | |
***
### Dramatic Variant
Designed for powerful first impressions. Best for short, self-explanatory titles where a background image could also add mood and emotion. Ideal for keynotes, event openings, and storytelling moments that need a cinematic feel.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :-------------------------------------------- |
| `title` | Yes | The main title of the slide. |
| `subtitle` | No | Optional text appearing below the title. |
| `helperText` | No | Optional smaller text for additional context. |
| `bgImageSrc` | No | URL or source path for the background image. |
#### **Layouts**
The `layout` parameter controls the position of the title.
| `title-center` | `title-bottom` |
| :--------------------------- | :-------------------------------- |
| | |
***
## Content Slide
Use these slides to outline the main sections or topics of your presentation. Ideal for agendas, chapter overviews, or roadmaps that help the audience understand what's coming next. Great for setting structure and guiding flow through your content.
### Classic Variant
Use this when you need a titled content index or agenda. Ideal for formal presentations where you want to clearly label the section and provide structure.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------ |
| `title` | Yes | The title of the content slide. |
| `items` | Yes | Array of list items (up to 16 items). |
#### **Layouts**
The `layout` parameter controls the arrangement of content.
| `title-top` | `title-left` |
| :----------------------------- | :------------------------------ |
| | |
***
### Expressive Variant
Use this when the list itself should take center stage. Best for moments where you want topics to feel confident and prominent without a title.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------ |
| `items` | Yes | Array of list items (up to 12 items). |
***
## Section Break Slide
Use section breaks to separate major topics and guide your audience through the presentation. They signal transitions, reset attention, and improve flow.
### Classic Variant
Use this slide to clearly separate major sections in your presentation. Ideal for signaling a topic change, giving the audience a moment to reset, and maintaining a clean, structured flow.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :------------------------------------------- |
| `title` | Yes | The section title. |
| `bgImageSrc` | No | URL or source path for the background image. |
***
### Dramatic Variant
Use this slide to introduce a new section with impact. Perfect for storytelling, key transitions, or moments where you want to create emphasis and build anticipation before moving forward.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :------------------------------------------- |
| `title` | Yes | The section title. |
| `body` | Yes | Supporting text for the section. |
| `bgImageSrc` | No | URL or source path for the background image. |
#### **Layouts**
The `layout` parameter controls the arrangement of content.
| `horizontal` | `vertical` |
| :------------------------------ | :---------------------------- |
| | |
***
## Key Figures Slide
These slides highlight important data, metrics, and takeaways at a glance. Use them to present statistics, milestones, or insights clearly and visually. They're ideal for emphasizing impact, supporting your narrative with evidence, and helping your audience quickly grasp what matters most.
### Insight Grid Variant
Use this to showcase multiple metrics or messages at once. Ideal for overviews, dashboards, or summarizing key points in a structured way.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------- |
| `tiles` | Yes | Array of tile objects (up to 9 tiles). |
**Tile Object Properties:**
| Name | Required | Description |
| :-------------- | :------- | :------------------------------------------------------------------------------------ |
| `primaryText` | Yes | The main text of the tile. |
| `secondaryText` | No | Optional subtitle text. |
| `bgType` | Yes | Background type: `{ bgColor: "aqua" \| "blue" \| "gray" }` or `{ bgImageSrc: "url" }` |
***
### Feature Highlights Variant
Perfect for breaking down a topic into supporting points represented by a recognizable icon. Use this to explain benefits, features, or sub-insights around a main idea.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------- |
| `title` | Yes | The main title for the slide. |
| `tiles` | Yes | Array of tile objects (up to 4 tiles). |
**Tile Object Properties:**
| Name | Required | Description |
| :-------------- | :------- | :------------------------------------------------------------------------------------ |
| `iconName` | Yes | Name of the icon. |
| `iconCategory` | Yes | Category of the icon. |
| `primaryText` | Yes | The main text of the tile. |
| `secondaryText` | No | Optional subtitle text. |
| `bgType` | Yes | Background type: `{ bgColor: "aqua" \| "blue" \| "gray" }` or `{ bgImageSrc: "url" }` |
#### **Layouts**
The `layout` parameter controls the arrangement of content.
| `title-top` | `title-left` |
| :----------------------------- | :------------------------------ |
| | |
***
### Hero Metric Variant
Use this to spotlight a single powerful number. Ideal for major achievements, growth metrics, or headline results.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :--------------------------------------------------- |
| `metric` | Yes | The statistic or data point (e.g., "85%", "\$2.3M"). |
| `caption` | Yes | Descriptive text providing context for the metric. |
| `bgImageSrc` | No | URL or source path for the background image. |
#### **Layouts**
The `layout` parameter controls the arrangement of content.
| `vertical` | `horizontal` |
| :---------------------------- | :------------------------------ |
| | |
***
### Headline Statement Variant
Best for bold takeaways or key messages. Use when you want to emphasize an important idea or a summarizing statement.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :------------------------------------------- |
| `eyebrow` | Yes | Short introductory text above the headline. |
| `headline` | Yes | The main statement or message. |
| `bgImageSrc` | No | URL or source path for the background image. |
#### **Layouts**
The `layout` parameter controls the position of content.
| `title-bottom` | `title-top` |
| :-------------------------------- | :----------------------------- |
| | |
***
### Key Info Variant
Use this to highlight up to six key figures, either to compare them or to showcase them individually. Ideal for performance summaries, progress updates, or presenting multiple important stats on one slide.
#### **Parameters**
| Name | Required | Description |
| :---------- | :------- | :------------------------------------------ |
| `infoItems` | Yes | Array of info item objects (up to 6 items). |
**Info Item Object Properties:**
| Name | Required | Description |
| :------------ | :------- | :---------------------------------- |
| `title` | Yes | The title for the info item. |
| `description` | Yes | Descriptive text for the info item. |
***
### Key Info with Title Variant
Use this to highlight up to six key figures given a specific context, either to compare them or to showcase them individually. Ideal for performance summaries, progress updates, or presenting multiple important stats on one slide.
#### **Parameters**
| Name | Required | Description |
| :---------- | :------- | :------------------------------------------ |
| `title` | Yes | The main title for the slide. |
| `infoItems` | Yes | Array of info item objects (up to 6 items). |
**Info Item Object Properties:**
| Name | Required | Description |
| :------------ | :------- | :---------------------------------- |
| `title` | Yes | The title for the info item. |
| `description` | Yes | Descriptive text for the info item. |
#### **Layouts**
The `layout` parameter controls the arrangement of info items.
| `horizontal-grid` | `horizontal-list` | `vertical-grid` |
| :---------------------------- | :---------------------------- | :-------------------------- |
| | | |
***
### Numbered Key Points Variant
Use this slide to present a sequence of steps, stages, or key points in a structured order. Ideal for processes, frameworks, roadmaps, or storytelling where progression matters and each point builds on the previous one.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------- |
| `items` | Yes | Array of item objects (up to 8 items). |
**Item Object Properties:**
| Name | Required | Description |
| :------ | :------- | :------------------------------- |
| `title` | Yes | The title for the key point. |
| `body` | Yes | The body text for the key point. |
***
## Image with Text Slide
These slides combine visuals with written content to support your message. Ideal for explaining ideas, adding context, introducing topics, or reinforcing key points. Great when you want imagery to enhance understanding without overpowering the content.
### Visual Cards Variant
Use this slide to present multiple items, each with an image, title, and short description. Ideal for showcasing features, examples, categories, or concepts where each visual needs its own explanation.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------- |
| `title` | Yes | The main title for the slide. |
| `cards` | Yes | Array of card objects (up to 4 cards). |
**Card Object Properties:**
| Name | Required | Description |
| :--------- | :------- | :------------------------------------- |
| `title` | Yes | The title for the card. |
| `body` | Yes | The body text for the card. |
| `imageSrc` | Yes | URL or source path for the card image. |
***
### Content with Image Variant
Use this to present a title or key statement alongside a supporting image, with optional body text for added context. Ideal for section openers, brand messaging, or narrative transitions where you want to introduce an idea and briefly expand on it.
#### **Parameters**
| Name | Required | Description |
| :------- | :------- | :----------------------------------------- |
| `title` | Yes | The main title of the slide. |
| `body` | No | Optional body text for additional context. |
| `images` | Yes | Array of image URLs (up to 4 images). |
#### **Layouts**
The `layout` parameter controls the position of the image.
| `image-right` | `image-center` |
| :------------------------------- | :-------------------------------- |
| | |
***
### Metrics with Image Variant
Use this to present key numbers alongside supporting imagery. Works well for case studies, impact reporting, etc.
#### **Parameters**
| Name | Required | Description |
| :-------- | :------- | :----------------------------------------- |
| `title` | Yes | The main title for the slide. |
| `metrics` | Yes | Array of metric objects (up to 4 metrics). |
| `images` | Yes | Array of image URLs (up to 4 images). |
**Metric Object Properties:**
| Name | Required | Description |
| :------------ | :------- | :------------------------------- |
| `metric` | Yes | The statistic or data point. |
| `description` | Yes | Descriptive text for the metric. |
***
### Personnel Showcase Variant
Designed to introduce people and roles. Perfect for team pages, leadership decks, company intros, or project collaborators.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------- |
| `title` | Yes | The main title for the slide. |
| `cards` | Yes | Array of card objects (up to 8 cards). |
**Card Object Properties:**
| Name | Required | Description |
| :--------- | :------- | :----------------------------------------- |
| `imageSrc` | Yes | URL or source path for the person's image. |
| `name` | Yes | The person's name. |
| `title` | Yes | The person's title or role. |
| `body` | No | Optional additional text about the person. |
***
### List with Image Variant
Use this slide to present a structured list of points alongside a supporting visual. Ideal for explaining processes, outlining steps, highlighting features, or summarizing key takeaways while keeping visual context.
#### **Parameters**
| Name | Required | Description |
| :------- | :------- | :------------------------------------- |
| `title` | Yes | The main title for the slide. |
| `items` | Yes | Array of item objects (up to 4 items). |
| `images` | Yes | Array of image URLs (up to 4 images). |
**Item Object Properties:**
| Name | Required | Description |
| :------------- | :------- | :-------------------------- |
| `iconName` | Yes | Name of the icon. |
| `iconCategory` | Yes | Category of the icon. |
| `primaryText` | Yes | The main text for the item. |
| `body` | No | Optional additional text. |
***
## Image Slide
Use this slide to showcase images without additional content. Ideal for setting mood, creating visual breaks, emphasizing a theme, or supporting a story moment. Works well for transitions, inspiration, or when an image alone communicates the message.
### Images Variant
Use this slide to showcase images without additional content. Ideal for setting mood, creating visual breaks, emphasizing a theme, or supporting a story moment.
#### **Parameters**
| Name | Required | Description |
| :------- | :------- | :------------------------------------ |
| `images` | Yes | Array of image URLs (up to 4 images). |
***
## Text Slide
Use these slides when text content is the primary focus. Ideal for detailed explanations, quotes, or narrative content that requires more space for written information.
### Text Body Variant
Use this slide for longer-form text content with a clear title. Ideal for detailed explanations, paragraphs of content, or narrative sections of your presentation.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :--------------------------- |
| `title` | Yes | The main title of the slide. |
| `paragraphs` | Yes | Array of paragraph strings. |
#### **Layouts**
The `layout` parameter controls the alignment of content.
| `title-center` | `title-left` |
| :--------------------------- | :------------------------------ |
| | |
***
### Pull Quote Variant
Use this slide to emphasize a powerful quote or statement. Ideal for testimonials, key takeaways, or impactful statements that deserve to stand alone.
#### **Parameters**
| Name | Required | Description |
| :----------- | :------- | :------------------------------------------- |
| `quote` | Yes | The quote or statement text. |
| `author` | No | Optional attribution for the quote. |
| `bgImageSrc` | No | URL or source path for the background image. |
#### **Layouts**
The `layout` parameter controls the position of content.
| `title-top` | `title-center` |
| :----------------------------- | :--------------------------- |
| | |
***
## Chart Slides
Use chart slides to turn numbers into clear insights. Ideal for showing trends, comparisons, distributions, and performance at a glance, helping your audience quickly understand patterns and key takeaways.
All chart variants use a `chart` field that accepts a chart component object. The available chart components are:
**2D Chart Components** — for bar, line, and area visualizations:
| Component | Props |
| :---------- | :------------------------------------------------- |
| `AreaChart` | `chartData` (required), `xAxisLabel`, `yAxisLabel` |
| `BarChart` | `chartData` (required), `xAxisLabel`, `yAxisLabel` |
| `LineChart` | `chartData` (required), `xAxisLabel`, `yAxisLabel` |
**1D Chart Components** — for distribution visualizations:
| Component | Props |
| :------------ | :--------------------- |
| `PieChart` | `chartData` (required) |
| `RadialChart` | `chartData` (required) |
***
### Chart with Context Variant
Chart with title and optional body for context or interpretation. Perfect for highlighting trends, calling out insights, or adding supporting notes.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :---------------------------------------------------------- |
| `title` | Yes | The title for the chart slide. |
| `body` | No | Optional text for context or interpretation. |
| `chart` | Yes | A chart component: `AreaChart`, `BarChart`, or `LineChart`. |
#### **Layouts**
The `layout` parameter controls the arrangement of the title, body, and chart.
| `title-only` | `title-body-top` | `title-body-bottom` |
| :----------------------- | :---------------------------------- | :------------------------------------- |
| | | |
| `title-left` | `title-body-left` |
| :-------------------------- | :----------------------------------- |
| | |
***
### Chart with Metrics Variant
Chart with title and highlighted key metrics. Use to showcase a chart alongside up to 4 important data points for added context.
#### **Parameters**
| Name | Required | Description |
| :-------- | :------- | :----------------------------------------------------------------- |
| `title` | Yes | The title for the chart slide. |
| `metrics` | Yes | Array of metric objects (up to 4 metrics). |
| `chart` | Yes | A chart component object: `AreaChart`, `BarChart`, or `LineChart`. |
**Metric Object Properties:**
| Name | Required | Description |
| :------------ | :------- | :------------------------------- |
| `metric` | Yes | The statistic or data point. |
| `description` | Yes | Descriptive text for the metric. |
***
### Dual Charts with Context Variant
Two charts side by side, each with its own title and body text. Ideal for showing parallel metrics, regional performance, or comparing separate trends.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :------------------------------------------- |
| `title` | Yes | The main title for the slide. |
| `cards` | Yes | Array of chart card objects (up to 2 cards). |
**Card Object Properties:**
| Name | Required | Description |
| :------ | :------- | :----------------------------------------------------------------- |
| `chart` | Yes | A chart component object: `AreaChart`, `BarChart`, or `LineChart`. |
| `title` | Yes | The title for this chart card. |
| `body` | Yes | Descriptive text for this chart card. |
***
### Pie Radial with Context Variant
Pie or radial chart with a title for showing distributions and proportions.
#### **Parameters**
| Name | Required | Description |
| :------ | :------- | :--------------------------------------------------------- |
| `title` | Yes | The title for the chart slide. |
| `chart` | Yes | A chart component object: `PieChartV2` or `RadialChartV2`. |
Only `PieChart` and `RadialChart` are supported in this variant. Do not use `AreaChart`, `BarChart`, or `LineChart`.
# Overview
Source: https://docs.thesys.dev/guides/basics
The essential guides for making your first API call, rendering UI, and connecting to data.
This section covers the fundamental building blocks of C1. These guides walk you through the complete, end-to-end flow: from making your first backend API call to rendering a dynamic, data-driven UI on the frontend.
Mastering these concepts is the first step to building any application with C1.
## Guides in This Section
* [**Integrating C1 in your backend**](/guides/implementing-api)
Call the C1 API from your backend using the OpenAI-compatible endpoint.
* [**Rendering C1 Responses into live UI**](/guides/rendering-ui)
Use the C1 React SDK to render the API response as interactive UI components.
* [**Streaming**](/guides/streaming)
Stream responses from the API to create a real-time, responsive user experience.
* [**Guiding UI generations**](/guides/guiding-outputs)
Use system prompts to control the structure and content of the generated UI.
* [**Integrate data via Tool Calling**](/guides/integrate-data/tool-calling)
Connect C1 to your own data sources and APIs using tool calling.
# Conversational API
Source: https://docs.thesys.dev/guides/conversational/backend-api
Create a conversational API for your application
Create a chat API endpoint that handles streaming responses from the C1 model.
Install the necessary packages for your backend API:
```bash npm theme={null}
npm install openai @crayonai/stream
```
```bash pip theme={null}
pip install openai fastapi uvicorn thesys-genui-sdk
```
First, create a simple in-memory message store to manage conversation history.
This in-memory store just stores the list of messages for a given `threadId`
including messages that are not sent to the client like the tool call messages.
```ts app/api/chat/messageStore.ts theme={null}
import OpenAI from "openai";
export type DBMessage = OpenAI.Chat.ChatCompletionMessageParam & {
id?: string;
};
const messagesStore: {
[threadId: string]: DBMessage[];
} = {};
export const getMessageStore = (threadId: string) => {
if (!messagesStore[threadId]) {
messagesStore[threadId] = [];
}
const messageList = messagesStore[threadId];
return {
addMessage: (message: DBMessage) => {
messageList.push(message);
},
getOpenAICompatibleMessageList: () => {
return messageList.map((m) => {
const message = { ...m };
delete message.id;
return message;
});
},
};
};
```
```python message_store.py theme={null}
from typing import Dict, List, Any
# Type alias for messages
DBMessage = Dict[str, Any]
messages_store: Dict[str, List[DBMessage]] = {}
class MessageStore:
def __init__(self, thread_id: str):
if thread_id not in messages_store:
messages_store[thread_id] = []
self.message_list = messages_store[thread_id]
def add_message(self, message: DBMessage):
self.message_list.append(message)
def get_openai_compatible_message_list(self) -> List[Dict[str, Any]]:
return [
{k: v for k, v in msg.items() if k != "id"}
for msg in self.message_list
]
def get_message_store(thread_id: str) -> MessageStore:
return MessageStore(thread_id)
```
Create the main API endpoint that handles incoming chat requests with streaming:
```ts app/api/chat/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { DBMessage, getMessageStore } from "./messageStore";
export async function POST(req: NextRequest) {
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: DBMessage;
threadId: string;
responseId: string;
};
// Initialize the OpenAI client
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed/",
apiKey: process.env.THESYS_API_KEY,
});
// Get message store and add user message
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
// Create streaming chat completion
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Transform the response stream
const responseStream = transformStream(
llmStream,
(chunk) => {
return chunk.choices[0].delta.content;
},
{
onEnd: ({ accumulated }) => {
const message = accumulated.filter((message) => message).join("");
messageStore.addMessage({
role: "assistant",
content: message,
id: responseId,
});
},
}
) as ReadableStream;
// Return the streaming response
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```python main.py theme={null}
import os
from fastapi import FastAPI
from pydantic import BaseModel
from openai import OpenAI
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message
from message_store import get_message_store, DBMessage
app = FastAPI()
class ChatRequest(BaseModel):
prompt: DBMessage
threadId: str
responseId: str
@app.post("/api/chat")
@with_c1_response()
async def chat(request: ChatRequest):
# Initialize the OpenAI client
client = OpenAI(
api_key=os.environ["THESYS_API_KEY"],
base_url="https://api.thesys.dev/v1/embed",
)
# Get message store and add user message
message_store = get_message_store(request.threadId)
message_store.add_message(request.prompt)
# Create streaming chat completion
stream = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=message_store.get_openai_compatible_message_list(),
stream=True,
)
# Stream the response
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
# Get and store the assistant message
assistant_message = get_assistant_message()
assistant_message["id"] = request.responseId
message_store.add_message(assistant_message)
```
Make sure to set your Thesys API key as an environment variable:
```bash theme={null}
export THESYS_API_KEY=
```
```bash Next.js (.env.local) theme={null}
THESYS_API_KEY=
```
```bash Python (.env) theme={null}
THESYS_API_KEY=
```
Your API endpoint is now ready to handle streaming chat conversations with the C1 model!
# Conversational UI Concepts
Source: https://docs.thesys.dev/guides/conversational/concepts
Additional concepts specific to building Chatbot Style interfaces
Here we cover the flow of a conversation and the data model (`Thread`, `Message`) used by C1Chat.
### The Flow of a Conversation
```mermaid theme={null}
sequenceDiagram
autonumber
actor U as User
participant F as Chat UI
participant T as Thread (History)
participant B as Backend Server
participant C1 as C1 API
U->>F: Types a prompt and hits send
F->>T: Create & append "User Message" to current thread
F->>B: Send new user message + relevant thread context
B->>C1: Construct payload & call C1 API (system prompt + history + user msg)
C1-->>B: Return assistant response
B-->>F: Deliver C1 response
F->>T: Store as "Assistant Message" in thread
F-->>U: Render "Assistant Message" in the UI
```
* User Input: The user types a prompt into the chat composer and sends it.
* Message Creation: The application creates a user message from this input and adds it to the current Thread's history.
* Backend Request: The frontend sends the new user message (often along with the thread context) to your backend server.
* API Call: Your backend constructs the full payload for the C1 API. This typically includes a predefined system prompt, the conversation history from the Thread, and the new user message.
* Assistant Response: The C1 API processes the request and returns a response, which your application stores as an assistant message.
* UI Update: The frontend receives this C1 response, adds it to the Thread as a new assistant message, and displays it in the UI.
#### Message
C1 API is openai compatible. So the message is the same as openai.
A `Message` is a basic unit in a conversation. Each message has a `role` that defines its author:
* **`user`**: Represents an input from the end-user.
* **`assistant`**: Represents a response from the AI. The content can be standard text or a C1 DSL string for rendering interactive UI.
* **`system`**: Provides high-level instructions to the AI and is typically not visible in the UI.
#### Thread: A Single Conversation
A `Thread` is an ordered list of `Message` objects. It represents a single, continuous conversation.
#### ThreadList: Managing Multiple Conversations
A `ThreadList` is the collection of all threads for a user. In a typical UI, this is represented by the chat history sidebar.
# Customising C1Chat
Source: https://docs.thesys.dev/guides/conversational/customizing/c1chat
Customise and style your conversational UI components to match your brand
C1 is designed to be highly customizable. Here are a few simple ways to customize C1 UI to your requirements:
C1 offers flexibility in its form factor, allowing you to choose between:
* **Full Page**: A complete page conversation interface, similar to ChatGPT.
* **Side Panel**: A copilot-style conversation interface.
To select a form factor, use the `formFactor` prop in the `C1Chat` component:
```tsx {8} theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
import { themePresets } from "@crayonai/react-ui";
export default function Home() {
return ;
}
```
C1 can easily be customized through a variety of pre-built themes. To apply a theme, you can import `themePresets` from `@crayonai/react-ui` and pass the preset
to the `theme` prop of the `C1Chat` component.
```tsx theme={null}
import { themePresets } from "@crayonai/react-ui";
export default function Home() {
return ;
}
```
You can toggle between light and dark modes by setting the mode property in the theme object. All Crayon theme presets fully support both modes.
```tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
import { themePresets } from "@crayonai/react-ui";
export default function Home() {
return (
);
}
```
You can set the agent name and logo by passing the `agentName` and `logoUrl` props to the `C1Chat` component. These values control the agent's display name in the
sidebar and the avatar shown next to its messages.
```tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
import { themePresets } from "@crayonai/react-ui";
import { useSystemTheme } from "./useSystemTheme";
export default function Home() {
const systemTheme = useSystemTheme();
return (
);
}
```
For advanced customization, you can override the CSS classes applied to UI components. For example, to hide the AI agent's logo next to its messages,
target the `.crayon-shell-thread-message-assistant__logo` class in your CSS.
You can find the classes attached to different UI components by inspecting the elements through the browser's developer tools.
```css custom.css theme={null}
.crayon-shell-thread-message-assistant__logo {
display: none;
}
```
Then import those styles in your component:
```tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
import { themePresets } from "@crayonai/react-ui";
import { useSystemTheme } from "./useSystemTheme";
import "./custom.css";
export default function Home() {
const systemTheme = useSystemTheme();
return (
);
}
```
# Getting Started with
Source: https://docs.thesys.dev/guides/conversational/getting-started
Render a complete chat interface and integrate with your backend
### Quick Start
For rapid development, `` includes a built-in, in-memory store that manages the conversation history. To get started, you only need to provide your backend endpoint's URL to the `apiUrl` prop.
For API implementation, please refer to [Conversational API](/guides/conversational/backend-api) section.
```tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
function App() {
return ;
}
```
This is the fastest way to get a functional chat interface running.
The in-memory store is for development and prototyping. The entire chat history will be cleared when the page is refreshed.
*Reference: [`` Component Props](/react-reference/c1-chat)*
### Full Implementation: fetching thread and thread list from backend
For a production application, user expect a persistent conversation history. This is done by managing threads in your backend and fetching them to the UI. The C1 SDK provides two hooks that handle all the complex state management and data fetching logic for this process.
#### `useThreadManager`: Managing a Single Conversation
This hook handles the state and data fetching for a **single thread**. Its responsibilities include:
* Fetching all messages for a given thread.
* Adding a new `user` message to the state.
* Calling your backend API to get an `assistant` response.
*Reference: [`useThreadManager` Hook API](/guides/conversational/state-management#usethreadmanager)*
#### `useThreadListManager`: Managing the Chat History
This hook handles the state and logic for the **list of all threads**. Its responsibilities include:
* Fetching the list of a user's conversations.
* Switching the active thread.
* Creating new threads and deleting old ones.
*Reference: [`useThreadListManager` Hook API](/guides/conversational/state-management#usethreadlistmanager)*
#### Connecting the Hooks to ``
To enable persistence, you initialize these hooks and pass their state and handlers to the `` component. This switches `` from its automatic in-memory mode to a "controlled" mode, where all state is managed by you.
```tsx theme={null}
import { C1Chat, useThreadManager, useThreadListManager } from "@thesysai/genui-sdk";
function PersistentChat() {
// Hook for managing the list of all conversations
const threadListManager = useThreadListManager({
// Configuration for fetching, creating, and deleting threads...
});
// Hook for managing the currently active conversation
const threadManager = useThreadManager({
threadId: threadListManager.activeThreadId,
// Configuration for fetching messages and calling the chat API...
});
return (
);
}
```
# Conversational UI
Source: https://docs.thesys.dev/guides/conversational/index
Building Generative UI Conversational Apps (like ChatGPT) with Thesys C1
This guide assumes that you have completed the [Rendering UI guide](/guides/rendering-ui).
C1 is ideal for building modern, conversational applications. Unlike traditional chatbots that are limited to text, a C1-powered conversational apps can render rich, interactive UIs—like forms, charts, and data tables—directly within the conversation.
To help you build these experiences quickly, the C1 SDK provides ``. It is a pre-built, "batteries-included" React component that gives you a complete chat interface and manages the underlying logic.
### What is ``?
The `` component provides a complete solution for conversational UI, handling the following out of the box:
* **A Complete Chat UI:** Renders the message list, a composer for user input, and loading indicators.
* **Thread and Message Management:** Automatically handles conversation history, message ordering, and thread state.
* **Generative UI Rendering:** Seamlessly renders both standard text messages and complex C1 DSL responses like forms and charts.
* **Built-in Streaming:** Manages streaming responses from your backend for a real-time user experience.
### Guides in This Section
* **[Concepts](/guides/conversational/concepts)** Core terminology of conversational apps: thread, message, and thread list.
* **[Getting Started with ``](/guides/conversational/getting-started)** Get the `` component running and connected to your backend.
* **[State Management](/guides/conversational/state-management)** Manage the UI state of the chat application.
* **[Customizing ``](/guides/conversational/customizing/c1chat)** Adapt the component's appearance with themes and change its layout.
* **[Backend API](/guides/conversational/backend-api)** Create a conversational API for your application.
* **[Conversation Persistence](/guides/conversational/persistence)** Persist conversation history to a database and fetch it on UI.
* **[Sharing Conversations](/guides/conversational/sharing/thread)** Save conversation history to a database and generate shareable links.
# Conversation persistence
Source: https://docs.thesys.dev/guides/conversational/persistence
Persisting threads and chat history using Firebase
## Overview
This guide provides step-by-step instructions to implement chat persistence using Firebase Firestore within a Next.js application powered by the **Thesys C1 GenUI SDK**.
By following this guide, you'll learn how to store, retrieve, and manage chat threads and messages, enabling a seamless user experience across sessions.
The complete code for the example referenced in this guide can be found in the
[Thesys examples repository](https://github.com/thesysdev/examples/tree/main/persistence-in-c1-chat-using-firebase).
## Implementation Steps
First, ensure your Firebase project is ready and the Firebase SDK is configured in your Next.js application.
1. Go to the [Firebase Console](https://console.firebase.google.com/),and copy the `firebaseConfig` object.
Here are the detailed step-by-step instructions on how to setup the database and fetch the firebase configuration:
1. **Create Firestore Database:**
* Create a new project or use an existing one in the [Firebase Console](https://console.firebase.google.com/).
* Navigate to "Firestore Database" and click "Create database".
* Choose to start in **Test mode** for easier setup for the demo. This allows open read/write access for about 30 days.
* **Important:** Test mode rules are insecure and should not be used in a production environment.
2. **Get Firebase Configuration:**
* In your Firebase project, go to "Project settings" (gear icon) > "General" tab.
* Under "Your apps", click "Add app" and select the Web platform (`>`).
* Register your app and Firebase will provide a `firebaseConfig` object. Copy this object.
2. **Update Firebase Config File (`src/firebaseConfig.ts`):**
Replace the placeholder values in `firebaseConfig` with your actual credentials in `src/firebaseConfig.ts`:
```typescript src/firebaseConfig.ts [expandable] theme={null}
import { initializeApp, getApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
// IMPORTANT: Replace these with your actual Firebase project configuration!
const firebaseConfig = {
apiKey: "YOUR_FIREBASE_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID"
};
// Initialize Firebase
let app;
if (!getApps().length) {
app = initializeApp(firebaseConfig);
} else {
app = getApp();
}
const db = getFirestore(app);
export { db };
```
Implement a service layer **that handles** persistence logic using Firestore.
Create `src/services/threadService.ts`:
**1. Core CRUD Functions:**
Implement functions to create, read, update, and delete threads and messages.
Refer to the [complete example `threadService.ts`](https://github.com/thesysdev/examples/blob/main/persistence-in-c1-chat-using-firebase/src/services/threadService.ts) for full implementations of others like `getThreadList`, `getUIThreadMessages`, `getLLMThreadMessages`, `updateMessage`, `deleteThread`, and `updateThread` similarly.
```typescript src/services/threadService.ts (createThread) theme={null}
export const createThread = async (name: string): Promise => {
const newThreadRef = await addDoc(collection(db, THREADS_COLLECTION), {
name: name,
messages: [],
createdAt: serverTimestamp(),
});
return {
threadId: newThreadRef.id,
title: name,
createdAt: new Date(),
};
};
```
```typescript src/services/threadService.ts (addMessages) theme={null}
export const addMessages = async (threadId: string, ...messages: Message[]) => {
if (!threadId) throw new Error("threadId is required for addMessages");
const threadRef = doc(db, THREADS_COLLECTION, threadId);
const threadSnap = await getDoc(threadRef);
if (!threadSnap.exists()) {
throw new Error(`Thread with id ${threadId} not found.`);
}
const existingMessages = (threadSnap.data()?.messages as Message[]) ?? [];
const newMessages = existingMessages.concat(messages);
await updateDoc(threadRef, {
messages: newMessages,
});
};
```
Configure C1Chat component (`src/app/page.tsx`) to use the functions in `threadService.ts` for data operations.
1. **Configure C1 SDK Hooks:**
Pass the `threadService` functions to the `useThreadListManager` and `useThreadManager` hooks.
```typescript src/app/page.tsx (Hook Configuration) [expandable] theme={null}
const threadListManager = useThreadListManager({
fetchThreadList: () => getThreadList(),
deleteThread: (threadId) => deleteThread(threadId),
updateThread: (t) => updateThread({ threadId: t.threadId, name: t.title }),
onSwitchToNew: () => {
replace(`${pathname}`);
},
onSelectThread: (threadId) => {
const newSearch = `?threadId=${threadId}`;
replace(`${pathname}${newSearch}`);
},
createThread: (message) => {
return createThread(message.message!);
},
});
const threadManager = useThreadManager({
threadListManager,
loadThread: async (threadId) => await getUIThreadMessages(threadId),
onUpdateMessage: async ({ message }) => {
await updateMessage(threadListManager.selectedThreadId!, message);
},
apiUrl: "/api/chat",
});
```
See the [complete example `page.tsx`](https://github.com/thesysdev/examples/blob/main/persistence-in-c1-chat-using-firebase/src/app/page.tsx) for detailed hook setup with imports.
Add Chat Api route to use Thesys C1 Api along with functions in `threadService.ts` for fetching historical messages and saving new ones.
1. **Fetch History for LLM:**
Before calling the LLM, fetch the existing messages for the thread using `getLLMThreadMessages`.
```typescript src/app/api/chat/route.ts (Fetch History) theme={null}
const llmMessages = await getLLMThreadMessages(threadId);
const runToolsResponse = client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
...llmMessages, // Add previous messages
{
role: "user",
content: prompt.content!,
},
],
stream: true,
tools,
});
```
2. **Save Messages on Stream End:**
In the `on("end")` event of the LLM stream, use `addMessages` to store the new user prompt and all assistant/tool messages generated in that turn.
```typescript src/app/api/chat/route.ts (Save Messages) theme={null}
runToolsResponse.on("end", async () => {
if (isError) {
return;
}
const runToolsMessagesWithId = allRunToolsMessages.map((m, index) => {
const id =
allRunToolsMessages.length - 1 === index // for last message (the response shown to user), use the responseId as provided by the UI
? responseId
: crypto.randomUUID();
return {
...m,
id,
};
});
const messagesToStore = [prompt, ...runToolsMessagesWithId];
await addMessages(threadId, ...messagesToStore);
});
```
Refer to the [complete example `/api/chat/route.ts`](https://github.com/thesysdev/examples/blob/main/persistence-in-c1-chat-using-firebase/src/app/api/chat/route.ts) for context.
## Running and Testing
1. Ensure your Firebase credentials are correctly set in `src/firebaseConfig.ts`.
2. Ensure your `THESYS_API_KEY` is set in `.env`.
3. Run `npm run dev`.
4. Test creating new chats, sending messages, switching between chats, and refreshing the page to see if history persists. Check the Firebase console to see data being written to Firestore.
**Test it Out!**
## Conclusion
By following this guide, you've integrated Firebase Firestore for robust chat persistence in your conversational application.
This setup provides a scalable backend for your chat data, enhancing user experience by preserving conversation history.
# Sharing a Single Message
Source: https://docs.thesys.dev/guides/conversational/sharing/message
Enable users to generate a shareable link for an individual message.
This guide assumes that you have completed the [Quickstart](/guides/conversational/getting-started).
An example project demonstrating implementation of this guide can be found
[here](https://github.com/thesysdev/examples/tree/main/sharing-generated-ui).
Let users share an individual response via a share modal. You provide a `generateShareLink(message)` function that calls your backend and returns a URL. The SDK handles the modal UI, copying, and confirmation.
Create a footer component using the pre-built `ResponseFooter.ShareButton`.
```tsx Footer.tsx theme={null}
import { useThreadListState } from "@crayonai/react-core";
import { ResponseFooter } from "@thesysai/genui-sdk";
export const Footer = () => {
const selectedThreadId = useThreadListState().selectedThreadId;
return (
{
const messageId = message.id;
const baseUrl = window.location.origin;
return `${baseUrl}/shared/${selectedThreadId}/${messageId}`;
}}
/>
);
};
```
Then pass it to `C1Chat`:
```tsx App.tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import Footer from "./Footer";
export default function App() {
return (
);
}
```
The button receives the full `message` object and opens a modal for link generation and copying.
Create a route and component on the frontend to render the shared message:
```tsx /app/shared/[threadId]/[messageId]/page.tsx [expandable] theme={null}
"use client";
import { Loader } from "@/app/components/Loader";
import type { Message } from "@crayonai/react-core";
import { C1ChatViewer } from "@thesysai/genui-sdk";
import { use, useEffect, useState } from "react";
import "@crayonai/react-ui/styles/index.css";
export default function ViewSharedMessage({
params,
}: {
params: Promise<{ threadId: string; messageId: string }>;
}) {
const { threadId, messageId } = use(params);
const [messages, setMessages] = useState([]);
useEffect(() => {
const fetchMessages = async () => {
const response = await fetch(`/api/share/${threadId}/${messageId}`);
const messageResponse = (await response.json()) as {
message: Message;
};
setMessages([messageResponse.message]);
};
fetchMessages();
}, [messageId, threadId]);
if (!messages || !messages.length) return ;
return ;
}
```
Implement an endpoint that returns the message for a given `threadId` and `messageId`.
Implement a message store to store the message history. If you've followed the [Quickstart](/guides/conversational/persistence), you'll have a message store already, which you can move
to a common location (such as `/lib/messageStore.ts`) and modify it to persist message history across API routes and requests as follows:
```ts /lib/messageStore.ts focus={12, 15} [expandable] theme={null}
import OpenAI from "openai";
export type DBMessage = OpenAI.Chat.ChatCompletionMessageParam & {
id?: string;
};
const messagesStore: {
[threadId: string]: DBMessage[];
} = {};
export const getMessageStore = (id: string) => {
const messageList = await fetchMessagesFromDB(id); // fetch from db here
return {
addMessage: (message: DBMessage) => {
// save to db here
},
messageList,
};
};
```
```ts /app/api/share/[threadId]/[messageId]/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { getMessageStore } from "@/lib/messageStore";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ threadId: string; messageId: string }> }
) {
const { threadId, messageId } = await params;
if (!threadId || !messageId) {
return NextResponse.json(
{ error: "Thread ID & Message ID are required" },
{ status: 400 }
);
}
const messageStore = getMessageStore(threadId);
const message = messageStore.messageList.find((m) => m.id === messageId);
return NextResponse.json({ message: message ?? null });
}
```
* Send a message and wait for the assistant response.
* Click the share button in the footer.
* Generate and copy the link from the modal.
# Sharing an Entire Thread
Source: https://docs.thesys.dev/guides/conversational/sharing/thread
Let users generate a shareable link for the full conversation thread.
This guide assumes that you have completed the [Quickstart](/guides/conversational/getting-started).
An example project demonstrating implementation of this guide can be found
[here](https://github.com/thesysdev/examples/tree/main/sharing-generated-ui).
Enable thread-level sharing with the `C1ShareThread` component. You provide a `generateShareLink(messages)` function; the SDK gathers messages, opens a modal, and handles copying/confirmation.
First, destructure the `C1Chat` component as explained the [Integrating Custom Components](/guides/custom-components) guide, so that you can integrate a custom header. Next,
use the `C1ShareThread` component to display a share button in your chat header, which automatically opens a modal to generate a share link using the `generateShareLink` function you defined.
The updated code for the chat page will look something like this:
```tsx page.tsx [expandable] theme={null}
// ... imports
export default function Home() {
// ... create a threadManager and threadListManager here
const selectedThreadId = threadListManager.selectedThreadId;
return (
);
}
```
For details on how to create a `threadManager` and a `threadListManager`,
refer to [this guide](/guides/conversational/persistence)
Want a custom button? Provide `customTrigger` to `C1ShareThread`:
```tsx theme={null}
Share Thread}
generateShareLink={
!selectedThreadId
? undefined
: async () => {
const baseUrl = window.location.origin;
return `${baseUrl}/shared/${selectedThreadId}`;
}
}
/>
```
Create a route and component on the frontend to render the shared thread:
```tsx /app/shared/[threadId]/page.tsx [expandable] theme={null}
"use client";
import type { Message } from "@crayonai/react-core";
import { C1ChatViewer } from "@thesysai/genui-sdk";
import { use, useEffect, useState } from "react";
import "@crayonai/react-ui/styles/index.css";
import { Loader } from "@/app/components/Loader";
export default function ViewSharedPage({
params,
}: {
params: Promise<{ threadId: string }>;
}) {
const { threadId } = use(params);
const [messages, setMessages] = useState([]);
useEffect(() => {
const fetchMessages = async () => {
const response = await fetch(`/api/share/${threadId}`);
const messages = await response.json();
setMessages(messages);
};
fetchMessages();
}, [threadId]);
if (!messages || !messages.length) return ;
return ;
}
```
Implement an endpoint that returns the thread for a given `threadId`.
Implement a message store to store the message history. If you've followed the [Quickstart](/guides/conversational/getting-started), you'll have a message store already, which you can move
to a common location (such as `/lib/messageStore.ts`) and modify it to persist message history across API routes and requests as follows:
```ts /lib/messageStore.ts [expandable] theme={null}
import OpenAI from "openai";
export type DBMessage = OpenAI.Chat.ChatCompletionMessageParam & {
id?: string;
};
const messagesStore: {
[threadId: string]: DBMessage[];
} = {};
export const getMessageStore = (id: string) => {
const messageList = await fetchMessagesFromDB(id); // fetch from db here
return {
addMessage: (message: DBMessage) => {
// save to db here
},
messageList,
};
};
```
```ts /app/api/share/[threadId]/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { getMessageStore } from "@/lib/messageStore";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ threadId: string }> }
) {
const { threadId } = await params;
if (!threadId) {
return NextResponse.json(
{ error: "Thread ID is required" },
{ status: 400 }
);
}
const messageStore = getMessageStore(threadId);
return NextResponse.json(messageStore.messageList);
}
```
* Open a conversation with multiple messages.
* Click the thread share button.
* Generate and copy the link from the modal.
# UI state management
Source: https://docs.thesys.dev/guides/conversational/state-management
Manage the UI state of the chat application using useThreadManager and useThreadListManager
Once you have the basic setup ready, users would expect their conversations to be saved upon refresh. This guide explains how to configure the `useThreadManager` and `useThreadListManager` hooks with your app's persistence logic.
Import `useThreadManager` and `useThreadListManager` from `@thesysai/genui-sdk` package.
They are wrapper around the `@crayonai/react-core` package with C1 specific logic.
## `useThreadManager`
A **Thread** is a single conversation session, maintaining its own history and context. The `ThreadManager`, obtained via the `useThreadManager` hook, controls its state and actions.
Key configurations:
The `ThreadListManager` object. For more information on how to get this object, see [useThreadListManager](#usethreadlistmanager).
Load all messages for a specific thread.
The id of the thread.
A list of messages in the current thread.
Save message updates (e.g., after a form submission).
Props object containing the message object that has been updated.
Returns nothing.
A backend endpoint (e.g., `"/api/chat"`). Thesys GenUI SDK does a POST request to this endpoint with the following three arguments in the request body. If you need to pass additional arguments, use the `processMessage` function.
Provide either `processMessage` or `apiUrl`.
The id of the thread.
The latest user message in openai format.
Unique ID for the assistant's response.
A custom function for more control over sending messages and receiving AI responses. Provide either `processMessage` or `apiUrl`.
A param object containing the following properties:
Current thread ID.
Conversation history (OpenAI format).
Unique ID for the assistant's response.
For request cancellation.
A promise that resolves with the Response object from your backend.
The `useThreadManager` hook uses the `Response` object from this function to handle streaming updates to the UI.
Application backend must use the `responseId` to set the final assistant's response message ID, as it's used for future updates to that message.
## `useThreadListManager`
The **Thread List** displays multiple conversation threads, typically in a sidebar. The `ThreadListManager`, from the `useThreadListManager` hook, manages this list.
Key configurations:
Fetch all thread list.
An array of thread objects.
Create a new thread when the user sends the first message.
The initial user message that triggers thread creation.
The newly created thread.
Delete a specific thread.
The ID of the thread to delete.
Returns nothing.
Update a thread's metadata, like its title.
The updated thread object.
Returns nothing.
UI Callback function: called when a user selects a thread from the list.
The ID of the selected thread.
Returns nothing.
UI Callback function: called to prepare for a new conversation.
No arguments are required.
Returns nothing.
By configuring these hooks with your service functions, you integrate your application's data operations (like saving to or loading from the database) to enable persistent chat experiences.
For a detailed walkthrough of implementing service functions with a Firebase backend, see:
* [Example: Using Firebase](/guides/conversational/persistence)
# Implementing Custom Actions (Buttons)
Source: https://docs.thesys.dev/guides/custom-actions
Connect C1-generated UI to your application's specific logic and workflows.
For a complete overview of the interactivity system, please read the **[Actions](/guides/interactivity)** guide.
This guide focuses on **Custom Actions**, which allow you to go beyond the built-in behaviors and trigger your application's unique functions directly from the C1 UI. This makes the generative interface a true, interactive part of your application, enabling powerful workflows such as:
* Downloading a generated report.
* Opening a product-specific checkout modal.
* Triggering a function in your application, like creating a new project or sending an email.
### Implementing a Custom Action
Let's walk through an example of implementing a `download_report` custom action. It involves two steps: defining the action on your backend and handling it on your frontend.
#### 1. Define the Custom Action (Backend)
To make the LLM aware of your custom action, you must define its name and the parameters it accepts. This is done by passing a `c1_custom_actions` object within the `metadata` of your API call.
We recommend using a schema library (like Zod for TypeScript or Pydantic for Python) to define your action's parameters.
```typescript theme={null}
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
const messages = [
{ role: "system", content: "When the user asks to download data, offer them a 'download_report' button." },
{ role: "user", content: "Can I get a copy of our quarterly sales data?" }
];
const response = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20250930",
messages: messages,
metadata: {
thesys: JSON.stringify({
c1_custom_actions: {
download_report: zodToJsonSchema(z.object({
reportType: z.enum(["sales", "marketing", "inventory"]).describe("The category of the report."),
format: z.enum(["csv", "pdf"]).default("pdf").describe("The file format for the download."),
quarter: z.string().optional().describe("The specific quarter for the report, e.g., 'Q3 2025'."),
})),
}
}),
},
});
```
```python theme={null}
import os
import json, jsonref
import openai
from pydantic import BaseModel, Field
from typing import Literal, Optional
client = openai.OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
# Define the parameters for the custom action using Pydantic
class DownloadReportParams(BaseModel):
reportType: Literal["sales", "marketing", "inventory"] = Field(..., description="The category of the report.")
format: Literal["csv", "pdf"] = Field(default="pdf", description="The file format for the download.")
quarter: Optional[str] = Field(default=None, description="The specific quarter for the report, e.g., 'Q3 2025'.")
messages = [
{"role": "system", "content": "When the user asks to download data, offer them a 'download_report' button."},
{"role": "user", "content": "Can I get a copy of our quarterly sales data?"}
]
def generate_schema(model: BaseModel):
schema = model.model_json_schema()
schema = jsonref.replace_refs(schema, proxies=False) # dereference $refs and $defs
schema.pop("$defs", None) # remove $defs
schema.pop("$ref", None) # remove $refs
return schema
# Construct the metadata payload
thesys_metadata = {
"c1_custom_actions": {
"download_report": generate_schema(DownloadReportParams)
}
}
response = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
messages=messages,
metadata={"thesys": json.dumps(thesys_metadata)},
)
```
When the LLM generates a UI that includes a "Download Report" button, it will attach the action type `download_report` and the corresponding parameters to it.
#### 2. Handle the Custom Action (Frontend)
On the frontend, you use the same `onAction` callback from the core Actions guide. You simply add a new `case` to your `switch` statement to handle your custom action type.
**Using ``**
Your handler should check for the `download_report` action type and then trigger your application's logic, like opening a modal or starting a file download.
```tsx theme={null}
{
switch (event.type) {
// Your custom action case
case "download_report":
// Optionally add a message to the UI to confirm the action
pushUserMessageToChat(`Downloading ${event.params.reportType} report...`);
// Trigger your application's download logic with the action's parameters
downloadReport(event.params);
break;
// Built-in action cases
case "open_url":
window.open(event.params.url, "_blank", "noopener,noreferrer");
break;
case "continue_conversation":
default:
const { llmFriendlyMessage, humanFriendlyMessage } = event.params;
pushUserMessageToChat(humanFriendlyMessage);
callApi(llmFriendlyMessage);
break;
}
}}
/>
```
**Using ``**
If you are using persistence with the `useThreadManager` hook, you need to pass the `onAction` callback directly to the hook.
The `` component handles built-in actions automatically, so its `onAction` prop is used exclusively for your custom actions.
```tsx C1Chat theme={null}
{
// C1Chat handles 'continue_conversation' and 'open_url',
// so you only need to handle your custom actions.
switch (event.type) {
case "download_report":
// Trigger your application's logic
downloadReport(event.params);
break;
}
}}
/>
```
```tsx useThreadManager theme={null}
import { useThreadManager, C1Chat } from "@thesysai/genui-sdk";
const threadManager = useThreadManager(
apiUrl="/api/chat",
onAction={(event) => {
switch (event.type) {
case "download_report":
// Trigger your application's logic
downloadReport(event.params);
break;
}
}}
);
```
# Implementing Custom Components
Source: https://docs.thesys.dev/guides/custom-components
Provide your own components for the C1 API to render
## Introduction
Custom components provide a flexible and highly customizable way to introduce your own React components for the C1 API to use in its responses.
Custom components unlock a lot of tailor-made interfaces that your application might require, and it can now work within C1.
| Package | Minimum version |
| :-------------------- | :-------------- |
| `@crayonai/react-ui` | `0.8.31` |
| `@thesysai/genui-sdk` | `0.6.34` |
| `C1-Version` | `v-20250915` |
To illustrate the usefulness of custom components, imagine a flight booking service uses the C1 API to enable generative UI for its users.
The user has asked for LOS to JFK flights on September 23, 2025. With the existing C1 components, the information is presented to the user in a C1 Form for selection.
With custom React components, the application can customize how the flight list looks, and have internal states for flight cards and functionality that they want to enable for the user, such as API fetching, interactions, among others.
## Integrate Custom Components
The following is a step-by-step guide on how to plug in your React components, along with instructions for C1 to use them in its generations.
For a working example, kindly visit [Thesys C1 examples on
github](https://github.com/thesysdev/examples/tree/main/c1-custom-component)
Use `@thesysai/genui-sdk`'s `useOnAction` to get an `onAction` callback.
This callback is used to handle user interactions with components.
In addition to `onAction`, each component can have its own internal functions for `useC1State` management, API fetching, etc.
Use `useC1State` to maintain per-response state of the component within C1.
Refer to [Custom Components Specification](#custom-component-specification) below for more details.
```ts src/app/components.tsx theme={null}
import { useOnAction, useC1State } from "@thesysai/genui-sdk";
export const FlightList = ({ flights }: { flights: Flight[] }) => {
const onAction = useOnAction();
// State management to manage component's internal state
const { getValue, setValue } = useC1State("FlightList")
return (
);
};
```
In order to inform C1 about the expected props for your component, start by defining a `Zod`-based schema.
```ts src/app/api/chat/route.ts theme={null}
import { z } from "zod";
// Schema of a flight
const flightSchema = z
.object({
flightNumber: z.string(),
departure: z.string(),
arrival: z.string(),
layover: z.string().optional(),
...
})
.describe(
"Represents a single flight option including schedule, price, and stops."
);
// Schema of the flight list component props
const FlightListSchema = z
.object({
flights: z.array(flightSchema),
})
// Always include descriptive text for the LLM to understand the component
.describe(
"Displays a list of available flights. Renders rich cards with airline, route, times, stops, and price. Includes a clear 'Select' action for each item."
);
```
Using the schema described above, convert them as `JSON` schemas and send it as a part of your C1 API payload.
Since we used the name `FlightList` for the React component in the frontend,
the `CUSTOM_COMPONENT_SCHEMAS` must have the same key, `FlightList`.
```ts src/app/api/chat/route.ts theme={null}
// Schema to be passed to the C1 API
const CUSTOM_COMPONENT_SCHEMAS = {
FlightList: zodToJsonSchema(FlightListSchema),
... // other custom components
};
export async function POST(req: NextRequest) {
...
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed/",
apiKey: process.env.THESYS_API_KEY,
});
const llmStream = client.chat.completions.runTools({
...
model: "c1/anthropic/claude-sonnet-4/v-20250915",
// Pass custom component schema
metadata: {
thesys: JSON.stringify({
c1_custom_components: CUSTOM_COMPONENT_SCHEMAS,
}),
},
});
...
}
```
```python theme={null}
import json, jsonref
from typing import List, Optional
from pydantic import BaseModel
client = openai.OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
class Flight(BaseModel):
"""Represents a single flight option including schedule, price, and stops."""
flightNumber: str
departure: str
arrival: str
layover: Optional[str] = None
class FlightList(BaseModel):
"""Displays a list of available flights. Renders rich cards with airline, route, times, stops, and price. Includes a clear 'Select' action for each item."""
flights: List[Flight]
def generate_schema(model: BaseModel):
schema = model.model_json_schema()
schema = jsonref.replace_refs(schema, proxies=False) # dereference $refs and $defs
schema.pop("$defs", None) # remove $defs
schema.pop("$ref", None) # remove $refs
return schema
# Construct the metadata payload
thesys_metadata = {
"c1_custom_components": {
"FlightList": generate_schema(FlightList)
}
}
response = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250930",
messages=messages,
metadata={"thesys": json.dumps(thesys_metadata)},
)
```
Custom React components can be used by the `` and `` components.
#### C1Chat
```ts src/app/page.tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import { FlightList } from "./components";
export default function Home() {
return (
);
}
```
#### C1Component
```ts theme={null}
...
import { C1Component } from "@thesysai/genui-sdk";
import { FlightList } from "./components";
;
```
## Custom Component Specification
### useOnAction
Returns a callback to record a user action with both a user-facing label and
an LLM-oriented description.
**Returns**
Callback to dispatch the action.
**Callback Arguments**
Visible to the user; concise, human-readable label for the action. Used to
give feedback to the user about their action.
Eg: When submitting a form for a trip planner, the human-friendly message can be "Submit response".
Sent to the LLM; richer context describing what happened. Used to send the
contents of the user action to the LLM.
Eg: When submitting a form for a trip planner, the LLM-friendly message can be "User selected `${form.destination}`, from `${form.startDate}` to `${form.endDate}`".
Refer to [Interactivity](/guides/interactivity) for
more details on `onAction`, and how `human-friendly` and `llm-friendly`
messages work.
### useC1State
Access a named piece of component state with getter and setter helpers.
**Arguments**
The state field key you wish to read/write.
**Returns**
Function that returns the current state for the given `name`.
Updates the field and triggers any save/persist callbacks.
# Custom Markdown Responses
Source: https://docs.thesys.dev/guides/custom-markdown-responses
Learn how to render custom markdown responses in the C1 UI.
If you're using interceptors, or guardrails, you might want to return a custom response instead of having
the LLM generate a response for a user query. For example, you might want to return a fixed response when a user requests for some PII instead of passing it to
the LLM.
To do this, you can use the `makeC1Response` function to create a `c1Response` object, and then use the `writeCustomMarkdown` method to write the custom response to
the response object:
Use the `makeC1Response` function to create a `c1Response` object by importing it from the `@thesysai/genui-sdk` package, and start writing the LLM response
content to this object:
```ts app/api/chat/route.ts {6, 9, 36-38, 43, 54} [expandable] theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
To add a custom markdown response, use the `writeCustomMarkdown` method defined on the `c1Response` object:
When present in `c1Response`, custom markdown responses take priority over LLM responses on the UI (ie: they will be the only thing rendered when present in the response),
even if the LLM response is also present in `c1Response`.
Therefore, although not strictly necessary, it is recommended to return early when using custom markdown responses to avoid invoking the C1 API. This can prevent
unnecessary token usage.
```ts app/api/chat/route.ts {7-8, 19-31} [expandable] theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
// This is a hypothetical function that validates the user query based on some criteria, such as identifying if it contains or requests PII.
import { checkForPII } from "./guardrails";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam;
threadId: string;
responseId: string;
};
if (checkForPII(prompt)) {
c1Response.writeCustomMarkdown(
"I'm unable to assist with this request because it contains, or asks for, PII (*personally identifiable information*). Please remove any sensitive information and try again."
);
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
Your custom response will now be rendered in the UI when the guardrail is triggered:
The [`thesys_genui_sdk`](https://pypi.org/project/thesys-genui-sdk/) package provides a `C1Response` class that can be used to add data related to custom markdown responses to the response.
If you are using FastAPI, the package provides a handy decorator `with_c1_response` to make this even easier.
Use the `C1Response` class to create a `c1_response` object by importing it from the `thesys_genui_sdk` package.
```python FastAPI [expandable] theme={null}
# main.py
import os
from pydantic import BaseModel
from fastapi import FastAPI, Request
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message
import openai
app = FastAPI()
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
class Prompt(TypedDict):
role: Literal["user"]
content: str
id: str
class ChatRequest(BaseModel):
prompt: Prompt
threadId: str
responseId: str
@app.post("/chat")
# this decorator will add the c1_response in a context variable
# and internally return the stream from your endpoint.
@with_c1_response()
async def chat(request: ChatRequest):
await generate_llm_response(request)
async def generate_llm_response(request: ChatRequest):
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[request.prompt],
stream=True,
)
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
# get_assistant_message() allows you to get the full response to store for message history
assistant_message_for_history = get_assistant_message()
```
```python Framework-Independent [expandable] theme={null}
# main.py
import asyncio
import os
from thesys_genui_sdk import C1Response
import openai
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(c1_response: C1Response, prompt: str):
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await c1_response.write_content(content)
# c1_response.get_assistant_message() allows you to
# get the full response to store for message history
assistant_message_for_history = c1_response.get_assistant_message()
await c1_response.end()
async def main():
c1_response = C1Response()
# In a web server, you would start an async task
# to generate the response
asyncio.create_task(generate_llm_response(c1_response, "Tell me about latest trends in AI."))
# This is the stream you'd return from your route
response_stream = c1_response.stream()
# Example of how to consume the stream
async for item in response_stream:
print(item, end="")
if __name__ == "__main__":
asyncio.run(main())
```
To add a custom markdown response, use the `write_custom_markdown` function.
When present in `c1_response`, custom markdown responses take priority over LLM responses on the UI (ie: they will be the only thing rendered when present in the response),
even if the LLM response is also present in `c1_response`.
Therefore, although not strictly necessary, it is recommended to return early when using custom markdown responses to avoid invoking the C1 API. This can prevent
unnecessary token usage.
```python FastAPI {5, 8-11, 32-37} [expandable] theme={null}
# main.py
import os
from pydantic import BaseModel
from fastapi import FastAPI, Request
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message, write_custom_markdown
import openai
# This is a hypothetical function that validates the user query based on some criteria,
# such as identifying if it contains or requests PII.
from guardrails import check_for_pii
app = FastAPI()
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
class Prompt(TypedDict):
role: Literal["user"]
content: str
id: str
class ChatRequest(BaseModel):
prompt: Prompt
threadId: str
responseId: str
@app.post("/chat")
@with_c1_response()
async def chat(request: ChatRequest):
if check_for_pii(request.prompt.content):
await write_custom_markdown(
"I'm unable to assist with this request because it contains, or asks for, PII (*personally identifiable information*). Please remove any sensitive information and try again."
)
return
await generate_llm_response(request)
async def generate_llm_response(request: ChatRequest):
# ...
```
```python Framework-Independent {7-10, 22-26} [expandable] theme={null}
# main.py
import asyncio
import os
from thesys_genui_sdk import C1Response
import openai
# This is a hypothetical function that validates the user query based on some criteria,
# such as identifying if it contains or requests PII.
from guardrails import check_for_pii
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(c1_response: C1Response, prompt: str):
# ...
async def main(prompt: str):
c1_response = C1Response()
if check_for_pii(prompt):
await c1_response.write_custom_markdown(
"I'm unable to assist with this request because it contains, or asks for, PII (*personally identifiable information*). Please remove any sensitive information and try again."
)
await c1_response.end()
else:
asyncio.create_task(generate_llm_response(c1_response, prompt))
response_stream = c1_response.stream()
async for item in response_stream:
print(item, end="")
if __name__ == "__main__":
asyncio.run(main("User prompt with possible pii"))
```
Your custom response will now be rendered in the UI when the guardrail is triggered:
# Introduction
Source: https://docs.thesys.dev/guides/deployment/introduction
Learn how to deploy your C1 application
A C1 application can be deployed just like any other web application,
but if you have followed the [Quickstart](/guides/setup), there are 2 very simple ways to deploy your application:
}
/>
}
/>
# Deploying with Render
Source: https://docs.thesys.dev/guides/deployment/render
To deploy your C1 application with Render, follow these steps:
Login to Render using GitHub or any other way you want. You may need to create an account if you do not have a Render account already.
Create a new service to deploy your application. For a C1 application, select the "New Web Service" option.
Give Render access to your remote repositories. If you have your application in a GitHub repository, select the "GitHub" option to give Render permission to access your repositories.
Once Render has access to your repositories, select the C1 application repository you wish to deploy.
Configure the service by customizing the service name, region, and instance type. For a simple C1 application, the "Free" instance should suffice. Finally, add the
`THESYS_API_KEY` environment variable (and any other environment variables you need) to the service.
The "Free" instance of Render spins down after periods of inactivity and may take time to spin back up when accessed again. If you need more features and a permanently available service,
consider upgrading to a paid plan.
That's it! Click the "Deploy Web Service" button to complete the deployment. Your application should be available at the URL provided by Render.
# Deploying with Vercel
Source: https://docs.thesys.dev/guides/deployment/vercel
To deploy your C1 application with Vercel, follow these steps:
Vercel will need access to your code to deploy your application. The easiest
way to provide this access is to push your code to a GitHub repository.
Sign in to Vercel using your GitHub account (which has access to your
application's repository)
When prompted, install the GitHub application to your account. You can give the application access to all repositories or only the ones you want to deploy.
Import the repository you want to deploy.
Add the project name, the `THESYS_API_KEY` environment variable (and any other environment variables you need), and click deploy.
That's it! Your application should now be deployed to Vercel. You can add any custom domains, enable insights, or deploy a different branch to preview it instantly.
# Error Handling
Source: https://docs.thesys.dev/guides/error-handling
Pass custom `onError` callback to handle application level errors gracefully
`C1Component` accepts an `onError` callback to handle errors that occur during rendering.
## Basic Usage
```tsx theme={null}
import { C1Component } from "@thesysai/genui-sdk";
{
console.error(`C1 Error: ${code}`, c1Response);
}}
/>
```
## Error Object
The `onError` callback receives an object with:
| Property | Type | Description |
| ------------ | -------- | ----------------------------------------- |
| `code` | `number` | Error code indicating the type of error |
| `c1Response` | `string` | The response content at the time of error |
## Example: Display Error State
```tsx theme={null}
import { C1Component } from "@thesysai/genui-sdk";
import { useState } from "react";
function App() {
const [error, setError] = useState<{ code: number; message: string } | null>(null);
if (error) {
return (
Something went wrong (Error {error.code}). Please try again.
);
}
return (
{
console.error(`C1 Error: ${code}`, c1Response);
setError({ code, message: c1Response });
}}
/>
);
}
```
# Integrating Autogen with Thesys
Source: https://docs.thesys.dev/guides/frameworks/autogen
Give your Autogen agents interactive UI components and dynamic workflows powered by Thesys C1
Connect Autogen agents to Thesys to render interactive UIs, charts, and dynamic components instead of plain text responses.
This guide assumes you have basic knowledge of Python, Streamlit, and Autogen.
You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
```bash theme={null}
pip install -U "autogen-agentchat" "autogen-ext[openai]" "streamlit" "streamlit-thesys"
```
Create a model client that connects to Thesys API:
```python main.py theme={null}
import asyncio
import os
import streamlit as st
import streamlit_thesys as thesys
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_core.models import ModelInfo
model_client = OpenAIChatCompletionClient(
base_url="https://api.thesys.dev/v1/embed",
api_key=os.getenv("THESYS_API_KEY"),
model="c1/anthropic/claude-sonnet-4/v-20250815",
model_info=ModelInfo(
vision=False,
function_calling=True,
json_output=False,
family="unknown",
structured_output=True,
),
)
```
Define your agent with tools and system message:
```python main.py theme={null}
# Define a simple function tool
async def get_weather(city: str) -> str:
"""Get the weather for a given city."""
return f"The weather in {city} is 73 degrees and Sunny."
# Create the agent
agent = AssistantAgent(
name="weather_agent",
model_client=model_client,
tools=[get_weather],
system_message="You are a helpful assistant.",
reflect_on_tool_use=True,
model_client_stream=True,
)
```
Create a Streamlit app to run the agent and render responses:
```python main.py theme={null}
async def main() -> None:
st.title("Autogen Generative UI Chat")
task = st.text_input("Enter a task:", value="What is the weather in New York?")
if st.button("Run"):
with st.spinner("Running..."):
result = await agent.run(task=task)
if result.messages:
final_message = result.messages[-1]
thesys.render_response(final_message.content)
await model_client.close()
if __name__ == "__main__":
asyncio.run(main())
```
```bash theme={null}
export THESYS_API_KEY=
streamlit run main.py
```
Access your app at `http://localhost:8501` to interact with your Autogen agent through Thesys-powered UI.
Find more examples and complete code on our GitHub repository.
# Integrating C1 with CopilotKit
Source: https://docs.thesys.dev/guides/frameworks/copilotkit
Build Generative UI copilots with CopilotKit & Thesys C1
This guide demonstrates how to integrate **C1 by Thesys** with **CopilotKit** - the open-source
Agentic Application Framework for building in-app AI Copilots, CoAgents, and Generative UI agents.
Leveraging Agentic Generative UI, shared state, human-in-the-loop (HITL) patterns,
and the AG-UI protocol, this integration enables the creation of customizable, real-time, and production-ready AI copilots within your application.
This guide assumes you have basic knowledge of CopilotKit.
You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
```bash theme={null}
npx create-next-app@latest my-copilot-app
```
```bash theme={null}
npm install --save @copilotkit/runtime openai@4.85.1
```
```ts app/api/completion/route.ts theme={null}
import {
CopilotRuntime,
OpenAIAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import OpenAI from "openai";
import { NextRequest } from "next/server";
const openai = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
const serviceAdapter = new OpenAIAdapter({
openai,
model: "c1/anthropic/claude-sonnet-4/v-20250815",
});
const runtime = new CopilotRuntime();
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/chat",
});
return handleRequest(req);
};
```
In this guide we will be using the `CopilotSidebar` component to create a sidebar copilot.
But you can easily replace it with `CopilotChat` or `CopilotPopup` to create a chat or popup copilot.
```bash theme={null}
npm install --save @thesysai/genui-sdk @crayonai/react-ui @copilotkit/react-core @copilotkit/react-ui
```
```tsx app/layout.tsx theme={null}
import "./globals.css";
import { ReactNode } from "react";
import { CopilotKit } from "@copilotkit/react-core";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
```tsx app/page.tsx theme={null}
"use client";
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
import {
CopilotSidebar,
ImageRenderer,
UserMessage,
} from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import "@crayonai/react-ui/styles/index.css";
import { useCopilotChat } from "@copilotkit/react-core";
import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";
const AssistantMessageRenderer = ({ message, isGenerating }: any) => {
const { appendMessage } = useCopilotChat();
// In the Assistant Message, render the C1Component rather than rendering a markdown message.
return (
{
appendMessage(
new TextMessage({
role: MessageRole.User,
// Action is a object with 2 keys: llmFriendlyMessage and humanFriendlyMessage.
// We stringify it because the content field of CopilotKit is a string.
content: JSON.stringify(action),
})
);
}}
/>
);
};
const UserMessageRenderer = ({ message }: any) => {
// Since content can either be a string or a json object (in the case of an Action)
// we need to parse it and extract the humanFriendlyMessage incase its a json object.
let content = message?.content;
try {
const { humanFriendlyMessage } = JSON.parse(message?.content || "{}");
content = humanFriendlyMessage;
} catch (error) {}
return (
);
};
export default function Page() {
return (
);
}
```
```bash theme={null}
export THESYS_API_KEY=
npm run dev
```
Find more examples and complete code on our GitHub repository.
# Integrating CrewAI with Thesys
Source: https://docs.thesys.dev/guides/frameworks/crewai
Give your CrewAI agents interactive UI components, dynamic workflows, and live dashboards powered by Thesys C1
Supercharge your CrewAI agents with Generative UI powered by C1 by Thesys.
Instead of limiting agents to text responses, you can render interactive UIs, create dynamic workflows,
and plug in real-time dashboards - all generated on the fly.
This guide assumes you have basic knowledge of CrewAI.
You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
You can follow the [CrewAI Quickstart](https://docs.crewai.com/en/quickstart) to create a new CrewAI agent.
```bash theme={null}
crewai create crew crewai-genui
cd genui-agent
```
Extend the [`BaseLLM`](https://docs.crewai.com/en/learn/custom-llm) class to create a new LLM class for Thesys.
```python src/crewai-genui/thesys_llm.py [expandable] theme={null}
from crewai import BaseLLM, Task
import os
import requests
from typing import List, Optional, Union, Dict, Any
class ThesysLLM(BaseLLM):
def __init__(self, model: str="c1/anthropic/claude-sonnet-4/v-20250815", temperature: Optional[float] = None):
super().__init__(model=model, temperature=temperature)
self.endpoint = "https://api.thesys.dev/v1/embed"
self.api_key = os.getenv("THESYS_API_KEY")
def call(
self,
messages: Union[str, List[Dict[str, str]]],
tools: Optional[List[dict]] = None,
**kwargs: Any
) -> str:
"""Call the LLM with the given messages."""
# Convert string to message format if needed
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
# Prepare request
payload = {
"model": self.model,
"messages": messages,
"temperature": self.temperature,
}
# Add tools if provided and supported
if tools and self.supports_function_calling():
payload["tools"] = tools
# Make API call
response = requests.post(
self.endpoint + "/chat/completions",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
},
json=payload,
timeout=300
)
response.raise_for_status()
result = response.json()
return result["choices"][0]["message"]["content"]
```
In the `crew.py` file, the `researcher` and `reporting_analyst` agents would be defined. The job of
of the `reporting_analyst` agent is to analyze the research and provide a summary of the findings.
This is the perfect place to use Thesys as the LLM and generate a properly formatted report complete with
charts and tables rather than just a plain text response.
So lets go ahead and update the `reporting_analyst` agent to use Thesys.
```python src/crewai-genui/crew.py {5, 12} theme={null}
@CrewBase
class CrewaiGenUI:
def __init__(self):
super().__init__()
self.thesys = ThesysLLM()
@agent
def reporting_analyst(self) -> Agent:
return Agent(
config=self.agents_config['reporting_analyst'], # type: ignore[index]
verbose=True,
llm=self.thesys
)
```
Now that we have the `reporting_analyst` agent using Thesys, we need to render the response.
There are multiple ways to go about it but for this guide we will use the [Streamlit SDK](/guides/frameworks/streamlit)
to render the response.
First we need to install the dependencies.
```bash theme={null}
pip install streamlit streamlit-thesys
```
And then we need to update the `main.py` file to serve a streamlit app.
```python src/crewai-genui/main.py theme={null}
import streamlit as st
import streamlit_thesys as thesys
def streamlit_app():
# Streamlit app
st.title("CrewAI Research & Reporting")
topic = st.text_input("Enter topic:", value="AI LLMs")
if st.button("Run Analysis"):
inputs = {
'topic': topic,
'current_year': str(datetime.now().year)
}
with st.spinner("Running analysis..."):
try:
result = CrewaiGenui().crew().kickoff(inputs=inputs)
thesys.render_response(result.raw if hasattr(result, 'raw') else str(result))
except Exception as e:
st.error(f"Error: {e}")
if __name__ == "__main__":
streamlit_app()
```
```bash theme={null}
export THESYS_API_KEY=
streamlit run main.py
```
This will start the Streamlit app at `http://localhost:8501` and you should be able to run your crewai agent
using the UI.
Find more examples and complete code on our GitHub repository.
# Google ADK Integration
Source: https://docs.thesys.dev/guides/frameworks/google-adk
Build conversational AI applications with Google ADK's Python agent framework and C1
Google ADK (Agent Development Kit) is Google's official Python framework for building intelligent agents with conversational AI capabilities. It provides a model-agnostic agent orchestration layer that works with various LLM providers through LiteLLM integration.
This guide demonstrates how to integrate Google ADK with Thesys C1 to create a full-stack chat application with generative UI. You'll leverage ADK's agent framework, session management, and streaming capabilities while using C1 models through OpenAI-compatible APIs.
We'll build a complete application in two parts:
* **Backend**: A FastAPI server with Google ADK's LlmAgent framework and Thesys C1 models
* **Frontend**: A React interface with C1Chat for rich conversational UI
This guide assumes you have basic knowledge of Python, FastAPI, and React. You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
## Part 1: Backend Implementation
The backend uses Google ADK's agent framework with LiteLLM to create an intelligent assistant that processes messages and streams responses via Thesys C1.
### Set up the project structure
We'll create separate directories for backend and frontend to keep concerns separated.
Create a new directory for your Google ADK project:
```bash theme={null}
mkdir google-adk-c1
cd google-adk-c1
mkdir backend frontend
```
### Install Python dependencies
We need FastAPI for the web server, Google ADK for the agent framework, LiteLLM for model integration, and uvicorn for serving the application.
Create a `requirements.txt` file in the backend directory:
```txt backend/requirements.txt theme={null}
fastapi
uvicorn[standard]
python-dotenv
python-multipart
litellm
google-adk
```
Create and activate a virtual environment:
```bash theme={null}
cd backend
python -m venv venv
# On macOS/Linux:
source venv/bin/activate
# On Windows:
# venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
```
### Create configuration
Centralize all configuration in one file to make it easy to manage API keys, models, and settings. ADK uses LiteLLM which expects OpenAI-compatible environment variables.
Create a `config.py` file for environment configuration:
```python backend/config.py theme={null}
import os
from dotenv import load_dotenv
load_dotenv()
# OpenAI Configuration (can use OpenAI, Thesys, or any OpenAI-compatible API)
THESYS_API_KEY = os.getenv("THESYS_API_KEY", "")
THESYS_BASE_URL = "https://api.thesys.dev/v1/embed"
# Use litellm format: "openai/model-name" for LiteLLM in ADK
THESYS_MODEL = "openai/c1/anthropic/claude-sonnet-4/v-20251230"
# Server Configuration
PORT = int(os.getenv("PORT", "8000"))
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
# Google ADK Configuration
APP_NAME = "c1chat_assistant"
USER_ID = "unknown"
# System Prompt
SYSTEM_PROMPT = """You are a helpful AI assistant powered by Google's Agent Development Kit (ADK) with OpenAI.
You leverage ADK's agent framework for orchestration while using OpenAI models for generation.
Be friendly, concise, and helpful in your responses."""
```
### Create the ADK agent
The agent uses Google ADK's LlmAgent framework with LiteLLM to integrate with OpenAI-compatible APIs (like Thesys). ADK handles session management, streaming, and conversation orchestration.
Create the Google ADK assistant agent:
```python backend/agents/assistant.py theme={null}
"""
Assistant Agent - Main conversational agent using Google Agent Development Kit (ADK).
Uses OpenAI client through ADK's LiteLLM wrapper for model-agnostic agent framework.
Integrates with C1Chat interface through streaming responses.
"""
from typing import AsyncGenerator
import os
from google.genai.types import Content, Part
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.adk.agents.run_config import RunConfig, StreamingMode
from config import (
THESYS_API_KEY,
THESYS_BASE_URL,
THESYS_MODEL,
SYSTEM_PROMPT,
APP_NAME,
USER_ID,
)
class AssistantAgent:
"""
Assistant agent using Google ADK with OpenAI client via LiteLLM.
Leverages ADK's agent framework while using OpenAI (or compatible) models.
"""
def __init__(self):
"""Initialize the assistant agent with Google ADK + OpenAI."""
# Set OpenAI API key for LiteLLM
os.environ["OPENAI_API_KEY"] = THESYS_API_KEY
if THESYS_BASE_URL:
os.environ["OPENAI_API_BASE"] = THESYS_BASE_URL
# Create LiteLLM model instance pointing to OpenAI
model = LiteLlm(model=THESYS_MODEL)
# Define the ADK agent with OpenAI model
self.agent = LlmAgent(
model=model,
name="c1",
instruction=SYSTEM_PROMPT,
tools=[], # Add tools here as needed
)
# Session service for managing conversation state
self.session_service = InMemorySessionService()
# Runner to execute agent
self.runner = Runner(
app_name="c1chat_assistant",
agent=self.agent,
session_service=self.session_service,
)
async def process_message(
self, thread_id: str, user_message: str
) -> AsyncGenerator[str, None]:
"""
Process a user message and stream the response using Google ADK + OpenAI.
Args:
thread_id: Unique identifier for the conversation thread
user_message: The user's message content
Yields:
Chunks of the assistant's response in SSE format
"""
# Create content for ADK
content = Content(role="user", parts=[Part(text=user_message)])
session = await self.session_service.get_session(
app_name=APP_NAME, user_id=USER_ID, session_id=thread_id
)
if not session:
# If it doesn't exist, create it explicitly
session = await self.session_service.create_session(
app_name=APP_NAME, user_id=USER_ID, session_id=thread_id
)
# Configure streaming mode for real-time chunk-by-chunk streaming
run_config = RunConfig(
streaming_mode=StreamingMode.SSE,
response_modalities=["TEXT"],
)
# Run the agent with streaming enabled
async for event in self.runner.run_async(
user_id=USER_ID,
session_id=session.id,
new_message=content,
run_config=run_config,
):
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
yield part.text
# Global agent instance
assistant_agent = AssistantAgent()
```
Create the `__init__.py` file:
```python backend/agents/__init__.py theme={null}
from .assistant import assistant_agent
__all__ = ["assistant_agent"]
```
### Create the FastAPI server
The server exposes the `/api/chat` endpoint that C1Chat will connect to, handling CORS and streaming responses. ADK's runner handles the heavy lifting of agent execution.
Create the main FastAPI server with streaming support:
```python backend/main.py theme={null}
"""
FastAPI Server for Google ADK + C1Chat Integration
Provides streaming chat endpoint compatible with C1Chat component.
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional
import uvicorn
from agents.assistant import assistant_agent
from config import PORT, FRONTEND_URL
# Request/Response Models
class ChatMessage(BaseModel):
"""Chat message model"""
role: str
content: str
id: Optional[str] = None
class ChatRequest(BaseModel):
"""Chat request model compatible with C1Chat"""
prompt: ChatMessage
threadId: str
responseId: Optional[str] = None
# Initialize FastAPI app
app = FastAPI(
title="Google ADK + C1Chat API",
description="Backend API for Google ADK with C1Chat integration",
version="1.0.0",
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=[FRONTEND_URL, "http://localhost:5173", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
"""Health check endpoint"""
return {
"status": "ok",
"message": "Google ADK + C1Chat API is running",
"version": "1.0.0",
}
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy"}
@app.post("/api/chat")
async def chat(request: ChatRequest):
"""
Chat endpoint compatible with C1Chat component.
Streams responses from the ADK agent.
Args:
request: ChatRequest containing the user's message and thread ID
Returns:
StreamingResponse with text/event-stream content
"""
try:
# Extract user message
user_message = request.prompt.content
thread_id = request.threadId
# Return streaming response
return StreamingResponse(
assistant_agent.process_message(thread_id, user_message),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
},
)
except Exception as e:
print(f"Chat endpoint error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
print(f"Starting Google ADK + C1Chat server on port {PORT}")
print(f"Frontend URL: {FRONTEND_URL}")
print(f"API will be available at: http://localhost:{PORT}/api/chat")
uvicorn.run("main:app", host="0.0.0.0", port=PORT, reload=True, log_level="info")
```
### Set up environment variables
Store your Thesys API key securely in environment variables instead of hardcoding it.
Create a `.env` file in the backend directory:
```bash backend/.env theme={null}
# Required: Your Thesys API key
THESYS_API_KEY=your_thesys_api_key_here
# Optional: Server configuration
PORT=8000
FRONTEND_URL=http://localhost:5173
```
### Test the backend
Verify the server starts correctly and the health endpoint responds before building the frontend.
Run the backend server:
```bash theme={null}
cd backend
python main.py
```
The server will start on `http://localhost:8000`. You can test the health endpoint:
```bash theme={null}
curl http://localhost:8000/health
```
## Part 2: Frontend Implementation
Now let's create a React frontend that integrates with our Google ADK backend using C1Chat.
### Set up the frontend project
We'll use Vite with React and TypeScript for fast development and type safety.
Create a new React project with Vite:
```bash theme={null}
cd ..
npm create vite@latest frontend -- --template react-ts
cd frontend
```
### Install dependencies
The Thesys GenUI SDK provides the C1Chat component that connects to our backend.
Install the necessary packages for C1Chat integration:
```bash theme={null}
npm install @thesysai/genui-sdk
```
### Create the main App component
C1Chat handles all the UI complexity - just point it to your backend API endpoint.
Create the main App component with C1Chat:
```tsx src/App.tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
function App() {
const apiUrl =
import.meta.env.VITE_API_URL || "http://localhost:8000/api/chat";
return ;
}
export default App;
```
### Update the main entry point
Import C1 styles globally to ensure the chat interface renders correctly.
Update the main.tsx file to import C1 styles:
```tsx src/main.tsx theme={null}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@thesysai/genui-sdk/styles'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
,
)
```
### Set up environment variables (optional)
Override the default API URL if your backend runs on a different port or domain.
Create a `.env` file in the frontend directory:
```bash frontend/.env theme={null}
# Backend API URL (optional, defaults to localhost:8000)
VITE_API_URL=http://localhost:8000/api/chat
```
### Run the frontend
Start the development server with hot reload for instant updates during development.
With your backend server already running, start the frontend:
```bash theme={null}
npm run dev
```
Visit `http://localhost:5173` to interact with your Google ADK + C1Chat application!
## Running Both Servers
You'll need **two terminal windows**:
**Terminal 1 - Backend:**
```bash theme={null}
cd backend
source venv/bin/activate # Activate venv
python main.py
```
**Terminal 2 - Frontend:**
```bash theme={null}
cd frontend
npm run dev
```
Open `http://localhost:5173` in your browser.
## Understanding Google ADK Integration
This implementation leverages several key components of Google ADK:
### ADK Components Used
1. **LlmAgent**: The core agent class that orchestrates conversations with instructions and optional tools
2. **LiteLlm**: ADK's model adapter that supports OpenAI-compatible APIs through LiteLLM
3. **InMemorySessionService**: Manages conversation sessions and history across multiple threads
4. **Runner**: Executes agent operations with streaming support and session management
5. **StreamingMode.SSE**: Server-Sent Events streaming for real-time responses
### Why Use Google ADK?
* **Model Agnostic**: Switch between different LLM providers without changing agent code
* **Session Management**: Built-in conversation history and state management
* **Tool Integration**: Easy addition of function calling and external tools
* **Production Ready**: Includes proper session handling, error management, and streaming
* **Framework Features**: Leverage ADK's orchestration, multi-agent support, and extensibility
### Adding Tools to Your Agent
You can extend the agent with tools (function calling) by adding them to the `LlmAgent`:
```python theme={null}
from google.adk.tools import Tool
# Define a custom tool
weather_tool = Tool(
name="get_weather",
description="Get current weather for a location",
# ... tool implementation
)
# Add to agent
self.agent = LlmAgent(
model=model,
name="c1",
instruction=SYSTEM_PROMPT,
tools=[weather_tool], # Add your tools here
)
```
## Example Queries
Try these example queries to test your application:
* "Create a task list for planning a vacation"
* "Show me a comparison table of programming languages"
* "Generate a chart showing my weekly expenses"
* "Create a form to collect user feedback"
* "Help me organize my study schedule"
Find the complete code and more examples on our GitHub repository.
# Frameworks
Source: https://docs.thesys.dev/guides/frameworks/index
Integrating C1 into popular Open Source Frameworks
Since Thesys provides an [OpenAI compatible endpoint](/api-reference/endpoints/embed) any framework
that support OpenAI SDK can be used directly with C1.
Guides for some of the most popular frameworks can be found below:
### JS frameworks
Integrate C1 by Thesys into the [TypeScript AI toolkit](https://ai-sdk.dev/) by Vercel.
Ship Generative UI based Conversational Copilots/CoAgents with [CopilotKit](https://www.copilotkit.ai/).
Ship Generative UI Agents with powered by [Mastra](https://mastra.ai/) - the TypeScript Agent Framework.
### Python frameworks
Build conversational AI agents with [Google ADK](https://adk.iqai.com/docs) Python framework and rich generative UI.
Build a crew of AI agents powered by [CrewAI](https://www.crewai.com/) that can perform autonmous actions at scale.
Build GenUI conversational single and multi-agent applications with [Autogen](https://microsoft.github.io/autogen/stable//index.html)
and Thesys.
Add Generative UI capabilities to your [LangChain](https://www.langchain.com/) agents.
Add a agentic presentation layer to your [LangGraph](https://www.langchain.com/langgraph) agents at scale.
Supercharge your [Streamlit](https://streamlit.io/) data apps with Generative UI capabilities of C1 by Thesys.
### Missing your favourite framework?
Please reach out to us on [Discord](https://discord.gg/Pbv5PsqUSv) if you'd like to see support for your favourite framework.
# LangChain Integration
Source: https://docs.thesys.dev/guides/frameworks/langchain
Build powerful AI agents with LangChain and C1 using tools and SQL database integration
LangChain provides a robust framework for building AI applications with tool calling capabilities.
This guide demonstrates how to integrate LangChain with C1 to create an intelligent agent that can execute SQL queries and provide conversational interfaces.
We'll build a complete application in two parts:
* **Backend**: A FastAPI server with LangChain agents and SQL tools
* **Frontend**: A React interface to interact with the agent
This guide assumes you have basic knowledge of Python, LangChain, and React. You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
## Part 1: Backend Implementation
The backend uses LangChain to create an intelligent agent that can execute SQL queries on a Chinook database using C1 as the underlying LLM.
Create a new directory for your LangChain project and set up the basic structure:
```bash theme={null}
mkdir backend db
cd backend
```
Create a `requirements.txt` file with the necessary dependencies:
```txt backend/requirements.txt theme={null}
fastapi==0.104.1
uvicorn==0.24.0
langchain==0.1.0
langchain-openai==0.0.2
langserve==0.0.30
pydantic==2.5.0
sqlite3
```
Install the dependencies:
```bash theme={null}
cd backend
pip install -r requirements.txt
```
For this example, we'll use the Chinook database. Create it in your `db` folder using the SQL script:
```bash theme={null}
cd db
curl -s https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql | sqlite3 Chinook.db
```
Create the main backend file with LangChain integration:
```python backend/main.py theme={null}
#!/usr/bin/env python
import os
import sqlite3
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from langchain_core.runnables import RunnableLambda
from langchain.agents import create_openai_tools_agent, AgentExecutor
from fastapi import FastAPI
from langserve import add_routes
from pydantic import BaseModel
# Input model
class ChainInput(BaseModel):
c1Response: str = "" # Can be empty
query: str
# 1. Create model
model = ChatOpenAI(
base_url="https://api.thesys.dev/v1/embed",
model="c1/anthropic/claude-sonnet-4/v-20251230",
api_key=os.environ.get("THESYS_API_KEY")
)
# 2. Create SQL tool
@tool
def execute_sql_query(query: str) -> str:
"""Execute a SQL query on the Chinook database and return the results.
Args:
query: The SQL query to execute
Returns:
The query results as a formatted string
"""
try:
conn = sqlite3.connect('db/Chinook.db')
cursor = conn.cursor()
cursor.execute(query)
results = cursor.fetchall()
# Get column names
column_names = [description[0] for description in cursor.description]
# Format results
if not results:
return "No results found."
# Create a formatted table
formatted_results = []
formatted_results.append(" | ".join(column_names))
formatted_results.append("-" * len(" | ".join(column_names)))
for row in results:
formatted_results.append(" | ".join(str(value) for value in row))
conn.close()
return "\n".join(formatted_results)
except Exception as e:
return f"Error executing SQL query: {str(e)}"
# 3. Create parser
parser = StrOutputParser()
# 4. Create agent prompt template
prompt = ChatPromptTemplate.from_messages([
("system", """You are a helpful assistant that can answer questions about the Chinook digital media store database.
You have access to a SQL tool that can execute queries on the database. The database contains information about:
- Artists, Albums, and Tracks
- Customers and their purchase history
- Employees and sales data
- Playlists and media types
When users ask questions about the music store data, use the SQL tool to query the database and provide accurate information.
Context from previous conversation: {context}"""),
("human", "{query}"),
("placeholder", "{agent_scratchpad}")
])
# 5. Create agent and chain
tools = [execute_sql_query]
agent = create_openai_tools_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
def format_inputs(inputs: ChainInput):
"""Transform ChainInput to prompt variables and execute agent"""
context = inputs.get("c1Response", "No previous context")
# Execute the agent
result = agent_executor.invoke({
"context": context,
"query": inputs["query"]
})
return result["output"]
# Create a proper runnable chain
chain = RunnableLambda(format_inputs)
# 6. App definition
app = FastAPI(
title="LangChain + C1 Server",
version="1.0",
description="A simple API server using LangChain's Runnable interfaces powered by Thesys C1",
)
# 7. Adding chain route
add_routes(
app,
chain.with_types(input_type=ChainInput),
path="/chain",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=4001)
```
Export your Thesys API key:
```bash theme={null}
export THESYS_API_KEY=
```
Run the backend server:
```bash theme={null}
cd backend
python main.py
```
You can test the API at `http://localhost:4001/chain/playground` to see the interactive documentation.
## Part 2: Frontend Implementation
Now let's create a React frontend that integrates with our LangChain backend and uses C1 for the generative UI.
Create a new React project with Vite:
```bash theme={null}
cd ..
npm create vite@latest frontend -- --template react-ts
cd frontend
```
Install the necessary packages for C1 integration:
```bash theme={null}
npm install @crayonai/react-ui @thesysai/genui-sdk
```
Create the main App.tsx file that connects to your LangChain backend:
```tsx src/App.tsx theme={null}
import "@crayonai/react-ui/styles/index.css";
import { ThemeProvider, C1Component } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
import { useState } from "react";
import "./App.css";
function App() {
const [isLoading, setIsLoading] = useState(false);
const [c1Response, setC1Response] = useState("");
const [question, setQuestion] = useState("");
const makeApiCall = async (query: string, c1Response: string) => {
setIsLoading(true);
setC1Response("");
try {
const response = await fetch("/api/chain/invoke", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
input: { query, c1Response }
}),
});
const data = await response.json();
setC1Response(data.output);
} catch (error) {
console.error("Error:", error);
} finally {
setIsLoading(false);
}
};
return (
);
}
export default App;
```
Create a Vite configuration to proxy API calls to your backend:
```ts vite.config.ts theme={null}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:4001',
changeOrigin: true,
}
}
}
})
```
With your backend server already running, start the frontend:
```bash theme={null}
npm run dev
```
Visit `http://localhost:5173` to interact with your LangChain + C1 application!
## Example Queries
Try these example queries to test your application:
* "Show me the top 5 best-selling albums"
* "What are the most popular genres in the store?"
* "List all employees and their roles"
* "Show me customers from Canada"
* "What's the total revenue by country?"
## Key Features
* **LangChain Integration**: Uses LangChain's agent framework for tool calling
* **SQL Tool**: Executes dynamic SQL queries on the Chinook database
* **C1 Visualization**: Automatically visualizes responses with rich UI components
* **Context Awareness**: Maintains conversation context across interactions
* **Error Handling**: Robust error handling for both database and API calls
This example demonstrates the power of combining LangChain's agent capabilities with C1's generative UI. You can extend this by adding more tools, different databases, or custom UI components.
## Next Steps
* Add more tools to your LangChain agent (web search, calculations, etc.)
* Implement user authentication and session management
* Add streaming responses for better UX
* Deploy your application to production
Find more examples and complete code on our GitHub repository.
# Integrating Mastra with Thesys
Source: https://docs.thesys.dev/guides/frameworks/mastra
Give your Mastra agents interactive UI components and dynamic workflows powered by Thesys C1
Combine **Mastra's** orchestration with Generative UI from Thesys to move beyond text-only agents.
Render agentic UIs that include real-time dashboards, dynamic forms, and interactive
workflows - all generated from your agent logic.
With lightweight React or TypeScript integration, you can turn multi-agent workflows into
production-ready applications that feel natural, adaptive, and user-friendly.
This guide assumes you have basic knowledge of Mastra and NextJS.
You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
```bash theme={null}
npx create-next-app@latest thesys-mastra-app
```
Install the dependencies:
```json package.json theme={null}
{
"dependencies": {
"@ai-sdk/openai": "^2.0.23",
"@crayonai/react-ui": "^0.8.24",
"@crayonai/react-core": "^0.7.6",
"@crayonai/stream": "^0.6.4",
"@mastra/core": "^0.15.2",
"@mastra/libsql": "^0.13.7",
"@mastra/loggers": "^0.10.9",
"@thesysai/genui-sdk": "^0.6.31",
...
}
}
```
```bash theme={null}
npm install
```
You can follow the [Mastra Quickstart](https://mastra.ai/en/docs/getting-started/installation) to create a new Mastra agent
or just follow along this guide. We'll set up the example weather agent from the Quickstart but change
the LLM provider to Thesys.
```ts src/server/index.ts theme={null}
import { Mastra } from "@mastra/core/mastra";
import { weatherAgent } from "./agents/weather-agent";
export const mastra = new Mastra({
agents: { weatherAgent },
});
```
```ts src/server/agents/weather-agent.ts [expandable] theme={null}
import { createOpenAI } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { weatherTool } from "../tools/weather-tool";
export const weatherAgent = new Agent({
name: "Weather Agent",
instructions: `
You are a helpful weather assistant that provides accurate weather information and can help planning activities based on the weather.
Your primary function is to help users get weather details for specific locations. When responding:
- Always ask for a location if none is provided
- If the location name isn't in English, please translate it
- If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
- Include relevant detailsa like humidity, wind conditions, and precipitation
- Keep responses concise but informative
- If the user asks for activities and provides the weather forecast, suggest activities based on the weather forecast.
- If the user asks for activities, respond in the format they request.
Use the weatherTool to fetch current weather data.
`,
model: createOpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
}).chat("c1/anthropic/claude-sonnet-4/v-20250815"),
tools: { weatherTool },
});
```
```ts src/server/tools/weather-tool.ts [expandable] theme={null}
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
interface GeocodingResponse {
results: {
latitude: number;
longitude: number;
name: string;
}[];
}
interface WeatherResponse {
current: {
time: string;
temperature_2m: number;
apparent_temperature: number;
relative_humidity_2m: number;
wind_speed_10m: number;
wind_gusts_10m: number;
weather_code: number;
};
}
export const weatherTool = createTool({
id: 'get-weather',
description: 'Get current weather for a location',
inputSchema: z.object({
location: z.string().describe('City name'),
}),
outputSchema: z.object({
temperature: z.number(),
feelsLike: z.number(),
humidity: z.number(),
windSpeed: z.number(),
windGust: z.number(),
conditions: z.string(),
location: z.string(),
}),
execute: async ({ context }) => {
return await getWeather(context.location);
},
});
const getWeather = async (location: string) => {
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
const geocodingResponse = await fetch(geocodingUrl);
const geocodingData = (await geocodingResponse.json()) as GeocodingResponse;
if (!geocodingData.results?.[0]) {
throw new Error(`Location '${location}' not found`);
}
const { latitude, longitude, name } = geocodingData.results[0];
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
const response = await fetch(weatherUrl);
const data = (await response.json()) as WeatherResponse;
return {
temperature: data.current.temperature_2m,
feelsLike: data.current.apparent_temperature,
humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m,
windGust: data.current.wind_gusts_10m,
conditions: getWeatherCondition(data.current.weather_code),
location: name,
};
};
function getWeatherCondition(code: number): string {
const conditions: Record = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Foggy',
48: 'Depositing rime fog',
51: 'Light drizzle',
53: 'Moderate drizzle',
55: 'Dense drizzle',
56: 'Light freezing drizzle',
57: 'Dense freezing drizzle',
61: 'Slight rain',
63: 'Moderate rain',
65: 'Heavy rain',
66: 'Light freezing rain',
67: 'Heavy freezing rain',
71: 'Slight snow fall',
73: 'Moderate snow fall',
75: 'Heavy snow fall',
77: 'Snow grains',
80: 'Slight rain showers',
81: 'Moderate rain showers',
82: 'Violent rain showers',
85: 'Slight snow showers',
86: 'Heavy snow showers',
95: 'Thunderstorm',
96: 'Thunderstorm with slight hail',
99: 'Thunderstorm with heavy hail',
};
return conditions[code] || 'Unknown';
}
```
```tsx src/app/page.tsx theme={null}
"use client";
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
export default function Home() {
return ;
}
```
We will modify the chat endpoint from the [template app](https://github.com/thesysdev/template-c1-next/blob/main/src/app/api/chat/route.ts)
to use our newly created Mastra agent and return the response as a SSE stream.
```ts src/app/api/chat/route.ts [expandable] theme={null}
import { NextRequest, NextResponse } from "next/server";
import { getMessageStore, MastraMessage } from "./messageStore";
import { mastra } from "../../../server";
export async function POST(req: NextRequest) {
const { prompt, threadId } = (await req.json()) as {
prompt: { content: string };
threadId: string;
};
const messageStore = getMessageStore(threadId);
const agent = mastra.getAgent("weatherAgent");
// Prepare messages including history and current user message
const userMessage = {
id: crypto.randomUUID(),
role: "user" as const,
content: prompt.content,
createdAt: new Date(),
};
const messages = [
...messageStore.getMastraCompatibleMessageList(),
userMessage,
];
// Add the user message to the store first
messageStore.addMessage(userMessage);
// Use streaming for the response
const stream = await agent.streamVNext(messages);
// Create a readable stream that processes the agent stream
const readableStream = new ReadableStream({
async start(controller) {
try {
let fullContent = "";
for await (const chunk of stream.fullStream) {
if (chunk.type === "text-delta") {
const text = chunk.payload.text;
if (text) {
fullContent += text;
// Send the text chunk to the client
controller.enqueue(new TextEncoder().encode(text));
}
}
}
// After streaming is complete, save the assistant message
const assistantMessage: MastraMessage = {
id: crypto.randomUUID(),
role: "assistant",
content: fullContent,
createdAt: new Date(),
};
messageStore.addMessage(assistantMessage);
controller.close();
} catch (error) {
controller.error(error);
}
},
});
return new NextResponse(readableStream, {
headers: {
"Content-Type": "text/plain",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
```
```ts src/app/api/chat/messageStore.ts [expandable] theme={null}
// Mastra-compatible message type
export type MastraMessage = {
id: string;
role: "user" | "assistant" | "system";
content: string;
createdAt: Date;
threadId?: string;
};
const messagesStore: {
[threadId: string]: MastraMessage[];
} = {};
export const getMessageStore = (id: string) => {
if (!messagesStore[id]) {
messagesStore[id] = [];
}
const messageList = messagesStore[id];
return {
addMessage: (message: MastraMessage) => {
messageList.push({
...message,
threadId: id,
});
},
messageList,
getMastraCompatibleMessageList: () => {
return messageList;
},
};
};
```
We need to modify the `.env.local` file to include the Thesys API key and then add
the Mastra server to the `next.config.ts` file and start the app.
```ts next.config.ts theme={null}
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
serverExternalPackages: ["@server/*"],
};
export default nextConfig;
```
Now simply start the app:
```bash theme={null}
export THESYS_API_KEY=
npm run dev
```
Find more examples and complete code on our GitHub repository.
# Building AI powered data apps in Python with Streamlit
Source: https://docs.thesys.dev/guides/frameworks/streamlit
Learn how to integrate Streamlit with Thesys to create Generative UI apps, AI-powered dashboards, and interactive interfaces with Python.
With just a few lines of Python, you can build dynamic Streamlit dashboards, create low-code AI interfaces,
and scale to production faster - all while keeping the simplicity developers love about Streamlit.
This guide assumes you have basic knowledge of Python and Streamlit.
You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
## Visualizing data
```bash theme={null}
pip install streamlit streamlit-thesys pandas
```
```python theme={null}
import streamlit as st
import pandas as pd
import streamlit_thesys as thesys
# Load some example data
df = pd.read_csv("sales.csv")
api_key = os.getenv("THESYS_API_KEY")
st.title("Generative Visualizations with Thesys")
# Generate a chart dynamically
thesys.visualize(
instructions="Show monthly sales as a line chart",
data=df,
api_key=api_key
)
```
```bash theme={null}
export THESYS_API_KEY=
streamlit run app.py
```
## Using the LLM API directly
Since Thesys is a OpenAI compatible API, you can use it with any OpenAI compatible library.
```bash theme={null}
pip install openai
```
```python theme={null}
import os
import openai
import streamlit as st
import streamlit_thesys as thesys
client = openai.OpenAI(
base_url="https://api.thesys.dev/v1",
api_key=os.getenv("THESYS_API_KEY"))
response = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20250815",
messages=[{"role": "user", "content": "Population trend in the US"}])
st.title("Population trend in the US")
thesys.render_reponse(response.choices[0].message.content)
```
```bash theme={null}
export THESYS_API_KEY=
streamlit run app.py
```
# Integrating C1 with Vercel AI SDK
Source: https://docs.thesys.dev/guides/frameworks/vercel-ai-sdk
Build Generative UI applications powered by Vercel AI SDK
Vercel AI SDK provides a robust framework for building AI applications with tool calling capabilities.
This guide demonstrates how to integrate C1 by Thesys with Vercel AI SDK to build Generative UI agents.
This guide assumes you have basic knowledge of Vercel AI SDK and React.
You'll also need a Thesys API key from the [C1 Console](https://console.thesys.dev/keys).
Integrating Thesys C1 with `useCompletion` from Vercel AI SDK allows you to easily create
Generative UI based capabilities for your agent. `useCompletion` enables the streaming of text completions from your AI provider, manages the input state,
and updates the UI automatically as new messages are received.
```bash theme={null}
npx create-next-app@latest thesys-vercel-ai-sdk
```
```bash theme={null}
npm install --save ai @ai-sdk/openai
```
```ts app/api/completion/route.ts theme={null}
import { streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { prompt }: { prompt: string } = await req.json();
const result = streamText({
model: createOpenAI({
apiKey: process.env.THESYS_API_KEY,
baseURL: "https://api.thesys.dev/v1/embed",
}).chat("c1/anthropic/claude-sonnet-4/v-20250815"),
prompt,
});
return result.toUIMessageStreamResponse();
}
```
```bash theme={null}
npm install --save @thesysai/genui-sdk @crayonai/react-ui @crayonai/react-core
```
```tsx app/page.tsx [expandable] theme={null}
"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
export default function Page() {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
}),
});
const [input, setInput] = useState("");
const handleC1Action = ({ llmFriendlyMessage, humanFriendlyMessage }) => {
sendMessage({
text: llmFriendlyMessage,
metadata: { humanFriendlyMessage },
});
};
return (
);
}
```
```bash theme={null}
export THESYS_API_KEY=
npm run dev
```
Find more examples and complete code on our GitHub repository.
# Guiding UI generations
Source: https://docs.thesys.dev/guides/guiding-outputs
Customize UI generations using system prompts
System prompts let you control how the model generates UIs.
They act as **instructions that guide the model's behavior** before it sees any user input, ensuring more consistent outputs
while giving the developer fine-grained control over the generations.
You can try out these prompts in the [C1 Playground](https://console.thesys.dev/playground).
## Guiding outputs
You can give the agent rules that it must follow. For example, you can instruct the agent to use a specific UI component for a certain type of question. Here's a demonstration
of how each rule affects the agent's output:
| Rule | Output |
| -------------------------------------------------------------------------------------------------- | ---------------------------- |
| Use tables to show structured data, such as financial highlights or key executives. | |
| Use graphs to visualize quantitative information, like stock performance or revenue growth. | |
| Use carousels to show information about products from the company. | |
## Passing the the system prompt
### Writing the system prompts
You can use the rules you've prepared in the previous step to write a system prompt. You can also instruct the agent to play a specific role or use a specific tone / language
using this prompt.
```ts systemPrompt.ts theme={null}
export const systemPrompt = `
You are a business research assistant. Your goal is to provide concise and
accurate information about companies. When responding, follow these rules:
Rules:
- Use tables to show structured data such as financial highlights, key executives, or product lists.
- Use graphs to visualize quantitative information like stock performance or revenue growth.
- Use carousels to show information about products from the company.
`;
```
```python system_prompt.py theme={null}
SYSTEM_PROMPT="""
You are a business research assistant. Your goal is to provide concise and
accurate information about companies. When responding, follow these rules:
Rules:
- Use tables to show structured data such as financial highlights, key executives, or product lists.
- Use graphs to visualize quantitative information like stock performance or revenue growth.
- Use carousels to show information about products from the company.
"""
```
### Add the system prompt to the agent
You can add the system prompt to your message history when passing it to the SDK to ensure that it is always the first message of any conversation.
If you've used the quickstart template, you will just need to make the highlighted changes to start using the system prompt.
```ts app/api/ask/route.ts theme={null}
import { systemPrompt } from "./systemPrompt";
export async function POST(req: NextRequest) {
const runToolsResponse = openai.chat.completions.create({
...
messages: [
{ role: "system", content: systemPrompt}, // Prepend the system prompt as the first message.
...messages,
],
...
});
...
}
```
```python main.py theme={null}
from system_prompt import SYSTEM_PROMPT
@app.get("/chat")
def chat():
...
response = openai.chat.completions.create(
model='c1/anthropic/claude-sonnet-4/v-20251230',
messages=[
{ "role": "system", "content": SYSTEM_PROMPT}
...messages
],
);
```
# Components
### Default Components
By default C1 supports 50+ components and advanced layouting options.
See [Library](/library) for more details on the support components.
### Custom Components
At times you will need specific domain specific components that are not
supported by C1 out of the box. In such cases you can extend C1 generations
by adding your own [Custom Components](/guides/custom-components).
## Best practices
* Keep prompts short; prefer a few clear rules over many vague ones
* Avoid conflicts (e.g., “always use a chart” vs “always use a table”)
* Scope to your domain and surface; add examples you actually expect
* Test with representative queries; refine iteratively
# How C1 Works
Source: https://docs.thesys.dev/guides/how-c1-works
A detailed look at the C1 integration, from the backend API call to UI rendering.
## Overview
Here is a visual overview of the entire process, from a user's query to the final rendered UI:
```mermaid theme={null}
sequenceDiagram
participant User
participant UI
participant Your Backend
participant C1 API
User->>+UI: Submits a prompt (e.g., "What did I spend last month?")
UI->>+Your Backend: POST /api/generate-ui (sends user prompt)
Your Backend->>+C1 API: Calls C1 Chat Completions endpoint (with prompt, history, system instructions)
C1 API-->>-Your Backend: Returns a UI specification object (C1 DSL)
Your Backend-->>-UI: Relays the UI specification (C1 DSL)
UI->>-User: Renders the specification (C1 DSL) using C1Component
```
1. User enters a prompt on the UI
2. UI sends the prompt to your backend API
3. Backend calls the C1 API with the prompt, history, system instructions.
4. C1 API returns a UI specification object (C1 DSL) which is generated by existing LLM API based on the model selected while making the call.
5. Backend relays the C1 Response to the UI
6. UI renders the C1 Response using ``
Let's see how integration with C1 works.
## The Backend API Call
The core integration pattern involves your backend acting as an intermediary between your UI client and the C1 API.
This allows you to add business logic, prepare data before calling C1 and secure your API keys.
The C1 API is **OpenAI-compatible**, so you can use the official `openai` client library.
If you already have openai integrated, the only change required is to configure the client with your Thesys API key and the C1 `baseURL`.
Before making the call to C1, your server can enrich the user's prompt with additional context, such as conversation history,
system instructions, or [integrate data](/guides/integrate-data/tool-calling) from your database.
```python main.py theme={null}
from openai import OpenAI
import os
# Create OpenAI client with Thesys endpoint
client = OpenAI(
api_key=os.getenv('THESYS_API_KEY'),
base_url='https://api.thesys.dev/v1/embed'
)
# Now use the client for your AI requests
response = client.chat.completions.create(
model='',
messages=[
{'role': 'user', 'content': 'Hello, world!'}
]
)
```
```ts src/app/api/chat/route.ts theme={null}
const { OpenAI } = require('openai')
// Create OpenAI client with Thesys endpoint
const client = new OpenAI({
apiKey: process.env.THESYS_API_KEY,
baseURL: 'https://api.thesys.dev/v1/embed'
})
// Now use the client for your AI requests
const response = await client.chat.completions.create({
model: '',
messages: [
{ role: 'user', content: 'Hello, world!' }
]
})
```
The response from the C1 API is a structured UI specification (C1 DSL) that is then passed back to your UI.
## UI Rendering
The C1 **React SDK** provides `` to handle the rendering.
It is reponsible to take the response returned from C1 and render it as interactive React components.
If you are building a chat interface, you can use [``](/guides/conversational).
It provides everything including chat history, user thread management. This drastically reduces the development time.
## C1 Response
Unlike standard LLM responses, which are plain text or markdown, a C1 Response is a string that is structured payload to contain rich content.
It uses an XML-like structure to package multiple types of content into a single string.
It is generally stored as assistant message content in the database.
```xml theme={null}
.........
```
The different tags represent different parts of the response:
* ``: Data for displaying real-time thinking-state indicators.
* ``: The primary Generative UI response.
* ``: Document-style content, such as Slides or Reports.
C1 provides backend helpers to help you create and stream this response payload. For a detailed guide on this topic please refer to [C1 response](/sdk-reference/c1-response).
C1 response can be streamed to the UI and rendered using the `` or `` component.
## Selecting a model
The C1 API supports multiple models. You can select the model based on your use case.
Current supported models are:
1. Anthropic - Claude Sonnet 4
2. OpenAI - GPT-5
To see the full list of supported models, please refer to the [Models](/api-reference/models-and-compatibility) page.
## Summary
C1 gives you full control over the end-to-end experience. Your **backend** orchestrates the call by adding context and business logic.\
And your **UI** is responsible for fetching the resulting C1 Response and using the C1 SDK to render the final interactive UI.
# AI Coding IDE Setup
Source: https://docs.thesys.dev/guides/ide-setup
Configure your IDE to access Thesys documentation for context-aware assistance
The **Thesys Docs MCP** (Model Context Protocol) server enables AI coding assistants to access our documentation directly. This means your AI can provide more accurate, context-aware help when building with C1.
## Cursor Setup
[Cursor](https://cursor.com) is an AI-powered code editor that can connect to MCP servers for enhanced context. Here's how to add the Thesys Docs MCP server.
### Quick Install
Click the button below to automatically install the Thesys Docs MCP server in Cursor:
## Claude Code Setup
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) is Anthropic's agentic coding tool that supports MCP servers. Here's how to add the Thesys Docs MCP server.
### Add the MCP Server
Run the following command in your terminal to add the Thesys Docs MCP server:
```bash theme={null}
claude mcp add-json thesys-docs '{"type":"http","url":"https://thesys-kd1f.mcp.tadata.com"}'
```
### Verify the Connection
Once configured, Claude Code will have access to the Thesys Docs MCP server. You can verify the server is connected by running:
```bash theme={null}
claude mcp list
```
## Replit Setup
[Replit](https://replit.com) is a cloud-based IDE with AI capabilities. Click the button below to add the Thesys Docs MCP server to your Replit workspace:
[](https://replit.com/integrations?mcp=eyJkaXNwbGF5TmFtZSI6IlRoZXN5cyBEb2NzIiwiYmFzZVVybCI6Imh0dHBzOi8vdGhlc3lzLWtkMWYubWNwLnRhZGF0YS5jb20ifQ==)
## Other Clients
For setup instructions for other AI clients (Claude Desktop, Windsurf, Cline, GitHub Copilot), visit our [MCP configuration page](https://app.tadata.com/thesys-kd1f/instructions).
# Integrating C1 in your backend
Source: https://docs.thesys.dev/guides/implementing-api
Learn how to invoke the C1 API from your backend using OpenAI library
This guide walks you through setting up your client, structuring your API requests, and implementing the most common backend patterns for both conversational and standalone applications.
## Implementation
### 1. Setup and Authentication
C1 API is designed to be fully compatible with the OpenAI Chat Completions API. The recommended way to interact with the endpoint is to use the official OpenAI client library for your preferred language.
To get started, you need to initialize the client with your **Thesys API Key** and the **C1 Base URL**.
You can create a new API key from [Developer Console](https://console.thesys.dev/keys)
```python main.py theme={null}
import os
from openai import OpenAI
# Initialize the client with your C1 API Key and the C1 Base URL
client = OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed"
)
```
```typescript src/app/api/chat/route.ts theme={null}
import OpenAI from 'openai';
// Initialize the client with your C1 API Key and the C1 Base URL
const client = new OpenAI({
apiKey: process.env.THESYS_API_KEY,
baseURL: 'https://api.thesys.dev/v1/embed',
});
```
### 2. Structuring the API Request
The core of every request is the `messages` array, which provides the conversational context to the model.
The array consists of one or more message objects, each with a `role` and `content`:
* `role: 'system'`: Provides high-level instructions or context for the AI. This is typically the first message in the array.
* `role: 'user'`: Represents a prompt or message from the end-user.
* `role: 'assistant'`: Represents a previous response from the AI.
**Conversation history** is managed by including the sequence of past `user` and `assistant` messages in the array before adding the latest user prompt.
```json theme={null}
[
{ "role": "system", "content": "You are a helpful assistant that generates UI." },
{ "role": "user", "content": "Show me last month's sales." },
{ "role": "assistant", "content": "" },
{ "role": "user", "content": "Now break it down by region." }
]
```
You must also specify the `model` you wish to use. For a full list of available models, see the [Models and Pricing](/api-reference/models-and-compatibility) guide.
### 3. Implementation Patterns
Your implementation will depend on whether you are building a conversational application that needs to remember context or a standalone tool that handles one-off requests.
In a conversational pattern, your backend must store and manage the history of the conversation. On each new request, you retrieve the history, add the new user message, and then save the assistant's response to persist the context.
The following is a simplified but complete example using an in-memory array for history. In a production application, you would replace this with a database.
```python main.py (FastAPI) theme={null}
import os
from openai import OpenAI
from fastapi import FastAPI
from pydantic import BaseModel
# --- Basic Setup ---
app = FastAPI()
client = OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed"
)
# In-memory store for conversation history (for demonstration purposes)
conversation_history = [
{"role": "system", "content": "You are a helpful assistant."}
]
class ChatRequest(BaseModel):
prompt: str
# --- API Endpoint ---
@app.post("/chat")
def chat(request: ChatRequest):
# 1. Add the new user message to the history
conversation_history.append({"role": "user", "content": request.prompt})
# 2. Call the C1 API with the full history
completion = client.chat.completions.create(
model="c1-model-name",
messages=conversation_history,
)
assistant_response = completion.choices[0].message
# 3. Add the AI's response to the history
conversation_history.append(assistant_response)
# 4. Return the latest response content to the frontend
return assistant_response.content
```
```typescript route.ts (Next.js) theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from 'openai';
import { ChatCompletionMessageParam } from "openai/resources/index.mjs";
// --- Basic Setup ---
const client = new OpenAI({
apiKey: process.env.THESYS_API_KEY,
baseURL: 'https://api.thesys.dev/v1/embed',
});
// In-memory store for conversation history (for demonstration purposes)
const conversationHistory: ChatCompletionMessageParam[] = [
{ role: "system", content: "You are a helpful assistant." }
];
// --- API Endpoint ---
export async function POST(req: NextRequest) {
const { prompt } = (await req.json()) as { prompt: string };
// 1. Add the new user message to the history
conversationHistory.push({ role: "user", content: prompt });
// 2. Call the C1 API with the full history
const completion = await client.chat.completions.create({
model: 'c1-model-name',
messages: conversationHistory,
});
const assistantResponse = completion.choices[0].message;
// 3. Add the AI's response to the history
conversationHistory.push(assistantResponse);
// 4. Return the latest response content to the frontend
return assistantResponse.content;
}
```
For standalone tools, like generating a dashboard widget from a prompt, you don't need to manage a persistent conversation history. Each request is self-contained.
The `messages` array is typically simpler, containing just a `system` message for context and the single `user` message.
```python main.py (FastAPI) theme={null}
import os
from openai import OpenAI
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
client = OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed"
)
class GenerationRequest(BaseModel):
prompt: str
@app.post("/generate-widget")
def generate(request: GenerationRequest):
completion = client.chat.completions.create(
model="c1-model-name",
messages=[
{"role": "system", "content": "You generate UI widgets for a financial dashboard."},
{"role": "user", "content": request.prompt}
],
)
assistant_response = completion.choices[0].message
return assistant_response.content
```
```typescript route.ts (Next.js) theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.THESYS_API_KEY,
baseURL: 'https://api.thesys.dev/v1/embed',
});
export async function POST(req: NextRequest) {
const { prompt } = (await req.json()) as { prompt: string };
const completion = await client.chat.completions.create({
model: 'c1-model-name',
messages: [
{ role: "system", content: "You generate UI widgets for a financial dashboard." },
{ role: "user", content: prompt }
],
});
const assistantResponse = completion.choices[0].message;
return assistantResponse.content;
}
```
## C1 API Reference
* **Endpoint**: `POST /chat/completions`
* **Authentication**: The API uses `Authorization: Bearer `. The client libraries handle this header for you.
### Supported Parameters
The C1 API supports the following standard OpenAI chat completion parameters:
* `model` (string, required): The model ID to use for the generation.
* `messages` (array, required): A list of message objects that form the conversation history.
* `stream` (boolean, optional): If `true`, the response will be streamed back in chunks.
* `temperature` (number, optional): Controls randomness. Defaults to 1.0.
* `max_tokens` (integer, optional): The maximum number of tokens to generate.
* `top_p` (number, optional): Nucleus sampling parameter.
* `stop` (string or array, optional): Sequences where the API will stop generating further tokens.
### Error Handling
The API returns standard HTTP status codes and an error object compatible with OpenAI's format in case of failure.
````json theme={null}
{
"error": {
"message": "Invalid API key provided.",
"type": "invalid_request_error",
"param": null,
"code": "invalid_api_key"
}
}```
````
# Model Context Protocol (MCP)
Source: https://docs.thesys.dev/guides/integrate-data/mcp
Use MCP to extend your agents with custom capabilities
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that
allows your agents to connect securely to external tools and data sources.
Think of MCP as a "universal connector" for AI - it standardizes how language models interact
with various systems like databases, APIs, file systems, and custom tools.
MCP transforms your agents from isolated models into powerful assistants that can access real-time data,
perform actions, and interact with your entire digital ecosystem through a single, standardized protocol.
## Example: Using filesystem MCP
[Filesystem](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem)
is a simple MCP that allows the LLM to execute disk based tools on your server. For example,
list files, read file, write file etc. In this guide, we will integrate it with `` to create a
conversational agent that can answer questions about your filesystem.
### Setting up the MCP Client
First, let's install the necessary dependencies to work with MCP in your C1 application.
You'll need to install the MCP client library and any specific MCP servers you want to use. For this example, we'll use a filesystem MCP server.
```ts cli theme={null}
> npm install @modelcontextprotocol/sdk
```
```python cli theme={null}
> pip install mcp
```
### Create an MCP client integration
Now let's create the MCP client using the `@modelcontextprotocol/sdk` package.
This implementation connects to a filesystem MCP server and handles tool execution.
```ts app/api/chat/mcp.ts expandable theme={null}
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import OpenAI from "openai";
export class MCPClient {
private mcp: Client;
private transport: StdioClientTransport | null = null;
public tools: OpenAI.ChatCompletionTool[] = [];
constructor() {
this.mcp = new Client({
name: "c1-chat-mcp-client",
version: "1.0.0"
});
}
async connect() {
// Connect to filesystem MCP server (no authentication required)
const command = "npx";
const args = [
"-y",
"@modelcontextprotocol/server-filesystem@latest",
process.cwd(),
];
this.transport = new StdioClientTransport({
command,
args,
});
await this.mcp.connect(this.transport);
// List available tools from the MCP server
const toolsResult = await this.mcp.listTools();
this.tools = toolsResult.tools.map((tool) => ({
type: "function" as const,
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
},
}));
}
async runTool({
tool_call_id,
name,
args,
}: {
tool_call_id: string;
name: string;
args: Record;
}) {
try {
const result = await this.mcp.callTool({
name,
arguments: args,
});
return {
tool_call_id,
role: "tool" as const,
content: JSON.stringify(result.content),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
tool_call_id,
role: "tool" as const,
content: JSON.stringify({
error: `Tool call failed: ${errorMessage}`,
}),
};
}
}
async disconnect() {
if (this.transport) {
await this.transport.close();
}
}
}
```
```python mcp.py expandable theme={null}
import os
from typing import Any, Dict, List
from mcp.client import Client
from mcp.client.stdio import StdioClientTransport
class MCPClient:
def __init__(self) -> None:
self._client = Client(name="c1-chat-mcp-client", version="1.0.0")
self._transport: StdioClientTransport | None = None
self.tools: List[Dict[str, Any]] = []
async def connect(self) -> None:
# Connect to filesystem MCP server (no authentication required)
command = "npx"
args = [
"-y",
"@modelcontextprotocol/server-filesystem@latest",
os.getcwd(),
]
self._transport = StdioClientTransport(command=command, args=args)
await self._client.connect(self._transport)
# List available tools from the MCP server and map to OpenAI tool schema
tools_result = await self._client.list_tools()
self.tools = [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": tool.inputSchema,
"strict": True,
},
}
for tool in tools_result.tools
]
async def run_tool(self, *, tool_call_id: str, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
try:
result = await self._client.call_tool(name=name, arguments=args)
return {
"tool_call_id": tool_call_id,
"role": "tool",
"content": result.content,
}
except Exception as e: # noqa: BLE001
return {
"tool_call_id": tool_call_id,
"role": "tool",
"content": {"error": f"Tool call failed: {e}"},
}
async def disconnect(self) -> None:
if self._transport is not None:
await self._transport.close()
```
### Integrate MCP with your C1 agent
Now let's update your chat route to use the streamlined MCP integration from the thesysdev examples. This approach uses OpenAI's `runTools` method for automatic tool execution.
Install the dependencies for streaming
```ts cli theme={null}
> npm install @crayonai/stream
```
```python cli theme={null}
> # no package required
```
```ts app/api/chat/route.ts {5, 9-10, 17-22, 40-54} expandable theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { DBMessage, getMessageStore } from "./messageStore";
import { MCPClient } from "./mcp";
import { JSONSchema } from "openai/lib/jsonschema.mjs";
// Initialize MCP client
const mcpClient = new MCPClient();
interface RequestBody {
prompt: DBMessage;
threadId: string;
responseId: string;
}
async function ensureMCPConnection(): Promise {
if (mcpClient.tools.length === 0) {
await mcpClient.connect();
}
}
export async function POST(req: NextRequest): Promise {
const { prompt, threadId, responseId } = (await req.json()) as RequestBody;
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed/",
apiKey: process.env.THESYS_API_KEY,
});
// Ensure MCP connection is established
await ensureMCPConnection();
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
...messages,
{ role: "user", content: prompt }
],
tools: mcpClient.tools.map((tool) => ({
type: "function",
function: {
name: tool.function.name,
description: tool.function.description || "",
parameters: tool.function.parameters as unknown as JSONSchema,
parse: JSON.parse,
function: async (args: unknown) => {
const results = await mcpClient.runTool({
tool_call_id: tool.function.name + Date.now().toString(),
name: tool.function.name,
args: args as Record,
});
return results.content;
},
},
})),
stream: true,
});
const responseStream = transformStream(
llmStream,
(chunk) => {
return chunk.choices[0].delta.content;
},
) as ReadableStream;
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
```python main.py expandable theme={null}
import os
import json
from typing import Any, Dict, List
from fastapi import FastAPI
from pydantic import BaseModel
from openai import OpenAI
from mcp import MCPClient
app = FastAPI()
client = OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
mcp_client = MCPClient()
class ChatRequest(BaseModel):
prompt: str
history: List[Dict[str, Any]] = []
async def ensure_mcp_connection() -> None:
if not mcp_client.tools:
await mcp_client.connect()
@app.post("/chat")
async def chat(req: ChatRequest) -> Dict[str, Any]:
await ensure_mcp_connection()
messages: List[Dict[str, Any]] = [
*req.history,
{"role": "user", "content": req.prompt},
]
# First request with available tools from MCP
completion = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=messages,
tools=mcp_client.tools,
)
# Handle tool calls loop until the model returns a final answer
while True:
choice = completion.choices[0]
message = choice.message
tool_calls = message.tool_calls or []
if not tool_calls:
return message.content
# Append assistant message that requested tools
messages.append(
{
"role": "assistant",
"content": message.content or "",
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
for tc in tool_calls
],
}
)
# Execute tools (via MCP) and append results
for tc in tool_calls:
args = json.loads(tc.function.arguments or "{}")
tool_result = await mcp_client.run_tool(
tool_call_id=tc.id,
name=tc.function.name,
args=args,
)
messages.append(
{
"role": "tool",
"tool_call_id": tool_result["tool_call_id"],
"content": json.dumps(tool_result["content"]),
}
)
# Ask the model again with tool results
completion = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=messages,
tools=mcp_client.tools,
)
```
### Test your MCP-enabled agent
Your agent now has access to powerful filesystem operations through MCP! You can test it with prompts like:
* **File operations**: "Create a new file called 'notes.txt' with today's meeting summary"
* **Directory browsing**: "List all the files in the current directory"
* **File reading**: "Read the contents of package.json and summarize the project dependencies"
* **File searching**: "Find all TypeScript files in the src directory"
See the full code with integrations for thinking states and error handling.
# Integrate data via Tool Calling
Source: https://docs.thesys.dev/guides/integrate-data/tool-calling
Connect your data to the API endpoint to present data-based answers
Generative UI is most useful when the model can call into your data and services.
C1 supports **tool integration**, so that your UIs are powered by live data rather than raw LLM responses.
## What are tools?
A tool is an API or function you expose to the model.
Instead of guessing/hallucinating values, the model can call your tool and use the results to generate UI.
Examples of tools:
* Fetching live stock prices from a finance API
* Querying a database for users or orders
* Calling an internal microservice
* Running a calculation or simulation
## How tools fit into the flow
Tool calling / Function calling behaves in the same way as any standardized LLM endpoint.
To learn more in depth refer to the [OpenAI Guide](https://platform.openai.com/docs/guides/function-calling)
on how it works.
```mermaid theme={null}
flowchart LR
U[User request]:::node --> M["Thesys C1"]
M --> T["Tool call: API / DB / function"]
T --> M
M --> DSL["C1 DSL (typed UI spec)"]
DSL --> R["C1 React SDK"]
R --> UI["Rendered UI (chart, table, form)"]
```
## Example: Integrating Web Search
This guide demonstrates how to equip an agent with a web search tool, enabling it to provide up-to-the-minute information.
### 1. Define a tool for the agent to use
To get started, we need to define the tool and how the agent should use it. For our company's research assistant, a web search tool is crucial for gathering current information. This guide adds a web search tool powered by [Tavily](https://tavily.com) to search the web.
You may need to install additional dependencies such as `zod`, `zod-to-json-schema`, and `@tavily/core`. You can install them using npm:
```ts cli theme={null}
> npm install zod zod-to-json-schema @tavily/core
```
```python cli theme={null}
> pip install tavily-python
```
```ts app/api/chat/tools.ts theme={null}
import { JSONSchema } from "openai/lib/jsonschema.mjs";
import { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { tavily } from "@tavily/core";
const tavilyClient = tavily({ apiKey: process.env.TAVILY_API_KEY });
export const tools: [
RunnableToolFunctionWithParse<{
searchQuery: string;
}>
] = [
{
type: "function",
function: {
name: "web_search",
description:
"Search the web for a given query, will return details about anything including business",
parse: (input) => {
return JSON.parse(input) as { searchQuery: string };
},
parameters: zodToJsonSchema(
z.object({
searchQuery: z.string().describe("search query"),
})
) as JSONSchema,
function: async ({ searchQuery }: { searchQuery: string }) => {
const results = await tavilyClient.search(searchQuery, {
maxResults: 5,
});
return JSON.stringify(results);
},
strict: true,
},
},
];
```
```python tool.py theme={null}
import os
import json
from typing import Callable, Dict
from tavily import TavilyClient
tavily_client = TavilyClient(api_key=os.environ.get("TAVILY_API_KEY", ""))
def web_search(searchQuery: str) -> str:
results = tavily_client.search(query=searchQuery, max_results=5)
return json.dumps(results)
tools = [
{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web for a given query, will return details about anything including business",
"parameters": {
"type": "object",
"properties": {
"searchQuery": {
"type": "string",
"description": "search query",
}
},
"required": ["searchQuery"],
"additionalProperties": False,
},
"strict": True,
},
}
]
tool_impls: Dict[str, Callable[..., str]] = {
"web_search": web_search,
}
```
### 2. Instruct the agent to use the tool
Now that the agent has a tool, we need to teach it when and how to use it. A system prompt is the perfect way to provide these instructions.
We can tell the agent to use the `web_search` tool whenever it needs current information to answer a question about a company.
Here's a sample system prompt:
```ts app/api/chat/systemPrompt.ts theme={null}
export const systemPrompt = `
You are a business research assistant just like crunchbase. You answer questions about a company or domain.
given a company name or domain, you will search the web for the latest information.`;
```
```python system_prompt.py theme={null}
SYSTEM_PROMPT = """
You are a business research assistant just like crunchbase. You answer questions about a company or domain.
given a company name or domain, you will search the web for the latest information.
"""
```
### 3. Pass the tool to the agent
Now you just need to pass the tool call function to the agent so it can start using the tool. If you've followed the [Quickstart](/guides/setup) guide, you can
pass the tool call function to the agent by making a couple of small changes:
1. Import the `tools` and `systemPrompt` to your `route.ts` file.
2. Replace the `create` call in your `route.ts` file with a convenient `runTools` call that takes the list of tools available to the agent.
Here's an example of how to do this:
```ts src/app/api/route.ts theme={null}
import { systemPrompt } from "./systemPrompt";
import { tools } from "./tools";
export async function POST(req: NextRequest) {
...
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
{ role: "system", content: systemPrompt },
...messages
],
tools,
stream: true,
});
...
}
```
```python main.py theme={null}
import os
import json
from typing import Any, Dict, List, Optional
from fastapi import FastAPI
from pydantic import BaseModel
from openai import OpenAI
from tool import tools, tool_impls
from system_prompt import SYSTEM_PROMPT
app = FastAPI()
client = OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
def run_chat_with_tools(
messages: List[str]
) -> str:
messages: List[Dict[str, Any]] = [
{"role": "system", "content": SYSTEM_PROMPT},
...messages
]
completion = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=messages,
tools=tools,
)
while True:
choice = completion.choices[0]
message = choice.message
tool_calls = message.tool_calls or []
# If there are no tool calls, return the assistant's answer
if not tool_calls:
return message.content or ""
# Record the assistant message that requested tools
messages.append(
{
"role": "assistant",
"content": message.content or "",
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
for tc in tool_calls
],
}
)
# Execute tools and append results
for tool_call in tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments or "{}")
result = tool_impls[name](**args)
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
}
)
# Ask the model again with tool results
completion = client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=messages,
tools=tools,
)
@app.post("/chat")
def chat():
return run_chat_with_tools(messages)
```
### 4. Test it out
Try asking "Who is the current president of United States?"
# Two-Step Visualize Pattern
Source: https://docs.thesys.dev/guides/integration-pattern/visualize
Integrate C1 Visualize API with your existing LLM infrastructure for beautiful UI generation
This guide demonstrates how to integrate C1 Visualize API with your existing agentic application using a two-step architecture. This pattern allows you to leverage any LLM provider (OpenAI, Anthropic, etc.) for tool-calling and business logic, while using C1 exclusively for generating beautiful, interactive UI components.
## Overview
The two-step Visualize pattern separates concerns between your primary LLM and the UI generation layer:
1. **Step 1 - Business Logic**: Your primary LLM handles user requests, executes tool calls, and generates a final text/markdown response
2. **Step 2 - UI Generation**: C1 Visualize API converts the text response into interactive, generative UI components
This approach is ideal when:
* You want to use your existing LLM infrastructure for tool-calling
* You need to maintain complete conversation history with your primary LLM
* You want to add beautiful UI generation without refactoring your agent logic
For most applications, we recommend using [C1 as the Gateway LLM](/guides/integration-patterns#c1-as-the-gateway-llm-preferred) instead, which provides lower latency and better context awareness.
## Architecture
```mermaid theme={null}
sequenceDiagram
participant User
participant Application
participant LLM (OpenAI)
participant C1 Visualize API
participant ThreadStore
User->>Application: Sends message
Application->>LLM (OpenAI): Sends request (with history)
Note over LLM (OpenAI): Processes, potentially calls tools
LLM (OpenAI)-->>Application: Returns final text response
Application->>ThreadStore: Stores LLM interaction history (AIMessage)
Application->>C1 Visualize API: Sends final text response
C1 Visualize API-->>Application: Returns GenUI response
Application->>ThreadStore: Stores User message & GenUI response (UIMessage)
Application-->>User: Displays GenUI response
```
## Key Concepts
### Message Types
The pattern uses two distinct message storage strategies:
**AIMessage**: Complete conversation history with your primary LLM
* Includes user prompts, assistant responses, tool calls and their results
* Essential for maintaining context in subsequent LLM calls
**UIMessage**: Display-optimized messages for the frontend
* User prompts and final generative UI responses from C1
* Used for rendering the chat interface
### The Two-Step Flow
1. **Step 1**: Call your primary LLM with full conversation context, allowing it to use tools and generate a complete response
2. **Step 2**: Send the final LLM response to C1 Visualize API, which transforms it into rich UI components without needing tool access
## Setup
### 1. Install Dependencies
```bash npm theme={null}
npm install openai @thesysai/genui-sdk @crayonai/react-core @crayonai/react-ui @crayonai/stream
```
```bash pip theme={null}
pip install openai fastapi uvicorn thesys-genui-sdk
```
### 2. Environment Variables
Create a `.env` file with your API keys:
```bash theme={null}
OPENAI_API_KEY=your_openai_api_key
THESYS_API_KEY=your_thesys_api_key
```
You can create a new API key from [Developer Console](https://console.thesys.dev/keys)
### 3. Configure OpenAI Clients
Create two OpenAI client instances - one for your standard LLM calls and one for C1 Visualize:
```typescript TypeScript/Next.js theme={null}
import OpenAI from "openai";
// Standard OpenAI client for business logic and tool-calling
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// C1 Visualize client for UI generation
const thesysClient = new OpenAI({
baseURL: "https://api.thesys.dev/v1/visualize",
apiKey: process.env.THESYS_API_KEY,
});
```
```python Python/FastAPI theme={null}
import os
from openai import OpenAI
# Standard OpenAI client for business logic and tool-calling
openai = OpenAI(
api_key=os.environ["OPENAI_API_KEY"]
)
# C1 Visualize client for UI generation
thesys_client = OpenAI(
base_url="https://api.thesys.dev/v1/visualize",
api_key=os.environ["THESYS_API_KEY"],
)
```
## Implementation
### Backend API Route
Create an API route that orchestrates the two-step process:
```typescript app/api/chat/route.ts [expandable] theme={null}
import { NextRequest } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { tools } from "./tools";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const thesysClient = new OpenAI({
baseURL: "https://api.thesys.dev/v1/visualize",
apiKey: process.env.THESYS_API_KEY,
});
const SYSTEM_PROMPT = `You are a helpful assistant.
Your outputs will be processed by a downstream visualizer agent to generate a user interface.
The visualizer agent is responsible for generating a user interface based on your output but it does not have access to the tool calls or the database.
In your response, you should provide enough information with possible actions like buttons, forms, links, etc. that the user can take to interact with the UI.
If user asks for output in a specific component (e.g., graph, table, chart), generate the output in the requested format so the visualizer can render it properly. Give hints in your output so that the downstream agent can use those hints to generate UI properly.`;
export async function POST(req: NextRequest) {
const { prompt, threadId, responseId } = await req.json();
// Step 1: Call Standard OpenAI API
const previousAiMessages = await getAIThreadMessages(threadId);
// Use runTools helper to handle tool execution automatically
const runner = openai.beta.chat.completions.runTools({
model: "gpt-4",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
...previousAiMessages,
{ role: "user", content: prompt.content },
],
temperature: 0.1,
stream: false,
tools: tools,
});
// Wait for OpenAI to complete (including tool calls)
await runner.done();
const finalMessages = runner.messages;
// Step 2: Store AI Interaction History
const newAiMessagesToStore = [];
// Add the user prompt
newAiMessagesToStore.push(prompt);
// Track messages and extract final assistant message
let finalAssistantMessageForUI;
runner.on("message", (message) => {
newAiMessagesToStore.push({ ...message, id: crypto.randomUUID() });
if (typeof message.content === "string") {
finalAssistantMessageForUI = message;
}
});
// Step 3: Validate Final Assistant Content
if (!finalAssistantMessageForUI ||
typeof finalAssistantMessageForUI.content !== "string") {
console.error("No final assistant message found");
return new Response("", { status: 200 });
}
// Step 4: Call C1 Visualize API
const thesysStreamRunner = thesysClient.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
...previousAiMessages,
{ role: "user", content: prompt.content },
{ role: "assistant", content: finalAssistantMessageForUI.content },
],
stream: true,
tools: [], // No tools needed for Visualize API
});
// Visualize API returns only one assistant message
thesysStreamRunner.on("message", async (message) => {
// Step 5: Store messages after streaming completes
await addMessages(
threadId,
newAiMessagesToStore,
[prompt, { ...message, id: responseId }]
);
});
const llmStream = await thesysStreamRunner;
// Step 6: Stream Response to Client
const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0]?.delta?.content || "";
});
return new Response(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
For tool definitions, see the [Tool Calling guide](/guides/integrate-data/tool-calling) for complete examples of how to define and implement tools for your application.
```python main.py [expandable] theme={null}
import os
import json
from typing import List, Dict, Any, Optional
from fastapi import FastAPI
from pydantic import BaseModel
from openai import OpenAI
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content
from tools import tools, tool_impls
app = FastAPI()
# Standard OpenAI client for business logic and tool-calling
openai = OpenAI(
api_key=os.environ["OPENAI_API_KEY"]
)
# C1 Visualize client for UI generation
thesys_client = OpenAI(
base_url="https://api.thesys.dev/v1/visualize",
api_key=os.environ["THESYS_API_KEY"],
)
SYSTEM_PROMPT = """You are a helpful assistant.
Your outputs will be processed by a downstream visualizer agent to generate a user interface.
The visualizer agent is responsible for generating a user interface based on your output but it does not have access to the tool calls or the database.
In your response, you should provide enough information with possible actions like buttons, forms, links, etc. that the user can take to interact with the UI.
If user asks for output in a specific component (e.g., graph, table, chart), generate the output in the requested format so the visualizer can render it properly. Give hints in your output so that the downstream agent can use those hints to generate UI properly."""
class ChatMessage(BaseModel):
role: str
content: str
id: Optional[str] = None
class ChatRequest(BaseModel):
prompt: ChatMessage
threadId: str
responseId: str
def run_chat_with_tools(
messages: List[Dict[str, Any]]
) -> str:
"""Step 1: Call Standard OpenAI API with tool execution"""
completion = openai.chat.completions.create(
model="gpt-4",
messages=messages,
temperature=0.1,
tools=tools,
)
while True:
choice = completion.choices[0]
message = choice.message
tool_calls = message.tool_calls or []
# If there are no tool calls, return the assistant's answer
if not tool_calls:
return message.content or ""
# Record the assistant message that requested tools
messages.append(
{
"role": "assistant",
"content": message.content or "",
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
for tc in tool_calls
],
}
)
# Execute tools and append results
for tool_call in tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments or "{}")
result = tool_impls[name](**args)
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
}
)
# Ask the model again with tool results
completion = openai.chat.completions.create(
model="gpt-4",
messages=messages,
temperature=0.1,
tools=tools,
)
@app.post("/api/chat")
@with_c1_response()
async def chat(request: ChatRequest):
# Step 1: Get AI thread messages
previous_ai_messages = await get_ai_thread_messages(request.threadId)
# Prepare messages for OpenAI
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
*previous_ai_messages,
{"role": "user", "content": request.prompt.content},
]
# Step 2: Call OpenAI with tools and get final response
final_assistant_message = run_chat_with_tools(messages.copy())
# Step 3: Store AI interaction history
new_ai_messages = [
{"role": "user", "content": request.prompt.content, "id": request.prompt.id},
{"role": "assistant", "content": final_assistant_message, "id": request.responseId},
]
await add_ai_messages(request.threadId, new_ai_messages)
# Step 4: Call C1 Visualize API
visualize_messages = [
*previous_ai_messages,
{"role": "user", "content": request.prompt.content},
{"role": "assistant", "content": final_assistant_message},
]
stream = thesys_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=visualize_messages,
stream=True,
)
# Step 5: Stream response to client
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
# Step 6: Store UI messages
await add_ui_messages(
request.threadId,
[
{"role": "user", "content": request.prompt.content, "id": request.prompt.id},
{"role": "assistant", "content": final_assistant_message, "id": request.responseId},
]
)
# Helper functions for message storage (implement based on your database)
async def get_ai_thread_messages(thread_id: str) -> List[Dict[str, Any]]:
# Implement fetching AI messages from your database
pass
async def add_ai_messages(thread_id: str, messages: List[Dict[str, Any]]):
# Implement storing AI messages to your database
pass
async def add_ui_messages(thread_id: str, messages: List[Dict[str, Any]]):
# Implement storing UI messages to your database
pass
```
### Frontend Component
Use the GenUI SDK to render the chat interface:
```tsx app/page.tsx [expandable] theme={null}
"use client";
import "@crayonai/react-ui/styles/index.css";
import {
C1Chat,
useThreadListManager,
useThreadManager,
} from "@thesysai/genui-sdk";
import * as apiClient from "@/src/apiClient";
export default function Home() {
const threadListManager = useThreadListManager({
fetchThreadList: () => apiClient.getThreadList(),
deleteThread: (threadId) => apiClient.deleteThread(threadId),
updateThread: (t) => apiClient.updateThread(t),
createThread: (message) => apiClient.createThread(message.message),
});
const threadManager = useThreadManager({
threadListManager,
loadThread: (threadId) => apiClient.getMessages(threadId),
onUpdateMessage: ({ message }) => {
apiClient.updateMessage(
threadListManager.selectedThreadId,
message
);
},
apiUrl: "/api/chat",
});
return (
);
}
```
## System Prompt Best Practices
When crafting your system prompt for the primary LLM, include guidance for the downstream visualizer:
1. **Mention the Visualizer**: Inform the LLM that its output will be processed by a UI generator
2. **Provide Context**: Explain that the visualizer doesn't have access to tools or database
3. **Request Structure**: Ask for actionable elements like buttons, forms, and links
4. **Format Hints**: Suggest specific UI components when appropriate (tables, charts, etc.)
### Example System Prompt
```typescript theme={null}
const SYSTEM_PROMPT = `You are a [domain-specific] assistant.
**Your Workflow:**
1. [Describe your agent's workflow]
2. [List key actions and tools]
3. [Specify behavior patterns]
**Visualizer Agent:**
Your outputs will be processed by a downstream visualizer agent to generate a user interface.
The visualizer agent is responsible for generating a user interface based on your output but it does not have access to the tool calls or the database.
In your response, you should provide enough information with possible actions like buttons to select items, forms to submit data, links to navigate, etc. that the user can take to interact with the UI.
If user asks for output in a specific component (e.g., graph, table, chart), generate the output in the requested format so the visualizer can render it properly. Give hints in your output so that the downstream agent can use those hints to generate UI properly.`;
```
## Message Persistence
Store two types of messages with distinct purposes:
**AIMessage Table**
* Stores complete LLM conversation history including tool calls and results
* Used for context in subsequent LLM calls
**UIMessage Table**
* Stores user-facing messages with generative UI responses
* Used for rendering chat interface
### Example Prisma Schema
```prisma schema.prisma [expandable] theme={null}
model Thread {
id String @id @default(uuid())
title String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
aiMessages AIMessage[]
uiMessages UIMessage[]
}
model AIMessage {
id String @id @default(uuid())
threadId String
thread Thread @relation(fields: [threadId], references: [id], onDelete: Cascade)
role String
content String?
toolCalls Json?
toolCallId String?
name String?
createdAt DateTime @default(now())
}
model UIMessage {
id String @id @default(uuid())
threadId String
thread Thread @relation(fields: [threadId], references: [id], onDelete: Cascade)
role String
content String?
createdAt DateTime @default(now())
}
```
## Troubleshooting
Enhance your system prompt to provide more context about expected UI patterns and include relevant metadata in the assistant's final message.
```typescript theme={null}
const SYSTEM_PROMPT = `You are a ${domainName} assistant.
...
Include specific details about ${relevantContext} in your responses.`;
```
For development, you can truncate to recent messages. For production, use prompt caching with conversation compaction:
```typescript theme={null}
// Development: Keep only the last N messages
const recentMessages = conversationHistory.slice(-10);
// Production: Use prompt caching and compaction
// Compact older messages into summaries while keeping recent context
```
## Best Practices
1. **Separate Concerns**: Keep business logic in your primary LLM and UI generation in C1
2. **Rich Context**: Provide detailed information in the final assistant message for better UI generation
3. **Actionable Elements**: Always include next-step actions (buttons, forms) in your responses
4. **Message Storage**: Store both AI and UI message histories for optimal context management
5. **Error Recovery**: Implement graceful fallbacks if either API call fails
6. **Testing**: Test the full two-step flow with various query types and edge cases
This pattern introduces additional latency since you must wait for the primary LLM to complete before C1 can start streaming. For most applications, consider using [C1 as the Gateway LLM](/guides/integration-patterns#c1-as-the-gateway-llm-preferred) instead.
See a complete working implementation of this pattern in our e-commerce agent example with tools, database integration, and production-ready code.
# Integration Patterns
Source: https://docs.thesys.dev/guides/integration-patterns
How to integrate Generative UI with your existing applications.
## Definitions
* **Gateway LLM**: The gateway LLM is the first LLM that is invoked when the user interacts with your application.
Like traditional API Gateways, the gateway LLM is responsible for planning how to process the user's request and invoke
other LLMs, tools or sub-agents to generate the final response.
* **Presentation Layer LLM**: The presentation layer LLM is the LLM that is used to generate the UI response.
It may or may not be the same as the gateway LLM.
## C1 as the Gateway LLM (Preferred)
In this pattern, C1 is used as a gateway LLM that can be used to generate responses to user queries which can
then call other tools, LLMs or sub-agents to generate the final response. This is the recommended pattern for most
applications as it allows you to use the full power of C1 and results in no additional latency over your existing
LLM.
```mermaid theme={null}
sequenceDiagram
participant User
participant C1Gateway as C1
participant Tools
participant SubAgents as Sub-agents/Other LLMs
User->>C1Gateway: User Query
alt Tool Calling Required
C1Gateway->>Tools: Execute Tool Calls
Tools-->>C1Gateway: Tool Results
end
alt Sub-agent Required
C1Gateway->>SubAgents: Invoke Sub-agent/LLM
SubAgents-->>C1Gateway: Sub-agent Response
end
C1Gateway->>User: Generate UI Components & Stream Response
```
In a practical example, let's say you are currently using OpenAI GPT 4.1 as your gateway LLM.
Rather than calling the OpenAI API directly, you can use C1 endpoint with a model version that is built on top of GPT 4.1.
All your system prompts, tools, etc. would remain the same and the only change needed would be to use the `` component
to render the response.
This is the preferred pattern for integrating Generative UI into your existing applications.
## C1 as a Presentation Layer LLM
In some cases, you might not want to replace your gateway LLM with C1 due to a variety of reasons (for example if you are
using a custom-model). In such cases, you can use the presentation layer LLM pattern where your existing LLM first generates a text-only
response and then C1 is used to generate a UI response based on the text response.
```mermaid theme={null}
sequenceDiagram
participant User
participant Gateway as Gateway LLM
participant C1Presentation as C1 (Presentation Layer LLM)
participant Tools as Tools / Sub-agents
User->>Gateway: User Query
alt Tool Calling Required
Gateway->>Tools: Execute Tool Calls
Tools-->>Gateway: Tool Results
end
Gateway->>Gateway: Generate Complete Text Response
Gateway->>C1Presentation: Text Response (for UI generation)
C1Presentation->>User: Generate UI Components & Stream Response
```
**Pros:**
* Can support any LLM since now the business logic layer LLM is separate from the presentation layer LLM.
**Cons:**
* Introduces additional latency since now you'll have to wait for the entire text response to be generated before you C1 can start streaming
* Generally speaking, the UI generation works better when C1 has more context of the current state and available functionality.
Since the text response is generated by a different LLM, C1 will not have access to these details which it could have used to
generate a more accurate UI response.
This pattern is not recommended for most applications as it results in additional latency and is not as flexible as the
gateway LLM pattern.
## C1 as a Tool
In this pattern your gateway LLM decides when to invoke C1 which is exposed to it as a tool call.
When the gateway LLM invokes C1, it will pass the current state of the application to C1 as context which it
can use to generate a more accurate UI response.
This has to be the final tool call in the chain of tool calls and the response returned by C1 should be directly
streamed back to the UI without any additional processing.
```mermaid theme={null}
sequenceDiagram
participant User
participant Gateway as Gateway LLM
participant Tools as Tools / Sub-agents
participant C1Tool as C1 (Tool)
User->>Gateway: User Query
alt Other Tool Calls Required
Gateway->>Tools: Execute Other Tool Calls
Tools-->>Gateway: Tool Results
end
alt If Decided to Call C1
Gateway->>C1Tool: Tool Call with Context & Current State
C1Tool->>User: Generate UI Components & Stream Response
else If Not
Gateway->>User: Return Text Response
end
```
**Examples:**
* The user asks to generate a report on the sales of the last 30 days which invokes C1 as a tool call.
* Your agent needs input from the user to execute its task so it generates a live form via C1 to collect the required information.
**Pros:**
* Can support any LLM since now the business logic layer LLM is separate from the presentation layer LLM.
**Cons:**
* Introduces additional latency since now you'll have to wait for the entire text response to be generated before you C1 can start streaming
* Since the decision to invoke C1 is made by the gateway LLM, it is more error prone and requires more maintenance.
# Actions
Source: https://docs.thesys.dev/guides/interactivity/actions
Handling user interactions
Actions represent any user interaction, like a button click or form submission. They are the foundation of interactivity in C1.
C1 supports two categories of actions:
* **Built-in Actions**: Common, predefined behaviors for tasks like submitting data or opening links.
* **Custom Actions**: Application-specific logic that you can define and trigger from the C1 UI.
### The `onAction` Handler
To handle actions, you pass a callback function to the `onAction` property of the ``. This function is called every time a user performs an action that requires a response from application.
```tsx theme={null}
{
// This function will run for any action.
console.log("Action triggered:", event);
handleAction(event);
}}
/>
```
The `event` object passed to your handler contains all the information about the interaction:
* **`type`** (string): The type of action that was triggered (e.g., `'continue_conversation'`).
* **`params`** (object): A payload containing data specific to the action (e.g., `{ url: '...' }` or `{ llmFriendlyMessage: '...' }`).
### Handling Built-in Actions
Built-in actions cover the most common use cases for an interactive UI.
#### Handling in ``
For conversational applications, the `` component automatically handles built-in actions like form submissions (`continue_conversation`) and opening URLs (`open_url`). For more details, see the [Conversational UI](/guides/conversational) section.
#### Handling in ``
When using `` directly, you can use a `switch` statement on the `event.type` to handle different actions.
```tsx theme={null}
{
console.log("Action event:", JSON.stringify(event, null, 2));
switch (event.type) {
case "open_url":
// Handle client-side actions like opening a link.
window.open(event.params.url, "_blank", "noopener,noreferrer");
break;
case "continue_conversation":
default:
// Handle the generative loop.
const { llmFriendlyMessage, humanFriendlyMessage } = event.params;
// Optionally display the user-friendly message in the UI.
pushUserMessageToChat(humanFriendlyMessage);
// Send the detailed message to the backend.
callApi(llmFriendlyMessage);
break;
}
}}
/>
```
In the `continue_conversation` case, the `params` object contains two important messages:
* **`llmFriendlyMessage`**: A detailed, context-rich prompt designed to be sent to your backend for the LLM. If a form was submitted, this string will contain the form's values.
* **`humanFriendlyMessage`**: A concise, human-readable label for the action. In a chat UI, this is what you would display as the user's message.
### Custom Actions
Custom actions allow you to trigger your application's specific logic directly from the C1-generated UI, creating a seamless bridge between the generative interface and your existing application code.
Examples of what you can build with custom actions include:
* Implementing a "Download Report" button.
* Opening a product-specific checkout modal.
* Triggering a copilot action that performs a task within your application.
For more details, see the [Custom Actions](/guides/custom-actions) section.
### Actions in Custom Components
To add handling for actions in custom components, you can use the `useOnAction` hook. See [Custom Components](/guides/custom-components) for more details.
# Forms & Inputs
Source: https://docs.thesys.dev/guides/interactivity/forms
Prompt the C1 to generate forms, and how to handle their state and submissions on the frontend.
In many cases, a form is a much more efficient way to gather structured information from a user than a back-and-forth text conversation. For example, if a user wants to plan a trip, the C1 API can present a form asking for their destination, dates, and accommodation type all at once.
C1 supports different of input types that the LLM can use to build a form:
* Text and Number inputs
* Date input and Textarea
* Dropdowns, Checkboxes, and Radio buttons
* Sliders and Switches
### Generating Forms
There are two primary methods to instruct the C1 API to generate a form: automatically from a tool schema (recommended), or manually via a system prompt.
#### Automatic Generation from a Tool Schema (Recommended)
The most robust way to generate a form is to provide the LLM with the JSON schema of a tool you want it to use. See [Integrating data](/guides/integrate-data/tool-calling) for more details.
C1 automatically renders the required input fields based on the tool's parameters.
For example, to create a Jira copilot, you can provide the schema for a `create_jira_issue` tool.
```typescript theme={null}
const tools = [
{
type: "function",
function: {
name: "create_jira_issue",
description: "Create a Jira issue",
parameters: {
type: "object",
properties: {
title: {
type: "string",
description: "The title of the Jira issue"
},
priority: {
type: "string",
enum: ["Low", "Medium", "High"],
description: "The priority of the Jira issue"
},
description: {
type: "string",
description: "A multiline description for the issue"
}
},
required: ["title", "priority", "description"]
}
}
}
];
const response = await client.chat.completions.create({
model: "c1-model-name",
messages: [
{ role: "system", content: "You are a helpful Jira copilot." },
{ role: "user", content: "Create a Jira issue with the title 'New Feature'" }
],
tools: tools,
});
```
```python theme={null}
tools = [
{
"type": "function",
"function": {
"name": "create_jira_issue",
"description": "Create a Jira issue",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The title of the Jira issue"
},
"priority": {
"type": "string",
"enum": ["Low", "Medium", "High"],
"description": "The priority of the Jira issue"
},
"description": {
"type": "string",
"description": "A multiline description for the issue"
}
},
"required": ["title", "priority", "description"]
}
}
}
]
response = client.chat.completions.create(
model="c1-model-name",
messages=[
{"role": "system", "content": "You are a helpful Jira copilot."},
{"role": "user", "content": "Create a Jira issue with the title 'New Feature'"}
],
tools=tools,
)
```
In this scenario, C1 will automatically render a form with fields for `title`, `priority`, and `description`. It will even pre-fill the `title` field, since that value was already provided in the user's prompt.
#### Manual Generation via System Prompt
For simpler cases or more direct control, you can instruct the LLM to generate a specific form using a system prompt.
```md theme={null}
You are a helpful travel planner assistant.
When a user wants to plan a trip, you should generate a form with the following fields:
- Destination: A text input field.
- Dates: A date input field.
- Accommodation Type: A dropdown with options for Hotel, Apartment, or Hostel.
```
### How Forms Work on the Frontend
Once a form is rendered, the C1 SDK handles most of the complexity for you.
#### Automatic State Management
As a user types into a C1-generated form, the state of each input field is **managed automatically** within the component. You do not need to write `useState` or `onChange` handlers to track these values.
This internal state is the same state that is captured and persisted via the `updateMessage` callback, as described in the **[Managing State](/guides/interactivity/state)** guide.
#### Handling Form Submissions
When a user clicks the submit button on a C1-generated form, it triggers the **`onAction`** callback. The `action` object passed to your handler will contain all the data from the form.
Here is how you would handle a form submission on the frontend:
```tsx theme={null}
import { useState } from "react";
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
function MyFormComponent() {
const [c1Response, setC1Response] = useState(/* initial C1 DSL with a form */);
const handleFormSubmit = async (action) => {
// The `action.payload` contains the form data as a JSON object
// e.g., { destination: "Paris", accommodation_type: "Hotel" }
console.log("Form submitted with payload:", action.payload);
// This is the message that will be shown to the user
// e.g., "Plan my trip"
console.log("Message to be shown to user:", action.humanFriendlyMessage);
// e.g., "The user submitted the trip planning form with the following details..."
// The `action.llmFriendlyMessage` is a string pre-formatted for the LLM
console.log("Message for LLM:", action.llmFriendlyMessage);
// Send this message to your backend to generate the next UI
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ prompt: action.llmFriendlyMessage }),
});
const c1Response = await response.text();
setC1Response(c1Response);
};
return (
);
}
```
You need to send `action.llmFriendlyMessage` to C1 Api as user prompt from you backend.
# Interactivity
Source: https://docs.thesys.dev/guides/interactivity/index
Building dynamic, responsive user experiences by managing client-side state and handling server-side actions.
## Overview
C1 does more than just render static displays; it generates fully interactive components that can respond to user input, remember information, and dynamically update themselves. This guide provides a high-level overview of the concepts that make this interactivity possible.
### How Interactivity Works
In C1, a rich user experience is built upon two fundamental concepts: **Client-Side State** and **Actions**.
#### Client-Side State: Remembering User Input
C1 components are stateful. This means they can remember information within the client's browser, such as the text a user has typed into an input field or a selection from a dropdown menu. This state is managed automatically by the C1 SDK and can even be configured to persist across page reloads, ensuring a seamless user experience. Understanding state is the key to working with forms and creating custom components that feel native to your application.
#### Actions: Responding to User Input
An **Action** is any user interaction designed to trigger a response from your application. When a user clicks a button or submits a form, an action is triggered, which your application can then handle.
Actions are flexible and can be used to drive a variety of behaviors:
* **Client-side effects:** Handle an action entirely in the frontend to perform tasks like opening a URL, showing a modal, or updating local application state.
* **Generative updates:** Send the action's data to your backend server, which can then call the C1 API to generate a completely new C1 DSL response, dynamically updating the user interface.
This allows you to build everything from simple UI events to complex, evolving generative experiences.
### Guides in This Section
This section contains the following guides to help you master interactivity in C1.
* **[Managing State](/guides/interactivity/state)**
How C1 handles component state automatically and how to plug your own custom components into its persistent state system using the `useC1State` hook.
* **[Handling Actions](/guides/interactivity/actions)**
Dive deep into handling user interactions.
* **[Guide: Working with Forms](/guides/interactivity/forms)**
A practical guide on how C1 generates and manages forms, leveraging both the state and action systems.
* **[Guide: Building Multi-Step Flows](/guides/interactivity/multi-step-flows)**
A step-by-step tutorial on chaining actions together to create complex, workflow-driven user experiences.
# Building Multi-Step Flows
Source: https://docs.thesys.dev/guides/interactivity/multi-step-flows
This guide assumes you have read and understood **[Managing State](/guides/interactivity/state)**, **[Handling Actions](/guides/interactivity/actions)** and **[Working with Forms](/guides/interactivity/forms)**.
### Overview
A multi-step flow, or "chained action", is the pattern of using the result of one user action to generate a new UI, which in turn contains new actions for the user to take. This is how you move from single interactions to complete, dynamic applications.
In this tutorial, we will build a simple company research tool. The initial UI will show company details and two buttons: "View Products" and "View Locations." Clicking a button will replace the current UI with the requested information, demonstrating a complete generative loop.
You can checkout the full implementation [here](https://github.com/thesysdev/examples/tree/main/standalone-c1-component).
### The Core Pattern: The Generative Loop
The entire concept of a multi-step flow is built on the generative loop we introduced in the "Handling Actions" guide. This loop can be repeated indefinitely: **User Action → New Prompt → New UI**.
```mermaid theme={null}
sequenceDiagram
participant User
participant Frontend
participant Backend (LLM)
User->>Frontend: Clicks "View Products" button
note over Frontend: onAction triggered
Frontend->>Backend: Sends action's prompt
Backend->>Frontend: Returns new C1 DSL (Product List UI)
note over Frontend: UI updates to show products
User->>Frontend: Clicks "Product Details" button (in new UI)
note over Frontend: onAction triggered again
Frontend->>Backend: Sends new action's prompt
Backend->>Frontend: Returns new C1 DSL (Product Detail UI)
```
### Implementation
Let's build our company research tool.
#### Step 1: Generating the Initial UI
First, we need to instruct the LLM to generate our starting UI. We'll create a backend endpoint that uses a system prompt to ask for company information *and* the action buttons.
```md theme={null}
# System Prompt
You are a business research assistant. For a given company, you will provide a brief overview.
At the end of your response, you MUST add two buttons: "Products" and "Locations".
```
When your frontend calls this endpoint with a prompt like "Tell me about Apple," the C1 API will return a C1 DSL that renders the company overview and the two interactive buttons.
#### Step 2: Handling the Action on the Frontend
Next, we'll set up our React component to handle the user clicking one of the buttons. The `handleAction` function is the core of the chain. It takes the `llmFriendlyMessage` from the action (e.g., *"Show products for Apple"*) and sends it back to your backend API.
Your backend will call the C1 API with the new prompt along with the conversation history(optional), and the C1 API will return a new C1 response.
New response is used to add into converstaion or replace the old response.
```tsx theme={null}
import { useState } from "react";
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
function CompanyResearchTool() {
const [c1Response, setC1Response] = useState(/* initial UI with buttons */);
const [isLoading, setIsLoading] = useState(false);
const handleAction = async (action) => {
if (isLoading) {
return;
}
setIsLoading(true);
try {
// 1. Send the new prompt from the action to the same backend endpoint
const response = await fetch("/api/research", {
method: "POST",
body: JSON.stringify({ prompt: action.llmFriendlyMessage }),
});
const c1Response = await response.text();
// 2. Update the state, replacing the old UI with the new one
setC1Response(c1Response);
} finally {
setIsLoading(false);
}
};
return (
);
}
```
With this implementation, you have created a complete multi-step flow.
This new UI could contain even more actions—like a "View Details" button for each product—allowing the loop to continue as deep as your application requires.
# Managing State
Source: https://docs.thesys.dev/guides/interactivity/state
How C1 tracks and persists values in generated UIs, enabling forms, inputs, and custom components to work seamlessly.
### Introduction to C1 State
When C1 generates a live UI, it doesn't just render static components. It also tracks the values inside those components—things like form inputs, toggles, or selections. This is called **C1 State**.
**Why it matters**
C1 State makes interactive flows possible:
* Form fields can hold values until the user submits them.
* Inputs, dropdowns, or toggles can remember a user's selection.
* State can be persisted and when user reloads the chat from history, the components will be restored to the state they were in when the user last interacted with them.
Without C1 State, every refresh would reset the UI from scratch.
### Automatic State Management
For all built-in C1 components, such as forms, text inputs, and toggles, state is managed for you automatically by the C1 SDK.
You do not need to write your own `useState` or `onChange` handlers to track the values of these components. When a user types into a C1-generated input, its value is tracked internally, providing a seamless "it just works" experience out of the box.
### Persisting State Across Sessions
By default, C1 State is stored in the browser's memory and will be lost if the user refreshes the page. To create a persistent experience, you can save the state to your database whenever it changes.
The `` provides the `updateMessage` callback prop for this purpose. This function is called every time a state value changes within the UI, providing you with a complete snapshot of the C1 DSL that includes the latest state.
You can then save this updated DSL string to your database.
```tsx theme={null}
{
// `updatedC1Response` is the full C1 DSL string with the latest state merged in.
// Save this string to your database to persist the UI's current state.
saveToDatabase({
content: updatedC1Response,
});
}}
/>
```
The `updateMessage` callback provides the complete C1 DSL with state already merged. You only need to store this single string, not the state and the original DSL separately.
### State in Custom Components
When building your own interactive components, you can integrate them into C1's state system using the `useC1State` hook.
This hook works similarly to React's `useState` but ensures that your component's state is tracked and managed by C1. It takes a unique key as its argument, which is used to identify and store the state value within the C1 DSL.
```tsx theme={null}
import { useC1State } from '@thesysai/genui-sdk';
// A custom toggle component
function CustomToggle() {
// 'toggle-enabled' is the unique key for this state value
const [value, setValue] = useC1State('toggle-enabled');
return (
);
}
```
By using this hook, your custom components will behave just like native C1 components, with their state being automatically tracked, persisted, and available in actions.
For more details, see the complete **[Custom Components](/guides/custom-components)** guide.
# Migrating to Generative UI
Source: https://docs.thesys.dev/guides/migrate-to-genui
Integrating Thesys C1 API in an existing application
This is an advanced guide that assumes you already have a working text-based LLM application.
If you are starting from scratch, see the [Quickstart](/guides/setup) guide.
C1 by Thesys is designed to be a drop in replacement for OpenAI's API.
to your existing application, you can start using C1 to upgrade your regular LLM workflows to Generative UI.
The first step is to change the OpenAI SDK instantiation to point to C1 rather than the default OpenAI endpoint.
This change is generally only required in one place in your application - where you instantiate the Gateway LLM
(the first LLM that is invoked when the user interacts with your application).
```bash theme={null}
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
```
And then change the model name to use one of the [supported models](/api-reference/models-and-compatibility).
```ts theme={null}
const response = await client.chat.completions.create({
// [!code --]
model: "gpt-4o",
// [!code ++]
model: "c1/anthropic/claude-sonnet-4/v-20250815",
messages: [{ role: "user", content: "Hello" }],
});
```
Once you've done this successfully, you will be able to see your application rendering Thesys DSL responses as plain text.
Now that we are able to see the DSL responses, we can add the C1Component to our application to render the DSL responses as a live UI.
First, install the necessary packages for C1 integration:
```bash theme={null}
npm install --save @thesysai/genui-sdk @crayonai/react-ui @crayonai/stream @crayonai/react-core
```
Now simply replace the `Markdown` component with the `C1Component` component.
```tsx theme={null}
// [!code --]
import { Markdown } from "react-markdown";
// [!code ++:2]
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
export default function App() {
// other app related logic
return (
// [!code --]
{response}
// [!code ++:3]
)
}
```
The preferred font family for C1 is `Inter`. You can import this font in your CSS file as follows:
```css theme={null}
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap");
```
At this point, you should be able to see your application rendering the DSL responses as a live micro-frontend.
To improve the user experience, you can stream the responses from the backend to the frontend.
This reduces the perceived latency of the application and makes it feel more responsive.
`` supports streaming the responses and progressively rendering the UI by passing the `isStreaming` prop.
This prop should be set to `true` when the response is being streamed and `false` when the response is done streaming.
```tsx theme={null}
```
You can easily implement this by storing `isStreaming` as a state variable that is set to `true` when the `fetch`
request is made and set to `false` when the request is complete.
At this point, you should be able to see your application streaming the responses and progressively rendering the UI.
At this stage, you should be able to see the application rendering buttons and forms but they won't be functional.
In order to make them functional, you need to pass the `onAction` callback to the `C1Component` and implement the logic to handle the action.
In most cases, you will want to treat the `onAction` callback as a way to trigger the next turn of the conversation.
This means it will function as if the user had typed in a query and hit enter.
```tsx theme={null}
{
// - Trigger the next turn of the LLM
// - Send llmFriendlyMessage to the LLM so that the next turn can be triggered.
// - Show humanFriendlyMessage in the chat UI as the user's message
}}
/>
```
It is important to have the distinction to improve the user experience. The `llmFriendlyMessage` would not be suitable for humans to read in most cases.
Your message store probably won't have the `llmFriendlyMessage` in it but in the long term, it is a good idea to store it in the message store.
Once you've implemented this all the buttons and forms should be functional.
While your chat interface is working, the form values don't persist when you refresh the page.
This is because the form values are also stored in the `c1Response` object. To enable persistence,
you can pass the `updateMessage` callback to the `C1Component` and implement the logic to persist the form values.
Typically `PUT` or `PATCH` requests are used to update the message in the database.
You might have to implement this endpoint in your backend if it's not already implemented.
```tsx theme={null}
{...}
updateMessage={(message) => {
// Update the message to the database
}}
/>
```
Once you've implemented this, you should be able to see UI generations, have functional buttons and forms and
the form values would persist when you refresh the page.
C1Component is a powerful abstraction that can be used to embed Generative UI within your application.
Above is a screenshot of a fork of [HuggingFace Chat UI](https://github.com/rabisg/chat-ui) integrated with C1Component
# Rendering C1 Responses into live UI
Source: https://docs.thesys.dev/guides/rendering-ui
C1 React SDK helps convert C1 API Responses into live UI components on the client side.
This guide covers the necessary setup and the core components provided by the SDK to get your frontend up and running.
## Setup
**Install the SDK**
You will need two packages: `@thesysai/genui-sdk` which provides the core rendering logic, and its peer dependency `@crayonai/react-ui` which contains the base UI primitives.
```bash theme={null}
npm install @thesysai/genui-sdk @crayonai/react-ui
```
**Import Required CSS**
For the components to be styled correctly, import the Crayon UI stylesheet at the root of your application (e.g., in `layout.tsx` or `App.tsx`).
```tsx theme={null}
import "@crayonai/react-ui/styles/index.css";
```
**Add the Default Font**
Inter is the default font used by C1 components. You can add it to your project by importing it in your global CSS file.
The font and other styles can be [customized](/guides/styling) later.
```css global.css theme={null}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
```
## Implementation
### The Core Renderer: ``
`` is the fundamental building block for rendering any C1 API response.
Its primary responsibility is to take the C1 response string returned from your backend API and transform it into a tree of interactive React components.
You are responsible for fetching the data from your backend and passing the resulting C1 response string to the `c1Response` prop.
```tsx app/page.tsx theme={null}
import { C1Component } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
const App = () => {
const response = await fetch("/chat");
return ;
};
```
### Providing a Theme with ``
C1Component must be wrapped in a `` to ensure it is styled correctly and have access to your application's design tokens.
This provider should be placed at the highest level in your application tree, enclosing any component that uses ``.
```tsx app/page.tsx theme={null}
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
const App = () => {
const response = await fetch("/chat");
return (
);
};
```
`ThemeProvider` allows you to customize the generations to match your brand & design system.
To learn more about customizing C1 Outputs please refer to our [Customization Section](/guides/styling)
### Handling User Actions with `onAction` callback
The UI generated by `` is not static; it can include interactive elements like buttons and forms.
You can handle events from these elements, such as form submissions, by using the `onAction` prop.
For a complete guide on this topic, see the guide on **[Interactivity and Actions](/guides/interactivity/actions)**.
### Implementing Conversational interfaces with ``
For conversational use cases, the SDK provides ``, a "batteries-included" component that simplifies the process.
This is an alternative to `` that wraps `` and `` and also manages its own state,
data fetching, and conversation history runtime.
Simply provide your backend API endpoint to the `apiUrl` prop, and the component will handle the rest.
* **Use ``** for quickly building chat applications.
* **Use ``** for non-chat UIs or when you need full control over data fetching and state management.
```tsx app/page.tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
const App = () => {
return ;
};
```
For a complete tutorial on using ``, see the section on **[Conversation UI](/guides/conversational)**.
# Response Footer
Source: https://docs.thesys.dev/guides/response-footer
Learn how to customize the C1 chat experience by adding a component to the footer of a response.
A response footer is a component that can be displayed at the bottom of a response after it has finished streaming.
This is useful for adding actions like regenerating a response, copying the content to the clipboard, or providing feedback (thumbs up/down).
The `@thesysai/genui-sdk` package provides a way to add a response footer via the `C1Chat` component. You can use the pre-built components that come with the SDK for common actions.
Here's how to add a response footer using the pre-built components:
The SDK provides a set of pre-built components that you can use to quickly create a response footer. These components are available under the `ResponseFooter` export.
First, create a new component that will wrap your footer elements. This component must be of type `ResponseFooterComponent`, which will provide it with `threadId` and `messageId` props. Then, use `ResponseFooter.Container` as a wrapper and add the desired buttons inside it.
The `ResponseFooter` component is meant for convenience. If you need more control over the footer, you can create your own component. As long as your
component adheres to the `ResponseFooterComponent` type, it can be passed to the `C1Chat` component.
Here's an example of how to use the pre-built `ShareButton`, `ThumbsUpButton`, and `ThumbsDownButton`:
```tsx FooterComponent.tsx theme={null}
import {
ResponseFooter,
type ResponseFooterComponent,
} from "@thesysai/genui-sdk";
const FooterComponent: ResponseFooterComponent = ({ messageId, threadId }) => {
return (
{
// The 'message' object is available here
// call an api to generate a share link for the provided message
const shareableLink = await generateShareLink(message);
return shareableLink;
}}
/>
{
// you can call an api to send feedback to your server here using messageId
console.log(
`Thumbs up for message: ${messageId} in thread: ${threadId}`
);
}}
/>
{
// you can call an api to send feedback to your server here using messageId
console.log(
`Thumbs down for message: ${messageId} in thread: ${threadId}`
);
}}
/>
);
};
export default FooterComponent;
```
The `ResponseFooterComponent` receives `messageId` and `threadId`, which you can use in your click handlers and backend API calls.
Next, pass your `FooterComponent` to the `C1Chat` component via the `customizeC1` prop.
```tsx App.tsx focus={8} theme={null}
import { C1Chat } from '@thesysai/genui-sdk';
import FooterComponent from './FooterComponent';
export default function App() {
return (
);
}
```
The pre-built `ResponseFooter` components accept props to customize their behavior.
#### `ResponseFooter.Container`
This is a simple container component that you can use to wrap your footer buttons. It provides basic styling and layout. It accepts `className` and `animate` props.
Whether to animate the footer when it is shown.
Takes class names as a string to modify the default styling.
#### `ResponseFooter.ShareButton`
A function that is called when the "Generate Link" button is clicked in the
share modal. It receives the full `message` object and should return a promise
that resolves to a shareable URL. The button handles the UI for copying the
link to the clipboard and shows a confirmation.
#### `ResponseFooter.ThumbsUpButton` & `ResponseFooter.ThumbsDownButton`
Both buttons use Crayon's `IconButton` component with `tertiary` as the default variant. For details on all the props that they accept, refer to Crayon's
[IconButton](https://crayonai.org/ui/?path=/docs/components-iconbutton--docs) documentation.
You should now see the response footer with the share and feedback buttons displayed below the agent's messages.
# Security & Compliance
Source: https://docs.thesys.dev/guides/security
C1 is a hosted service that is built to be secure and compliant.
When using C1 via the API all data is encrypted in transit & no data is retained on our servers.
We follow the highest standard of security practices to protect your data.
## FAQs
Yes, all data is encrypted in transit and at rest.
No user data is retained on our servers while using the API.
We only store metadata about your requests and usage that is necessary
for purposes like billing and improving our service.
We use US based regions via cloud providers.We utilize cloud provider regions
based in the United States to host our services, ensuring optimal performance, reliability, and compliance.
Yes, we can work with you to set up a private C1 environment.
No, we do not support bringing your own LLM keys however we can work with you to set up a private C1 environment.
Please reach out to us at [security@thesys.dev](mailto:security@thesys.dev).
Yes, we are SOC 2, ISO 27001, and GDPR compliant. For more information, please visit our trust center at [trust.thesys.dev](https://trust.thesys.dev).
# Quickstart
Source: https://docs.thesys.dev/guides/setup
Get started quickly with building your first Generative UI application
## Overview
This guide provides a quickstart for building your first Generative UI application with C1.
## Components
C1 provides two components for building Generative UI applications:
* ``: A component that renders the Generative UI.
* [``](/guides/conversational): A pre-built chat component that includes a chat history, a message composer, and a loading indicator.
If you're unsure about which component to use check our [comparison guide](/guides/c1chat-vs-c1component)
for more details.
Check out the [Basics section](/guides/basics) for further details on implementing backend API and rendering the UI.
## NextJS
The easiest way to begin is by using C1 within a [NextJS](https://nextjs.org) project
### Create with the CLI
1. Create a new app called `my-c1-project`
2. `cd my-c1-project` and start the dev server
3. Visit [http://localhost:3000](http://localhost:3000)
```bash theme={null}
npx create-c1-app
cd my-c1-project
npm run dev
```
### System requirements
* Node.js 20.9 or later
* macOS, Windows or Linux
***
## Python
```bash theme={null}
git clone https://github.com/thesysdev/template-c1-fastapi.git my-c1-project
```
### Start the python server
```bash theme={null}
cd my-c1-app/backend
pip install -r requirements.txt
uvicorn main:app --reload
```
### Start the frontend server
```bash theme={null}
cd my-c1-app/ui
npm install
npm run dev
```
### System requirements
* Node.js 20.9 or later (for the frontend server)
* Python 3.x version (for the backend server)
* macOS, Windows or Linux
# Streaming (Optional)
Source: https://docs.thesys.dev/guides/streaming
Improve your application's user experience by streaming C1 responses in real-time.
LLM responses can take several seconds to complete.
Streaming allows your UI to start rendering the moment the first piece of the C1 response is available,
drastically reducing perceived latency and creating a much more responsive and engaging experience for the end-user.
## Overview
Streaming involves your entire application stack.
The C1 API sends the response in chunks, your backend forwards these chunks, and your UI progressively renders them as they arrive.
```mermaid theme={null}
sequenceDiagram
participant UI
participant Backend as "Your Backend"
participant C1 as "C1 API"
UI->>Backend: POST /api/chat (initiates request)
activate Backend
Backend->>C1: Call Completions API (stream: true)
activate C1
C1-->>Backend: Streams C1 DSL chunks…
Backend-->>UI: Forwards stream chunks…
Note right of UI: UI begins rendering immediately.
C1-->>Backend: …stream continues…
Backend-->>UI: …stream continues…
Note right of UI: UI updates as chunks arrive.
C1--xBackend: Stream ends
Backend--xUI: Stream ends
deactivate Backend
```
## Backend: Enabling the Stream
To enable streaming, you must first set `stream: true` in your call to the C1 API. Your backend's primary role is then to efficiently forward this stream to the UI with the correct `Content-Type: text/event-stream` header.
Our server-side SDKs for Python and Node.js provide helpers to simplify this process.
To simplify streaming in FastAPI, we provide the `thesys-genui-sdk` Python library. You can install it via pip:
`pip install thesys-genui-sdk`
The library provides the `@with_c1_response` decorator, which automatically handles setting the correct response headers and creating a streaming context. Inside the decorated function, you can use the `write_content` helper to yield each chunk from the LLM stream.
For framework independent streaming, see the [`thesys-genui-sdk` package on PyPI](https://pypi.org/project/thesys-genui-sdk/).
```python main.py theme={null}
import os
from openai import OpenAI
from fastapi import FastAPI
from pydantic import BaseModel
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message
# --- Setup ---
app = FastAPI()
client = OpenAI(
api_key=os.environ.get("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed"
)
class ChatRequest(BaseModel):
prompt: str
# --- API Endpoint ---
@app.post("/chat")
@with_c1_response() # This decorator handles the streaming response
async def chat(request: ChatRequest):
# 1. Enable streaming from the C1 API
stream = client.chat.completions.create(
model="c1-model-name",
messages=[{"role": "user", "content": request.prompt}],
stream=True,
)
# 2. Yield each chunk using the write_content helper
for chunk in stream:
content = chunk.choices.delta.content
if content:
await write_content(content)
# 3. Store the assistant message in the database if required
assistantMessage = get_assistant_message()
# messageStore.addMessage(assistantMessage) # replace with your own database login
```
For Node.js, we provide helpers in the `@thesysai/genui-sdk/server` package. The `makeC1Response` function creates an object that manages the response stream.
You can then use a utility like `transformStream` from `@crayonai/stream` to process the stream from the C1 API and pipe the content chunks into the response object using its `writeContent` method.
```typescript app/api/chat/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { makeC1Response } from "@thesysai/genui-sdk/server";
// --- Setup ---
const client = new OpenAI({
apiKey: process.env.THESYS_API_KEY,
baseURL: 'https://api.thesys.dev/v1/embed',
});
// --- API Endpoint ---
export async function POST(req: NextRequest) {
const { prompt } = (await req.json()) as { prompt: string };
const c1Response = makeC1Response();
// 1. Enable streaming from the C1 API
const llmStream = await client.chat.completions.create({
model: "c1-model-name",
messages: [{ role: "user", content: prompt }],
stream: true,
});
// 2. Pipe the LLM stream into our response stream
transformStream(llmStream,
(chunk) => {
const contentDelta = chunk.choices.delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return null; // We are not transforming, just observing
},
{ onEnd: () => {
const assistantMessage = c1Response.getAssistantMessage();
// store the assistant message in the database if required
// messageStore.addMessage(assistantMessage); // replace with your own database login
c1Response.end();
} }
);
// 3. Return the managed response stream
return new NextResponse(c1Response.responseStream, {
headers: { "Content-Type": "text/event-stream" },
});
}
```
## UI: Rendering the Stream
Handling a streaming response on the UI requires manually fetching the data, reading the stream chunk by chunk, and updating your component's state as new data arrives.
While this involves more code than a standard fetch request, it gives you full control over the user experience. This section breaks down a complete, working example.
### Manual Stream Handling with ``
Here is a full React component that fetches a streaming C1 DSL response from a backend endpoint and renders it progressively.
```tsx app/page.tsx theme={null}
import { useState } from "react";
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
function MyStreamingComponent() {
const [c1Response, setC1Response] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleGenerate = async (prompt: string) => {
setIsLoading(true);
setC1Response(""); // Clear previous response
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
if (!response.body) {
throw new Error("Response body is empty.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedResponse = "";
// Read the stream chunk by chunk
while (true) {
const { done, value } = await reader.read();
if (done) break; // Exit loop when stream is finished
const chunk = decoder.decode(value);
accumulatedResponse += chunk;
// Update state to re-render the component with new data
setC1Response(accumulatedResponse);
}
} catch (error) {
console.error("Error fetching or reading stream:", error);
} finally {
setIsLoading(false);
}
};
return (
{/* Your application's input form */}
{/* C1Component renders the streaming DSL */}
);
}
```
### Code Breakdown
Let's walk through the key parts of the code above.
1. **State Management:** We use two state variables:
* `c1Response`: An accumulating string that holds the C1 DSL as it arrives from the stream. It starts empty.
* `isLoading`: A boolean to track the request status, which is passed to the `isStreaming` prop.
2. **The Fetch Request:** Inside the `handleGenerate` function, we initiate a standard `fetch` call to our streaming backend endpoint.
3. **Reading the Stream:** This is the core of the logic.
* We get a `reader` from the `response.body`.
* The `while (true)` loop continuously calls `reader.read()` to get the next chunk of data.
* A `TextDecoder` converts each raw data chunk into a string.
* We append this string to our `accumulatedResponse` variable and update the `c1Response` state with `setC1Response()`. **This state update is what causes the UI to render progressively.**
* The loop breaks when the stream sends a `done: true` signal.
4. **Connecting to ``:**
* The `c1Response` state variable is passed directly to the ``. As this state updates with each new chunk, the component re-renders to display the incoming UI.
* The `isLoading` state is passed to the `isStreaming` prop, which can be used by the component to display loading indicators.
### All-in-One Solution: ``
For conversational interfaces, `` is the simplest solution. It has streaming enabled by default and encapsulates all the complex state and stream-handling logic shown above. As long as the `apiUrl` you provide points to a streaming backend endpoint, no further UI configuration is required.
```tsx app/page.tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
import "@crayonai/react-ui/styles/index.css";
export default function App() {
// This will stream automatically if /api/chat is a streaming endpoint
return ;
}
```
For guides on using ``, please refer to the [Conversational UI](/guides/conversational) section.
# Customizing Charts
Source: https://docs.thesys.dev/guides/styling/charts
Control the appearance of charts by defining global and chart-specific color palettes.
Chart colors are controlled within the same theme object used by the ``. This allows you to define palettes that are consistent with your brand and that can automatically adapt to light and dark modes.
### The Chart Palette System
Chart styling uses a simple override system. You can set a default color palette for all charts and then, if needed, provide specific palettes for individual chart types.
#### The Global Fallback: `defaultChartPalette`
The `defaultChartPalette` is the fastest way to apply a consistent color scheme. The array of colors you provide will be used for all chart types that do not have a more specific palette defined.
```tsx theme={null}
{/* application components */}
```
#### Specific Overrides: `barChartPalette`, `lineChartPalette`
For more detailed control, you can define a unique color palette for each chart type. A specific palette, like `barChartPalette`, will always be used instead of the `defaultChartPalette` for that chart type.
This example sets a default for all charts but provides a unique color set just for bar charts.
```tsx theme={null}
{/* Your C1Component */}
```
#### Available palette keys
```typescript theme={null}
interface ChartColorPalette {
defaultChartPalette?: string[]; // Fallback for all charts
barChartPalette?: string[]; // Specific to BarChart
lineChartPalette?: string[]; // Specific to LineChart
areaChartPalette?: string[]; // Specific to AreaChart
pieChartPalette?: string[]; // Specific to PieChart
radarChartPalette?: string[]; // Specific to RadarChart
radialChartPalette?: string[]; // Specific to RadialChart
}
```
### Applying Palettes with Light and Dark Modes
You can define different chart palettes for your `theme` and `darkTheme` objects. This is useful for creating separate light and high contrast dark theme.
```tsx theme={null}
const lightTheme = {
// Palettes for light mode
areaChartPalette: ["#2563eb", "#dc2626", "#16a34a"],
defaultChartPalette: ["#374151", "#111827", "#1f2937"],
};
const darkTheme = {
// High-contrast palettes for dark mode
areaChartPalette: ["#3b82f6", "#ef4444", "#22c55e"],
defaultChartPalette: ["#d1d5db", "#f3f4f6", "#e5e7eb"],
};
{/* Your C1Component */}
;
```
# Overriding Styles with CSS
Source: https://docs.thesys.dev/guides/styling/css-overrides
Apply custom CSS for precise, specific style adjustments.
This is an advanced technique. Though we try to keep the class names and markup stable, they can change between SDK versions. Always review your overrides after updating the SDK.
Custom CSS is the final layer of the styling system. You should always start with **Theming** for broad changes, and then use CSS for small, specific tweaks that the theme system cannot handle.
Good use cases for custom CSS include:
* Changing the font weight of a specific card's title.
* Adding a unique border to an element.
* Adjusting the margin on a particular button.
### How to Find CSS Classes
The most reliable way to find the correct class name for a C1 element is to use your browser's developer tools.
1. In your browser, right-click the element you want to style.
2. Select "Inspect" from the context menu.
3. The developer tools panel will open, highlighting the element's HTML. The `class` attribute will contain the class names you can use as CSS selectors.
### Applying Your Custom CSS
Let's say we want to make the title of all C1-generated cards uppercase.
You can add your override rules in a CSS file.
```css theme={null}
/* styles/custom.css */
/* Target the C1 card title class to make it uppercase */
.crayon-header {
text-transform: uppercase;
letter-spacing: 0.05em;
}
```
### Best Practices
* **Be Specific, But Not Too Specific:** Avoid overly complex selectors like `div > span > p.c1-title`.
These are very likely to break with SDK updates. Prefer targeting a single, stable-looking class like `.c1-card-title`.
# Styling
Source: https://docs.thesys.dev/guides/styling/index
Customize the appearance of C1-generated UIs to match your application's brand, from broad themes to specific CSS changes.
When you integrate C1, you want the generated UI to look and feel like a natural part of your application.
C1's styling system is designed for this purpose, giving you control over the appearance of the components it creates.
### Approaches to Styling
C1 styling system uses a layered approach, allowing you to make broad, application-wide changes quickly, and then make more specific adjustments as needed.
**Theming**
This is your starting point and the most effective way to apply your brand's style. Using a central theme object with our ``, you can control global styles like colors, fonts, spacing, and border-radius. This is the fastest way to ensure brand consistency across all generated components.
**Component-Specific Customization**
Some complex components, like charts, have their own detailed styling options that you can define within the theme object. This allows for more detailed control over a specific component's appearance when needed.
**CSS Overrides**
For specific adjustments that the theming system doesn't cover, you can use standard CSS to target and override the styles of any element. This gives you the maximum level of control and should be used for making final, precise changes.
### Guides in This Section
This section contains the following guides to help you customize your C1 components.
* **[Theming](/guides/styling/theming)**: Use the `` to apply global styles, use presets, and configure light and dark modes.
* **[Customizing Charts](/guides/styling/charts)**: A deep dive into the specific theme properties available for styling charts.
* **[Overriding Styles with CSS](/guides/styling/css-overrides)**: A guide on how to apply custom CSS classes for precise control over component styles.
# Theming
Source: https://docs.thesys.dev/guides/styling/theming
Apply global styles, use presets, and manage light/dark modes with the ``.
Theming is the starting point for applying your brand's style to C1 components. The `` component applies global styles—like colors, typography, and corner radiuses—to all generated UI nested within it.
Place the `` at a high level in your application tree, wrapping the areas where C1 components will be rendered.
### `` Props
The provider is configured through three main props:
**`theme`**
The `theme` prop accepts a theme object that defines the styles for **light mode**.
**`darkTheme`**
The `darkTheme` prop accepts a theme object that defines the styles for **dark mode**.
**`mode`**
The `mode` prop is a string that controls which theme is currently active. It accepts following values:
* `'light'`: selects the light theme.
* `'dark'`: selects the dark theme.
### Using Theme Presets
The `@crayonai/react-ui` package includes a collection of pre-built `themePresets` to help you get started quickly. Available themes include `candy`, `carbon`, `play`, `neon`, and more.
To use a preset, import `themePresets` and pass the desired theme to the provider's props.
```tsx theme={null}
import { ThemeProvider } from "@thesysai/genui-sdk";
import { themePresets } from "@crayonai/react-ui";
function App() {
const userPrefersDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
return (
{/* application components */}
);
}
```
### Creating a Custom Theme
For full control over the appearance of the UI, you can create and pass your own theme object.
A detailed guide on the theme object's structure and all available tokens is coming soon.
# Thinking States
Source: https://docs.thesys.dev/guides/thinking-states
Learn to enhance user experience by updating them about the agent's thinking process in real-time
Thinking states are a great way to let the user know what the model is doing. This is especially useful for long-running tasks or when the user is waiting for a response,
such as when the agent is performing a slow tool call.
The `@thesysai/genui-sdk` package provides a `makeC1Response` function that can be used to add data related to thinking states to the response.
Here's a simple example of how to use thinking states:
Use the `makeC1Response` function to create a `c1Response` object by importing it from the `@thesysai/genui-sdk` package, and start writing the LLM response
content to this object:
```ts app/api/chat/route.ts {6, 9, 36-38, 43, 54} [expandable] theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
To add a thinking state, use the `writeThinkItem` method defined on the `c1Response` object:
```ts app/api/chat/route.ts {11-14} [expandable] theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
c1Response.writeThinkItem({
title: "Thinking...",
description: "Diving into the digital depths to craft you an answer.",
});
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
If you'd like to use thinking states with long-running tool calls, simply call the aforementioned `writeThinkItem` method inside the
tool call handler. For example, for the `webSearch` tool implemented in the [Tool Calling](/guides/integrate-data/tool-calling) guide, you can add a thinking state as follows:
Next, modify the tool call handler to call a function that updates the thinking state:
```ts app/api/chat/tools.ts {23} [expandable] theme={null}
import type { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import Exa from "exa-js";
import type { JSONSchema } from "openai/lib/jsonschema.mjs";
const exa = new Exa(process.env.EXA_API_KEY!);
export const getWebSearchTool = (
writeThinkingState: () => void
): RunnableToolFunctionWithParse<{ query: string }> => ({
type: "function",
function: {
name: "webSearch",
description: "Use this tool to perform a web search.",
parse: JSON.parse,
parameters: zodToJsonSchema(
z.object({
query: z.string().describe("The query to search for."),
})
) as JSONSchema,
function: async ({ query }: { query: string }) => {
writeThinkingState();
return await exa.search(query, { numResults: 5 });
},
strict: true,
},
});
```
Next, pass the `writeThinkingState` function to the tool call handler:
```ts app/api/chat/route.ts {9 - 15} theme={null}
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
{ role: "system", content: systemPrompt },
...messageStore.getOpenAICompatibleMessageList(),
],
stream: true,
tools: [
getWebSearchTool(() => {
c1Response.writeThinkItem({
title: "Searching the web...",
description:
"Scouring the digital universe for the most relevant and up-to-date insights.",
});
}),
],
toolChoice: "auto",
});
```
```ts app/api/chat/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { DBMessage, getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
import { getWebSearchTool } from "./tools";
import { systemPrompt } from "./systemPrompt";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
c1Response.writeThinkItem({
title: "Thinking...",
description: "Diving into the digital depths to craft you an answer.",
});
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: DBMessage;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
{ role: "system", content: systemPrompt },
...messageStore.getOpenAICompatibleMessageList(),
],
stream: true,
tools: [
getWebSearchTool(() => {
c1Response.writeThinkItem({
title: "Searching the web...",
description:
"Scouring the digital universe for the most relevant and up-to-date insights.",
});
}),
],
toolChoice: "auto",
});
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end();
const message = accumulated.filter((message) => message).join("");
messageStore.addMessage({
role: "assistant",
content: message,
id: responseId,
});
},
}
) as ReadableStream;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
```
If you'd like to add a custom thinking state component, you can do so by passing a `customizeC1` prop to the `C1Chat` component or the
`useThreadManager` hook.
Your custom component should accept the following props:
An array of thinking state items, where each item contains:
* title: The title
of the thinking state
* content: The content/description of the thinking state
* ephemeral: Whether this thinking state should be temporary or persist after the response is done streaming
Indicates if a thinking state is active. Use this to display a loader or shimmer while processing.
Example custom think component:
```tsx [expandable] theme={null}
import { ThinkComponent } from "@thesysai/genui-sdk";
import styles from "./styles.module.css";
const CustomThink: ThinkComponent = ({ thinkItems, thinkingInProgress }) => {
return (
);
};
```
You may pass your custom component to the `C1Chat` component or the `useThreadManager` hook like this:
```tsx C1Chat {3} theme={null}
```
```tsx useThreadManager {3} theme={null}
const threadManager = useThreadManager({
// other threadManager parameters
customizeC1: { thinkComponent: CustomThink },
});
```
You should now see the thinking state on the UI while the agent is processing the response:
The [`thesys_genui_sdk`](https://pypi.org/project/thesys-genui-sdk/) package provides a `C1Response` class that can be used to add data related to thinking states to the response.
If you are using FastAPI, the package provides a handy decorator `with_c1_response` to make this even easier.
```python FastAPI [expandable] theme={null}
# main.py
from pydantic import BaseModel
from fastapi import FastAPI, Request
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message
import openai
app = FastAPI()
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
@app.post("/chat")
# this decorator will add the c1_response in a context variable
# and internally return the stream from your endpoint.
@with_c1_response()
async def chat(request: ChatRequest):
await generate_llm_response(request)
class ChatRequest(BaseModel):
prompt: Prompt
threadId: str
responseId: str
async def generate_llm_response(request: ChatRequest):
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": request.prompt}],
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
# get_assistant_message() allows you to get the full response to store for message history
assistant_message_for_history = get_assistant_message()
```
```python Framework-Independent [expandable] theme={null}
# main.py
import asyncio
from thesys_genui_sdk import C1Response
import openai
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(c1_response: C1Response, prompt: str):
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await c1_response.write_content(content)
# c1_response.get_assistant_message() allows you to
# get the full response to store for message history
assistant_message_for_history = c1_response.get_assistant_message()
await c1_response.end()
async def main():
c1_response = C1Response()
# In a web server, you would start an async task
# to generate the response
asyncio.create_task(generate_llm_response(c1_response, "Tell me about latest trends in AI."))
# This is the stream you'd return from your route
response_stream = c1_response.stream()
# Example of how to consume the stream
async for item in response_stream:
print(item, end="")
if __name__ == "__main__":
asyncio.run(main())
```
To add a thinking state, use the `write_think_item` function. This is useful to let the user know that a long-running process is happening.
```python FastAPI {15-19} [expandable] theme={null}
# main.py
from fastapi import FastAPI, Request
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message, write_think_item
import openai
app = FastAPI()
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(request: Request):
# ...
await write_think_item(
title="Thinking...",
description="Diving into the digital depths to craft you an answer."
)
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": request.prompt}],
stream=True,
)
# ...
```
```python Framework-Independent {12-15} [expandable] theme={null}
# main.py
import asyncio
from thesys_genui_sdk.context import C1Response
import openai
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(c1_response: C1Response, prompt: str):
await c1_response.write_think_item(
title="Thinking...",
description="Diving into the digital depths to craft you an answer."
)
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
# ...
```
If you are using tools, you can call `write_think_item` before executing a long-running tool to provide feedback to the user.
```python FastAPI [expandable] theme={null}
async def web_search(query: str):
await write_think_item(
title="Searching the web...",
description=f"Looking for information on '{query}'"
)
# ... perform web search
results = await some_search_api(query)
return results
```
```python Framework-Independent [expandable] theme={null}
async def web_search(query: str, c1_response: C1Response):
await c1_response.write_think_item(
title="Searching the web...",
description=f"Looking for information on '{query}'"
)
# ... perform web search
results = await some_search_api(query)
return results
```
This is a frontend customization and is independent of the backend implementation. You can follow the same guide as for Node.js by passing a `customizeC1` prop to the `C1Chat` component or the `useThreadManager` hook.
Your custom component should accept the following props:
An array of thinking state items, where each item contains:
* title: The title
of the thinking state
* content: The content/description of the thinking state
* ephemeral: Whether this thinking state should be temporary or persist after the response is done streaming
Indicates if a thinking state is active. Use this to display a loader or shimmer while processing.
Example custom think component:
```tsx [expandable] theme={null}
import { ThinkComponent } from "@thesysai/genui-sdk";
import styles from "./styles.module.css";
const CustomThink: ThinkComponent = ({ thinkItems, thinkingInProgress }) => {
return (
);
};
```
You may pass your custom component to the `C1Chat` component or the `useThreadManager` hook like this:
```tsx C1Chat {3} theme={null}
```
```tsx useThreadManager {3} theme={null}
const threadManager = useThreadManager({
// other threadManager parameters
customizeC1: { thinkComponent: CustomThink },
});
```
You should now see the thinking state on the UI while the agent is processing the response:
# What is C1 by Thesys?
Source: https://docs.thesys.dev/guides/what-is-thesys-c1
the Generative UI API for AI-native applications.
## Introduction
Large Language Models (LLMs) are powerful tools for processing and generating text. However, when building AI-native applications, relying solely on text-based responses often limits user experience and functionality. Traditionally, transforming an LLM's output into a rich, interactive user interface requires extensive manual coding on the UI.
**Generative UI** is a paradigm shift where AI dynamically generates the user interface itself.
Thesys provides [C1 API](/guides/implementing-api) and [React SDK](/guides/rendering-ui) to bring Generative UI to your applications.
With C1, you can send a natural language prompt, and the API will generate and stream live, interactive UI components directly into your client application.
## See it in action
To understand how C1 works, consider a practical example. You can send a natural language prompt like the one below directly to the C1 API.
Live interactive demo of C1 in action
## Key Features
* **Interactive Components** \
The generated UI is not static. C1 natively supports a range of interactive elements, including charts with tooltips, clickable elements that continue the conversation and forms that can capture user input to trigger subsequent actions.
* **Themeable** \
The generated UI seamlessly adapts to your brand with a custom theme. Dark mode is supported out of the box.
* **Real-time UI Streaming** \
C1 streams the UI as it's generated, not after. This allows components to appear on the screen progressively, creating a more responsive and fluid user experience without waiting for the full response to be ready.
* **OpenAI-Compatible API** \
The API is designed for seamless backend integration. By using the familiar OpenAI SDKs, you can adopt C1 with minimal changes to your existing code. On the UI, [C1 React SDK](/guides/rendering-ui) replaces a traditional markdown renderer.
* **Robust Error Handling**
* LLM providers might go down but C1 won't. C1 internally retries the request or routes to fallback providers, so your users are not affected.
* LLM might generate incomplete or invalid response. C1 detects this and seamlessly fixes it in realtime.
## What can I build with it
* **Analytics dashboards**
*“Show me monthly revenue trends”* → live line chart.
* **Conversational agents**
*“Book me a flight and confirm details”* → multi-step form in chat.
* **Internal tools**
*“List all users who signed up in the last 30 days”* → interactive table with line chart showing week-by-week growth.
* **E-commerce flows**
*“Add this product to my cart and checkout”* → interactive checkout UI.
* **Rebuilding AI agents with Generative UI**
Any existing AI agent - customer support bots, copilots, research assistants, or vertical-specific tools - can be reimagined with a Generative UI layer.
Instead of limiting the agent to text-only interactions, C1 lets it generate forms, dashboards, and workflows that make the agent truly useful in production.
## FAQs
Tools like Vercel AI SDK, CopilotKit, and AGUI mainly help you build chat-style interfaces from LLM outputs -
but the heavy lifting of turning those text responses into actual UI still falls on developers.
C1 by Thesys is different: it's an LLM API that generates UI directly, not just text.
That means you can plug C1 into those frameworks (or use it on its own) and skip the manual step of
converting model outputs into components.
See [Frameworks](/guides/frameworks) for guides on how to integrate C1 in these frameworks.
C1 by Thesys is designed to be a drop in replacement for any AI agent that is built on top of the OpenAI API.
Its as simple as
1. Change OpenAI baseURL to `api.thesys.dev/v1/embed`
2. Replace your `` renderer with ``
See [Migrating to GenUI](/guides/migrate-to-genui) guide for more details.
Yes, absolutely! One of the core design principles of C1 is to be able to support any design system, any components.
There are a couple of layers to be able to do this:
* See [Styling Guide](/guides/styling) to understand how to match C1 responses to your design system.
* [Custom Components](/guides/custom-components) helps close the last mile gap for specific components like Weather Card, Flight Seat map
that are not supported by C1 out of the box.
# Display Information
Source: https://docs.thesys.dev/library/display-information
Use these components to present static content, visual structure, and supporting context in your UI.
**Includes:** `TextContent`, `Img`, `TagBlock`, `ListBlock`, `Table`, `Card`, `Accordion`, `Steps`, `Carousel`, `CodeBlock`
## Text Content
**Definition**\
Text Content is a component that displays written information as formatted text using markdown syntax.
**Uses**\
**-** Ensures consistent styling of long-form content throughout the product.\
**-** Safely format and structure text while preventing layout-breaking or inaccessible content.
### Visual Variants
### Text Formatting
### Typographic Styles
### Customize with Prompts
* **Markdown Features:** Ask C1 to format text with bold, italics, links, strikethroughs, blockquotes, headers, lists, and checklists.
> Ex. *"Create a help article with headings, bullet points, and bolded keywords"*
* **Specialized Content:** Present information as tables, include code blocks with syntax highlighting, or render math equations.
> Ex. *"Show a markdown table comparing feature availability in Free vs. Pro plans"*
* **Visual Variants:** Specify the variant style C1 should apply for emphasis or context.
> Ex. *"Render the introduction using the 'clear' variant and FAQs using the 'card' variant"*
***
## Callout
**Definition**\
Callouts are specialized UI components designed to highlight important information by visually separating it from regular text content.
**Uses**\
**-** Draw attention to critical information, warnings, system status, and announcements.\
**-** Emphasize tips, best practices, or important takeaways.\
**-** Provide positive feedback for completed actions, milestones, or achievements.\
**-** Make key information concise and visually distinct.
### Variants
### Anatomy
### Customize with Prompts
* **Variant Selection:** Pick styles such as info, success, warning, or error.
> Ex. *"Show a warning callout for password strength issues"*
* **Title and Description:** Customize tone, length, and inclusion of emojis.
> Ex. *"Create a success callout titled '🎉 Well Done!' with a message about completing onboarding."*
* **Placement and Audience:** Modify where and when a callout appears.
> Ex. *"Display a tip callout at the top of the settings page."*
***
## Images
**Definition**\
The Image component displays a single image with proper formatting, accessibility, and responsive behavior.
**Uses**\
**-** Enhance understanding of complex topics with visual aids.\
**-** Add variety and break up large blocks of text to keep users engaged.
### Aspect Ratios
### Customize with Prompts
* **Source and Alt Text:** Specify image URLs and accessible descriptions.
> Ex. *"Add an image of a solar panel with alt text 'Residential rooftop solar installation'"*
* **Sizing and Cropping:** Choose what aspect ratio to use.
> Ex. *"Display the product image in a 1:1 square ratio"*
***
## Tags
**Definition**\
Tags are short, descriptive labels used to categorize content or highlight attributes.
**Uses**\
**-** Help users find relevant items by category or keyword.\
**-** Provide quick at-a-glance context.
### Anatomy
### Customize with Prompts
* **Label Content:** Specify tag text and icon use.
> Ex. *"Tag article with 'Beginner' and include an appropriate emoji."*
* **Grouping:** Use multiple tags to create meaningful clusters.
> Ex. *"Add tags to each section for price range, one unique feature and rating."*
***
## Lists
**Definition**\
A list is an ordered or unordered collection of related items displayed as static text.
**Uses**\
**-** Break long content into scannable sections.\
**-** Show relationships between points with nesting.
### Styles
### Customize with Prompts
* **List Type:** Choose between numbered, bulleted, or checklist styles.
> Ex. *"Show an ordered list of onboarding steps."*
* **Nesting:** Add hierarchy for complex content.
> Ex. *"Create a bullet list with sub-points under each main idea"*
***
## List Blocks
**Definition**\
List Blocks are visually rich lists where each item can be clicked to view more details.
**Uses**\
**-** Showcase features, products, or directory items.\
**-** Provide scannable previews with an action path.
### Anatomy
### Customize with Prompts
* **Item Elements:** Specify inclusion of images, subheaders and icons.
> Ex. *"Create a list block of courses with thumbnails, course titles, and expertise level."*
* **Item Content:** Customize the content in each list block, and specify the number of list items a block should have. Ask C1 for brief or descriptive text.
> Ex. *"Include 10 products in the list block and add descriptive text for each."*
***
## Tables
**Definition**\
Displays structured data in rows and columns for easy comparison and scanning.
**Uses**\
**-** Compare attributes across multiple items.\
**-** Provide summaries or structured recaps.
### Anatomy
### Customize with Prompts
* **Table Structure:** Customize headers and total number of columns and rows.
> Ex. *"Table comparing Basic, Pro, and Enterprise plans with 4 features listed in rows."*
* **Content Type:** Customize the type (tags, text, numbers, icons, emojis) of content each cell in a column should have.
> Ex. *"Use tag blocks for status indicators."*
***
## Accordions
**Definition**\
Accordions are vertically stacked, expandable sections that reveal hidden content when expanded.
**Uses**\
**-** Organize related content in a progressive disclosure format.\
**-** Reduce visual clutter while still providing access to detailed information.
### Anatomy
### Customize with Prompts
* **Section Titles and Order:** Specify number of sections, their sequence and edit trigger text content.
> Ex. **"Rename all section triggers to be more descriptive and catchy."**
* **Content:** Customize content types included in an accordion.
> Ex. *"Don't include charts in accordions."*
***
## Steps
**Definition**\
Display a sequence of numbered stages or actions in a linear flow to guide users through a multi-step process.
**Uses**\
**-** Provide clear sequential guidance for processes like onboarding, checkout, or tutorials.\
**-** Break complex tasks into manageable actions and help users anticipate what comes next.
### Anatomy
### Customize with Prompts
* **Structure:** Specify how many steps the task should be divided into and edit the sequence if necessary.
> Ex. *"Show how to sign up in 3 steps."*
* **Content:** Customize the title for each step, and the content in each step.
> Ex. *"Change the first step's title to "Download Software" and update its content to include system requirements"*
***
## Code Blocks
**Definition**\
Display monospaced, formatted code with syntax highlighting.
**Uses**\
**-** Present examples for developers to copy and reuse.\
**-** Showcase syntax for tutorials or documentation.
### Customize with Prompts
* **Language and Highlighting:** Define the programming language for syntax color-coding.
> Ex. *"Show a JavaScript function to calculate factorial"*
***
### Tabs
**Definition**\
A horizontal list of controls that organizes content into sections users can toggle between without leaving the current page, where only one section is visible at a time.
**Uses**\
**-** Reduce vertical scrolling by layering content instead of displaying sections one below another.\
**-** Let users choose what sections of content to engage with, when the sections are distinct and may not all be relevant to the user.
### Variants
### Anatomy
### Customize With Prompts
* Tab Labels and Icons: Customize the text label and inclusion of icons.
> Ex. *"Add icons to all the tabs."*
* Content Type: Tell C1 which components must be included in tabs and how it should be laid out.
> Ex. *"Put today's specials at the top of each tab and include images."*
* \*\*Number and Sequence of Tabs: \*\*Customize the sequence and number of tabs that the content should be organized into. Tabs are horizontally scrollable.
> Ex. *"Move the Drinks tab between Main Course and Desserts. Add a tab for Specials."*
***
# Form Elements
Source: https://docs.thesys.dev/library/form-elements
Allow users to enter or select values within an interface to gather user intent for the system to respond with relevant actions or information.
**Includes:** `Input`, `Select`, `RadioGroup`, `CheckboxGroup`, `Slider`, `DatePicker`
## Inputs
**Definition**\
An input field is a form component that allows users to enter data directly, such as text, numbers, or other characters, by typing or pasting into the field.
**Uses**\
**-** Capture freeform data such as names, emails, phone numbers, or search queries.\
**-** Collect structured inputs like dates, times, passwords, or numeric values with formatting/validation.\
**-** Support longer responses for feedback, notes, or descriptions.
### Types
### Types of Text-Based Inputs
### Types of Numerical Inputs
### Anatomy
### Customize with Prompts
* **Specific Input Types:** Ask C1 to create input fields that accept only specific input types like numbers, letters, include special characters etc.
> Ex. *"Create an integer input for years of experience."*
* **Label, Placeholder, and Hint/Helper Text:** Ask C1 to use your specified label, placeholder, or helper text, or change their tone or specificity.
> Ex. *"Create a user feedback form with the placeholder 'Tell us your thoughts...' and the helper text 'This helps us make your experience better'."*
***
## Select
**Definition**\
A select input field is a form component that, when interacted with, displays a dropdown menu containing a predefined list of options.
**Uses**\
**-** Provides a clean, space-efficient way to present multiple finite choices to pick from.\
**-** Collecting standardized personal data from users.\
**-** Provide filtering and sorting options for content.\
**-** Preferences and settings options for users to select from.
***
### Types
### Anatomy
### Customize with Prompts
* **Dropdown Menu Structure:** Prompt C1 to create a dropdown menu with specified labels, grouped options and separators between each group.
> Ex. *"Create a support form with a dropdown for inquiry type. Organize options with labeled sections: 'Sales Questions' (General Sales, Pricing, Demos), 'Technical Support' (Bug Reports, Installation Help, Account Issues), and 'Business' (Partnerships, Billing, Refunds). Use separators between each section and arrange in logical order."*
* **Customize Select Field:** Use custom placeholder text and mention whether a specified option should be selected by default.
> Ex. *"Create a product order form for shipping method. Use custom placeholder 'Choose your preferred delivery option' and set 'Standard Shipping' as the default selection. Include options for Express, Standard, and Economy shipping."*
***
## Radio Group
**Definition**\
A radio group is a list of mutually exclusive options where the user can select one option from a given set.
**Uses**\
**-** Exposes all available options to the user upfront, reducing clicks and enabling easy comparison.\
**-** Leverages user familiarity to clearly indicate that only one option can be chosen.\
**-** Suitable for forms, settings, quizzes and surveys where only one valid answer is possible.
### Anatomy
### Customize with Prompts
* **Options and Labels:** Choose how many Radio Items should be displayed, their sequence and content.
> Ex. *"Create a radio group for size options, ordered from smallest to largest: XS, S, M, L, XL."*
* **Default Selection:** Specify whether an option should appear pre-selected in a Radio Group.
> Ex. *"Set 'Auto' as the default selected option."*
***
## Checkbox Group
**Definition**\
A checkbox UI component is an input element that lets users select or deselect one or more independent options from a set.
**Uses**\
**-** Checkbox groups with multiple items let users select one or more options from a given list.\
**-** Single checkboxes let users confirm agreement or consent, toggle preferences and enable/select optional features in a flow.\
**-** Let users perform administrative tasks like bulk actions (select multiple items to take action with).
### Anatomy
### Customize with Prompts
* **Checkbox Item Properties:** Specify the number and order of checkbox items, their content, and what sequence they should be arranged in.
> Ex. *"Create checkboxes for 3 meal preferences: Vegetarian, Vegan, Gluten-free"*
* **Default Selection:** Tell C1 whether a checkbox item(s) should be checked by default.
> Ex. *"Make the 'Agree to terms' checkbox pre-checked."*
***
## Slider
**Definition**\
A slider is a type of input field that lets users select a value or a range of values from a continuous pre-defined scale by dragging a handle along a track.
**Uses**\
**-** Adjusting settings like volume, brightness and intensity of states like image filters, zoom levels, playback speed, etc.\
**-** Filtering content by a range of values like price, age, date, distance radius, etc.\
**-** Timeline scrubbing, progress indicators, setting thresholds and inputs such as rating scales/satisfaction scores.
### Variants
### Anatomy
### Customize with Prompts
* **Slider Variant and Selection Type:** Choose the slider variant, continuous (smooth) or discrete (stepped), and selection mode (single value or range) that C1 should generate. In case of discrete sliders, define increment size.
> Ex. *"Add a continuous price range slider from \$0 to \$2000, default range \$200-\$800, for filtering laptops."*
* **Set Slider Limits and Values:** Specify the minimum and maximum values, whether negatives/decimals are allowed, and any default value or range.
> Ex. *"Add a star rating slider from 1.0 to 5.0 stars, allow half-star increments (0.5 steps)."*
* **Add Visual Cues:** Optionally, include icons on either end of the slider as visual indicators of its direction and function.
> Ex. *"Create a continuous zoom slider for image viewer.*
>
> * *Range: 10% to 500%*
> * *Default: 100%*
> * *Left icon: zoom-out (magnifying glass with minus)*
> * *Right icon: zoom-in (magnifying glass with plus)"*
***
## Date Picker
**Definition**\
A date picker is an input component that lets users select dates or a range of dates from a calendar interface that appears temporarily on interaction with a select field.
**Uses**\
**-** Eliminate user effort and errors of manual typing for a formatted input like date(s).\
**-** Let users enter personal data (birthdays, anniversaries, deadlines) etc.\
**-** Select date/start and end dates for booking and reservations.\
**-** Filter records/content by specific dates or ranges.\
**-** Selecting and managing important dates for records, contracts, regulatory compliance, and financial periods.
### Variants
### Anatomy
### Customize with Prompts
* **Selection Mode:** Specify whether the date picker should allow selecting a single date or a range of dates.
> Ex. *"Create a single date picker for selecting date of birth."*
* **Validation Rules:** Set a limit to the range of dates that can be selected, and how far in the past and future users would be allowed to make the selection.
> Ex. *"Add a single date picker for appointment dates that:*
>
> * *Allows dates starting from tomorrow (no same-day bookings)*
> * *Goes up to 6 months in the future maximum*
> * *Restricts selection to weekdays only"*
***
## How to Make Forms Using Form Elements
### Anatomy
* Who will use this form?
* What's the end goal?
* When and where will they encounter it?
> Ex. *"Build a contact form for support requests"*
* List the specific information you need to collect
* Include any choices, confirmations, or agreements the user needs to provide
> Ex. *"Create a registration form with username, email, password, confirm password, and agree to terms checkbox"*
* C1 chooses appropriate input types based on your field requirements, but you can customize if needed.
> Ex. *"Include a text area for detailed feedback rather than a single line"*
* Request specific label or hint content if needed.
> Ex. *"Add reassuring privacy hints for sensitive data collection"*
* Specify what actions users can take after filling the form
> Ex. *"Include both Submit and Cancel buttons"*
# Component Library
Source: https://docs.thesys.dev/library/index
Explore the full set of components supported by C1.
C1 comes with a growing set of ready-to-use UI components.
These components can be combined and customized to build interactive, production-ready interfaces without needing to hand-code layouts or behaviors from scratch.
You can also extend this library with your own [custom components](/guides/custom-components).
Charts, graphs, and interactive visualizations for analytical use cases.
Input elements and form controls for collecting user data.
Components for presenting content: cards, images, text blocks, code snippets, and more.
Actions such as buttons and follow-up prompts to drive user interaction.
# Trigger Actions and Control States
Source: https://docs.thesys.dev/library/triggers
Allow users to take actions, trigger events, or control states in the interface using these components.
**Includes:** `Button`, `FollowupBlock`, `SwitchGroup`, `ToggleGroup`
## Buttons
**Definition**\
A clickable element that initiates an action, with the intended behavior clearly communicated through the button’s label and/or icon.
**Uses**\
**-** Initiating **primary actions** (typically one per screen) that drive the main flow, such as submitting a form, proceeding to the next step, or signing in, etc.\
**-** Providing **alternative action** (secondary) options that support or contrast the primary action, like canceling, skipping, or navigating back.\
**-** Facilitating context-specific **localized/inline interactions**, found inside components.
### Variants
### Sizes
### Button Groups
### Anatomy
### Customize with Prompts
* **Orientation of Button Groups:** Tell C1 whether the button group should be horizontal or vertical.
> Ex. *"Stack these buttons vertically instead of side by side."*
* **Label Content:** Prompt for inclusion of icons in the label and their placement (left, right or both). Prompt C1 to change or use custom text in the label.
> Ex. *"Add a download icon on the right right of the button and change the label to "Get Now"*
* **Button Type:** Customize type of button generated (primary, secondary, tertiary).
> Ex. "*"Change the 'Cancel' button to tertiary style"*
***
## Follow-up Block
**Definition**\
A list of anticipated queries/topics, in logical order, related to the main content where the corresponding information is not preloaded and hidden, but retrieved only when the user clicks.
**Uses**\
**-** Enable curiosity by anticipating further queries about the main content or related subject matter.\
**-** Reduce effort needed to explore related content by making it accessible inline.\
**-** Retain brevity of the main content by assigning peripheral information to the follow-up blocks.
### Customize With Prompts
* **Question Format and Voice:** Customize how follow-up questions are phrased, such as using active or passive voice and choosing between direct questions or conversational prompts.
> Ex. *"Present the follow-ups as topics to be explored rather than questions"*
* **Question Types:** Choose whether questions should be aimed at going deeper into the main content, broaden its coverage, or branch into related areas.
> Ex. *"Add three questions that gradually move from basics to more advanced topics"*
* **Number of Questions:** Change the number of questions to be displayed in a follow-up block can be specified to C1
> Ex. *"Expand this to five detailed follow-ups to cover all aspects"*
## Switch Group
**Definition**
A switch group is a collection of toggle switches that allows users to control multiple related boolean (on/off) settings.
**Uses**\
**-** Provide users quick controls with immediate effect and visual feedback.\
**-** Indicate current state, settings or preferences clearly.
### Variants
### Anatomy
### Prompt-Based UI Controls
* **Default State**: Mention whether a switch in a group should be turned on or off by default.
> Ex. *"Set all switches in the 'Notifications' group to ON by default."*
* **Grouping and Categorization**: Tell C1 if the switches should be further grouped into categories.
> Ex. *"Group switches into 'Account Settings', 'Notifications', and 'Privacy' categories."*
* **Switch Label**: Provide or change the labels for each switch in a group by prompting C1.
> Ex. *"Rename the switches in the 'Notifications' group to 'Email Alerts', 'SMS Alerts', and 'Push Notifications'."*
* **Number of Switches**: Customize the number of switches that should be generated in a group or particular sub-group.
> Ex. *"Add 2 more switches to the 'Accessibility' group."*
# Data Visualizaton
Source: https://docs.thesys.dev/library/visualize-data
Present quantitative data as various graphic forms based on density and complexity of data.
**Includes:** `LineChart`, `AreaChart`, `BarChart`, `PieChart`, `RadarChart`, `RadialChart`
## Line Chart
**Definition** \
A line chart is a type of graph that displays trends or changes in data over a continuous interval by connecting individual data points with lines.
**Uses**\
**-** Show trends, correlations and fluctuations over time.\
**-** Ideal for dense time data where changes between points matter.\
**-** Examples: stock prices, sensor data, temperature readings
**Illustrative Variants**
***
## Area Chart
**Definition**\
An area chart displays quantitative data over a continuous interval using filled areas beneath plotted data points connected with lines.
**Uses**\
**-** Emphasize volume or magnitude of change over time, not just direction.\
**-** Showing cumulative values.\
**-** Examples: Tracking total revenue growth broken down by product line, monitoring energy consumption trends from multiple sources
**Illustrative Variants**
***
## Bar Chart
**Definition**\
A bar chart uses rectangular bars to visually represent and compare values across categories. The length or height of each bar corresponds to the value it represents.
**Uses**\
**-** Compare quantities across discrete groupings such as time intervals, sequential stages in a process, or related variables within a set.\
**-** Ideal for categorical data and most effective when there are few data series, allowing for clear comparison between items.\
**-** Examples: Bug reports by severity level, top 10 highest grossing films, sales by region in Q1
### Vertical Bar Chart
### Horizontal Bar Chart
**Illustrative Variants**
***
## Radial Chart
**Definition**\
A type of chart that has a **circular y-axis** where values are compared based on proportion of the circular path completed (not the length of the arcs).
**Uses**\
**-** Emphasizing completion and visual appeal over comparison.\
**-** Useful when the space available small.\
**-** Examples: progress towards a goal, completion levels for different learning modules\\
**Illustrative Variants**
***
## Pie Chart
**Definition**\
A type of chart that displays proportionate **parts of a whole** as slices of a circle. Each slice represents a category’s contribution to the total, with its angle proportional to the value it represents.
**Uses**\
**-** To show the composition of a finite whole or at a given point in time \
**-** Best used when the data series is categorical and not too dense.\
**-** Examples: market share by brand, survey responses to multiple choice questions, time spent across activities in a day
**Illustrative Variants**
***
## Radar Chart
**Definition**\
A chart that plots three or more variables on axes radiating from a central point. Data points are connected to form a polygon for each data series, showing patterns or comparisons across categories
**Uses**\
**-** Comparing multiple metrics across sub-categories\
**-** Emphasize symmetry, imbalance or dominance\
**-** Examples: evaluating candidates across skills, products across features, showing how departments score across metrics like speed, cost, and quality
**Illustrative Variants**
***
## Anatomy
1. `title`**:** Describes the main insight the chart reveals, supported by a subtitle.
2. `tooltip`**:** Expose the value(s) tied to a point, segment, or shape in the chart during interaction.
3. `grid` **:** Polar (in Radial and Radar Charts) or categorical grid lines for value reference.
4. `legend`**:** Maps colors to data categories they represent. When required, legends may also include value types/units of their respective axes.
### Legend Variants
***
## Prompt-Based UI Controls
A simple prompt in plain language given to C1 is enough to control the following properties.
* **Visibility of Elements**
* Tell C1 whether x and y axis labels should be visible in the chart.
* **Orientation and Layout (only Bar Charts)**
* Ask C1 to use vertical or horizontal bars in Bar Charts.
* Choose whether you want to use a stacked or grouped Bar Chart.
* **Data Units**
* Prompt whether you want the quantitative data to be shown in thousands (K), millions (M), billions (B), or trillions (T).
* **Category Labels**
* Rename category labels shown in the chart (eg. "rename Product A to Mobile").
# Cooking Assistant
Source: https://docs.thesys.dev/prompt-gallery/cooking-assistant
Transform ingredients into customized recipes, with nutritional information and dietary modifications to suit your health goals.
| role | prompt |
| ---------- | ------ |
| **system** | |
| **user** | |
## Sample output
# Data Visualizer
Source: https://docs.thesys.dev/prompt-gallery/data-visualizer
Convert raw data into insightful visualizations with explanations that highlight key trends and patterns for better decision-making.
| role | prompt |
| ---------- | ------ |
| **system** | |
| **user** | |
## Sample output
# Lawyer On-Call
Source: https://docs.thesys.dev/prompt-gallery/lawyer-on-call
Simplify complex legal language into clear explanations, with relevant citations to the original document.
| role | prompt |
| ---------- | ------------------------------------------------ |
| **system** | |
| **user** | Explain the following text to me:
|
## Sample output
# Travel Planner
Source: https://docs.thesys.dev/prompt-gallery/travel-planner
Create personalized itineraries with local recommendations, accommodation options, and transportation logistics based on your preferences.
| role | prompt |
| ---------- | ------ |
| **system** | |
| **user** | |
## Sample output
# Chat Component
Source: https://docs.thesys.dev/react-reference/c1-chat
React component for building conversational AI interfaces with C1's chat functionality.
The `C1Chat` component is a React component that allows you to build conversational AI interfaces with C1. It renders a full chat interface
with a sidebar, composer, user and agent messages. You can customize the `C1Chat` component to your needs through the following props:
```tsx theme={null}
import { C1Chat } from "@thesysai/genui-sdk";
```
## Props
The API endpoint URL that handles the API calls made with each user message.
If provided, the `C1Chat` component automatically handles the API calls to the
backend. If not provided, you will need to manually implement and pass the
`threadManager` and `threadListManager`.
Manager for handling thread-related operations and state. The easiest way to
create this is through the `useThreadManager` hook.
Manager for handling thread list operations and state. The easiest way to
create this is through the `useThreadListManager` hook.
Function to process messages and handle the conversation flow.
The unique identifier for the current thread.
Array of messages in the conversation.
Unique identifier for the response being processed.
Controller for aborting the message processing operation.
**Deprecated**: Use `formFactor` instead. Defines the display type of the chat
component.
Defines the form factor and layout of the chat component. Use `'full-page'` for
a fullscreen chat interface, `'side-panel'` for a sidebar-style layout, or
`'bottom-tray'` for a collapsible tray at the bottom of the page.
When `formFactor` is set to `'bottom-tray'`, the following additional props are available:
Control the open state of the bottom tray (controlled mode).
Callback when the bottom tray open state changes.
Default open state for the bottom tray (uncontrolled mode).
**Deprecated**: Wrap `C1Chat` with `ThemeProvider` instead of passing theme here.
Theme configuration object for customizing the appearance of the chat component.
The theme mode for the chat component. This can be either 'light' or 'dark'.
The main theme configuration object.
The dark theme configuration object used when in dark mode. If not provided,
the `theme` object will be used.
The name of the AI agent that will be displayed in the chat interface.
URL for the agent's logo image. This is displayed alongside the agent's
messages in the chat.
Controls the scrolling behavior of the chat interface. `'once'` scrolls once per
interaction, `'user-message-anchor'` anchors to user messages, and `'always'`
maintains continuous scrolling.
Provide custom components and configuration to override the default behavior and components in the C1 UI.
A library of custom components to use in the C1 UI, keyed by component name.
Custom React component to render the thinking state.
Custom React component rendered at the footer of each assistant response.
Controls how artifacts are displayed. Possible values are `'AUTO_OPEN'`, `'OPEN_ON_MOUNT'`, and `'OVERVIEW'`.
Custom React component rendered as an action button on artifacts.
Function to enable exporting artifacts as PDF. When provided, an export button is shown on artifacts.
Whether artifact editing is enabled. When `true`, users can edit artifacts inline.
When set to `true`, disables the built-in `ThemeProvider` wrapping. Useful when you are
providing your own `ThemeProvider` higher up in the component tree.
Function that generates a shareable link for the current conversation thread.
When provided, a share button is shown in the chat interface.
Function that generates a shareable link for a specific artifact.
When provided, a share button is shown on artifacts.
Callback invoked when a user triggers an action in the chat interface (e.g. clicking a button in a generated UI component).
The type identifier for the action.
Parameters associated with the action.
A human-readable description of the action (legacy field).
A machine-readable description of the action sent to the LLM (legacy field).
Welcome message shown when the thread is empty.
Can be either a props object with `title`, `description`, and optional `image`, or
a custom React component that will be wrapped with `WelcomeScreen` for styling.
```tsx theme={null}
// Props-based configuration
welcomeMessage={{
title: "Hi, I'm C1 Assistant",
description: "I can help you with your questions.",
image: { url: "/logo.png" },
}}
```
Conversation starters shown when the thread is empty. Clickable prompts that
help users begin a conversation with predefined options.
```tsx theme={null}
conversationStarters={{
variant: "short", // "short" for pill buttons, "long" for list items
options: [
{ displayText: "Help me get started", prompt: "Help me get started" },
{ displayText: "What can you do?", prompt: "What can you do?", icon: },
],
}}
```
# C1 Component
Source: https://docs.thesys.dev/react-reference/c1-component
React component for embedding Generative UI into your applications.
`C1Component` is a React component that allows you to embed Generative UI into your applications.
It renders an individual response from the C1 by Thesys API and can be used in a variety of ways to embed
Generative UI into your applications.
```tsx theme={null}
import { C1Component } from "@thesysai/genui-sdk";
```
## Props
The response received through the C1 API that needs to be rendered.
Boolean indicating whether the `c1Response` is currently being streamed.
A function that enables `C1Component` to update the `c1Response` state.
The callback triggered when the user performs an action such as clicking on a button or submitting a form.
The message that is appropriate to display to the user.
The message that should be sent to the LLM as user message.
For example, when the user submits a form by clicking on "Submit" button on the generated UI, "Submit" may be the `humanFriendlyMessage` and the form data may be the `llmFriendlyMessage`.
Thus, you would likely want to use the `llmFriendlyMessage` in your API call to the backend so that the LLM can use the form data to generate a response.
# Theme Provider
Source: https://docs.thesys.dev/react-reference/theme-provider
React component for customizing the C1 UI.
The `ThemeProvider` component provides theme context to all descendant C1 components. It is used to customize the appearance of the C1 UI.
To configure the theme, and switch between light and dark mode, you can pass the following props to the `ThemeProvider` component:
```tsx theme={null}
import { ThemeProvider } from "@thesysai/genui-sdk";
```
## Props
The theme mode for the chat component. This can be either 'light' or 'dark'.
The main theme configuration object. If not provided, the default theme will be used. Partial theme can be provided, in which case,
only the provided properties will be overridden, while the rest of the options will be taken from the default theme.
The dark theme configuration object used when in dark mode. If not provided,
the `theme` object will be used.
# C1 Response
Source: https://docs.thesys.dev/sdk-reference/c1-response
Server-side helpers for constructing and streaming C1 Response.
Unlike standard LLM responses, which are plain text or markdown, a C1 Response is a string that is structured payload to contain rich content.
It uses an XML-like structure to package multiple types of content into a single string.
It is generally stored as assistant message content in the database.
```xml theme={null}
.........
```
### Node.js SDK (`@thesysai/genui-sdk/server`)
This SDK provides a function to create a Response Builder object that manages the creation and streaming of the C1 Response.
```typescript theme={null}
import { makeC1Response } from "@thesysai/genui-sdk/server";
import { transformStream } from "@crayonai/stream";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
// ... use c1Response methods ...
return new NextResponse(c1Response.responseStream, {
headers: { "Content-Type": "text/event-stream" },
});
}
```
#### Main Function
Creates and returns an instance of the Response Builder object.
#### Response Builder Methods
The object returned by `makeC1Response()` has the following methods and properties:
Pipes the raw string response from a C1 API call (e.g., from the Generative UI or Artifacts endpoint) into the response stream.
Adds a custom markdown response to the stream. This will be wrapped in `` tags.
Adds a thinking state entry to the `` part of the response stream.
The title of the thinking state.
A longer description of the current process.
Signals that the stream is complete and closes it. This should be called in the `onEnd` callback of your stream processing.
The `ReadableStream` object that should be returned from your API route.
***
### Python SDK (`thesys_genui_sdk`)
The `thesys_genui_sdk` package provides the `C1Response` class, which acts as the Response Builder.
Please refer to the [Python SDK Reference](https://pypi.org/project/thesys-genui-sdk/) for more details.
### A Practical Example: Adding Thinking States
A common use case for the Response Builder is to add real-time thinking state indicators to the stream before the main content from the LLM arrives. This significantly improves the user's perception of responsiveness.
For a complete, step-by-step example of how to use these helpers, see the **[Thinking States](/guides/thinking-states)** guide.
# Community
Source: https://docs.thesys.dev/support/community
Feel free to send a message in the #help channel on [Discord](https://discord.gg/Pbv5PsqUSv) and someone from our team will get back to you.
# Productionizing
Source: https://docs.thesys.dev/support/production-checklist
Use this checklist to ensure your C1 application is production-ready.
### 1. Use a stable model
Always use one of the latest stable models listed on the [Models & Pricing](/api-reference/models-and-compatibility) page. Avoid using deprecated models.
### 2. Verify model compatibility with your dependencies
Refer to the [Compatibility Matrix](/api-reference/models-and-compatibility) to confirm that your chosen model is compatible with your versions of the Crayon and GenUI SDKs. In general, if your SDK versions meet or exceed the minimum requirements listed, you are good to go. Any backwards-incompatible changes will be noted separately on that page.
### 3. Customize C1 with your brand colors and components (optional)
C1 is highly flexible and can be customized to match your brand colors and components before deployment.
Learn how to customize C1 with your brand colors in the [UI Customizations](/guides/styling) guide.
If you are using `C1Chat`, you may also want to add custom components, such as a sidebar or composer. See the [Integrating Custom Components](/guides/custom-components) guide for details.
To add a footer with share or feedback buttons below each agent response, refer to the [Integrating a Response Footer](/guides/response-footer) guide.
# Troubleshooting
Source: https://docs.thesys.dev/support/troubleshooting
Commonly encountered errors and their solutions
## Generations don't look like the demos
Make sure that you are installing the CSS library and the correct fonts.
```tsx page.tsx theme={null}
import "@crayonai/react-ui/styles/index.css";
```
```css globals.css theme={null}
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
```
## Empty Agent Response
C1 renders an empty agent response as shown above when the component is unable to correctly parse the response content sent by the backend. Here's a checklist to
make sure you're not missing anything:
Check your network tab to ensure that the response streamed by your API follows the expected format. The C1 component expects an XML-formatted response with
the primary content wrapped in a `` tag. Here's an example of the expected format:
```xml theme={null}
```
### The `transformStream` function call
Make sure that the `transformStream` function call on your backend (if you're streaming the response) returns the correct part of the chunk:
```ts theme={null}
transformStream(llmStream, (chunk) => {
return chunk.choices[0]?.delta?.content;
});
```
### The `processMessage` function
If you're using a custom `threadManager` created using a `useThreadManager` hook, make sure that the `processMessage` function is returning the correct
object. The `processMessage` function should directly return the response from your backend, which has already transformed the stream into the correct format:
```ts theme={null}
processMessage: async ({ messages }) => {
const latestMessage = messages[messages.length - 1];
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({
prompt: latestMessage,
threadId: threadListManager.selectedThreadId,
responseId: crypto.randomUUID(),
}),
});
return response;
};
```
Ensure that the LLM model you're using on the backend, the GenUI SDK version, and the Crayon packages are compatible with each other. Refer to the
[Compatibility Matrix](/api-reference/models-and-compatibility) for more information.
## System prompt not being followed
If your model is not following the system prompt, there are a few things you can try:
* **Try rephrasing the prompt:** Try to rephrase the prompt in a different way, and emphasize the instructions that the model was previously not following.
* **Try meta prompting:** Meta prompting is a technique where you use an LLM to generate or refine a prompt to be fed into another LLM. This can help improve accuracy in many cases.
* **Try using a different model:** Try using another stable model from [this list](/api-reference/models-and-compatibility).
## Still having issues?
Feel free to send a message in the #help channel on [Discord](https://discord.gg/Pbv5PsqUSv) and someone from our team will get back to you.