> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thesys.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an AI Search Engine

> 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.

<iframe src="https://search-with-c1.vercel.app" width="100%" height="600px" style={{ border: "1px solid #e5e7eb", borderRadius: "8px" }} title="AI Search Demo" />

**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

<CodeGroup dropdown>
  ```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
  ```
</CodeGroup>

When prompted, select:

* TypeScript: Yes
* ESLint: Yes
* Tailwind CSS: Yes
* App Router: Yes
* Customize default import alias: No

### Install Dependencies

<CodeGroup dropdown>
  ```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
  ```
</CodeGroup>

If using Google Gemini instead of Exa:

<CodeGroup dropdown>
  ```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
  ```
</CodeGroup>

Optional (for image search):

<CodeGroup dropdown>
  ```bash npm theme={null}
  npm install google-images
  ```

  ```bash pnpm theme={null}
  pnpm add google-images
  ```

  ```bash yarn theme={null}
  yarn add google-images
  ```
</CodeGroup>

### 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
```

<Tip>
  We recommend Exa for better neural search results. Get your API key from [exa.ai](https://exa.ai).
</Tip>

## Step 1: Set Up Search Tools

C1 uses OpenAI's tool calling to trigger searches. Here's how to define a search tool:

<CodeGroup dropdown>
  ```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"],
              },
          },
      }]
  ```
</CodeGroup>

<Tip>
  The `writeThinkItem` calls show users what's happening while search runs in the background. This creates a better UX than silent loading.
</Tip>

## 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:

<CodeGroup dropdown>
  ```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,
      }
  ```
</CodeGroup>

### Option B: Google Gemini with Grounding

Gemini 2.5 has built-in Google Search grounding:

<CodeGroup dropdown>
  ```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
  ```
</CodeGroup>

## Step 3: Create the Main API Endpoint

Now connect everything with C1:

<CodeGroup dropdown>
  ```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)
  ```
</CodeGroup>

## 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.`;
```

<Note>
  The key is being specific about **when** to use visual components and **how** to structure them. Vague prompts lead to inconsistent results.
</Note>

## 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:

<CodeGroup dropdown>
  ```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",
          }
  ```
</CodeGroup>

### How Image Search Works

The C1 SDK handles images automatically through a `searchImage` callback:

<CodeGroup dropdown>
  ```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
  <C1Component
    c1Response={response}
    searchImage={imageSearch}  // SDK calls this when it needs images
  />
  ```

  ```python Frontend Integration theme={null}
  # For Python backends, the frontend still uses React
  # The searchImage prop works the same way regardless of backend language
  ```
</CodeGroup>

**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`.

<Note>
  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 }`.
</Note>

## Step 6: Add Caching (Optional but Recommended)

Cache responses to avoid re-searching identical queries:

<CodeGroup dropdown>
  ```typescript cache.ts theme={null}
  const cache = new Map<string, { response: string; timestamp: number }>();
  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(),
      }
  ```
</CodeGroup>

## Step 7: Add Thread Management for Follow-ups (Optional)

To enable follow-up questions that reference previous searches, implement thread management:

<CodeGroup dropdown>
  ```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<string, ThreadMessage[]>();

  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
  ```
</CodeGroup>

### Using Thread History

Update your main endpoint to include thread history:

<CodeGroup dropdown>
  ```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)
  ```
</CodeGroup>

### 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.

<Note>
  **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.
</Note>

## 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 (
    <div className="min-h-screen bg-gray-50">
      <C1Chat
        apiUrl="/api/ask"
        threadId={threadId}
        searchImage={searchImage}
        placeholder="Search anything..."
        title="AI Search"
      />
    </div>
  );
}
```

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<string> {
  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:

<CodeGroup dropdown>
  ```bash npm theme={null}
  npm run dev
  ```

  ```bash pnpm theme={null}
  pnpm dev
  ```

  ```bash yarn theme={null}
  yarn dev
  ```
</CodeGroup>

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

<Note>
  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.
</Note>

## Key Concepts

<AccordionGroup>
  <Accordion title="Why use tool calling instead of direct API calls?">
    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.
  </Accordion>

  <Accordion title="Exa vs Gemini: Which should I use?">
    * **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).
  </Accordion>

  <Accordion title="How does C1 know what UI to generate?">
    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.
  </Accordion>

  <Accordion title="Can I add my own data sources?">
    Yes! Create additional tools for databases, APIs, or documents. C1 can combine web search with your private data.
  </Accordion>
</AccordionGroup>

## 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

<CardGroup cols={2}>
  <Card title="Try Live Demo" icon="play" href="https://search-with-c1.vercel.app">
    Experience the complete AI search app in action. Search for anything and see C1 generate beautiful, contextual UI in real-time.

    **Try it now →**
  </Card>

  <Card title="View Source Code" icon="github" href="https://github.com/thesysdev/search-with-c1">
    Complete implementation with thread management, error handling, caching, and deployment config. Everything from this guide and more.

    **Star on GitHub →**
  </Card>
</CardGroup>
