Tambo Lockup

Start Here

Get an introduction to Tambo's features by using our starter app

Download and run our generative UI chat template to get an understanding of the fundamental features of Tambo. This application will show you how to build generative UI, integrate tools, send messages to Tambo, and stream responses.

Demo GIF

Template Installation

1. Download the code

Create and navigate to your project
npm create tambo-app@latest my-tambo-app && cd my-tambo-app

This will copy the source code of the template app into your directory and install the dependencies. See the source repo here.

2. Get a Tambo API key

Initialize Tambo
npx tambo init

To send messages to Tambo, you need to create a Project through Tambo and generate an API key to send with requests. This command will walk you through the setup of your first Tambo project, generate an API key, and set the API key in your project automatically.

3. Run the app

Run the app
npm run dev

Start the app and go to localhost:3000 in your browser to start sending messages to Tambo!

Customize

To get a better understanding of what's happening in this application, try to make a change to update Tambo's capabilities.

In /src/lib/tambo.ts you'll see how the template registers components and tools with Tambo. In /src/app/chat/page.tsx you'll see how those tools and components are passed to the TamboProvider to 'register' them.

Add a component

Let's create and register a new component with Tambo to give our AI chat a new feature.

In src/components create a new file called recipe-card.tsx and paste the following code into it:

recipe-card.tsx
"use client";

import { ChefHat, Clock, Minus, Plus, Users } from "lucide-react";
import { useState } from "react";

interface Ingredient {
  name: string;
  amount: number;
  unit: string;
}

interface RecipeCardProps {
  title?: string;
  description?: string;
  ingredients?: Ingredient[];
  prepTime?: number; // in minutes
  cookTime?: number; // in minutes
  originalServings?: number;
}

export default function RecipeCard({
  title,
  description,
  ingredients,
  prepTime = 0,
  cookTime = 0,
  originalServings,
}: RecipeCardProps) {
  const [servings, setServings] = useState(originalServings || 1);

  const scaleFactor = servings / (originalServings || 1);

  const handleServingsChange = (newServings: number) => {
    if (newServings > 0) {
      setServings(newServings);
    }
  };

  const formatTime = (minutes: number) => {
    if (minutes < 60) {
      return `${minutes}m`;
    }
    const hours = Math.floor(minutes / 60);
    const remainingMinutes = minutes % 60;
    return remainingMinutes > 0
      ? `${hours}h ${remainingMinutes}m`
      : `${hours}h`;
  };

  const totalTime = prepTime + cookTime;

  return (
    <div className="bg-white max-w-md rounded-xl shadow-lg overflow-hidden border border-gray-200">
      <div className="p-6">
        <div className="mb-4">
          <h2 className="text-2xl font-bold text-gray-900 mb-2">{title}</h2>
          <p className="text-gray-600 text-sm leading-relaxed">{description}</p>
        </div>

        <div className="flex items-center gap-6 mb-6 text-sm text-gray-600">
          <div className="flex items-center gap-2">
            <Clock className="w-4 h-4" />
            <span>{formatTime(totalTime)}</span>
          </div>
          <div className="flex items-center gap-2">
            <Users className="w-4 h-4" />
            <span>{servings} servings</span>
          </div>
          {prepTime > 0 && (
            <div className="flex items-center gap-2">
              <ChefHat className="w-4 h-4" />
              <span>Prep: {formatTime(prepTime)}</span>
            </div>
          )}
        </div>

        <div className="mb-6 p-4 bg-gray-50 rounded-lg">
          <div className="flex items-center justify-between">
            <span className="font-medium text-gray-700">Adjust Servings:</span>
            <div className="flex items-center gap-3">
              <button
                onClick={() => handleServingsChange(servings - 1)}
                className="p-2 rounded-full bg-white border border-gray-300 hover:bg-gray-50 transition-colors"
                disabled={servings <= 1}
              >
                <Minus className="w-4 h-4" />
              </button>
              <span className="font-semibold text-lg min-w-[3rem] text-center">
                {servings}
              </span>
              <button
                onClick={() => handleServingsChange(servings + 1)}
                className="p-2 rounded-full bg-white border border-gray-300 hover:bg-gray-50 transition-colors"
              >
                <Plus className="w-4 h-4" />
              </button>
            </div>
          </div>
        </div>

        <div className="mb-6">
          <h3 className="text-lg font-semibold text-gray-900 mb-3">
            Ingredients
          </h3>
          <ul className="space-y-2">
            {ingredients?.map((ingredient, index) => (
              <li
                key={index}
                className="flex items-center gap-3 p-2 rounded-md hover:bg-gray-50 transition-colors"
              >
                <div className="w-2 h-2 bg-orange-500 rounded-full flex-shrink-0" />
                <span className="font-medium text-gray-900">
                  {(ingredient.amount * scaleFactor).toFixed(
                    (ingredient.amount * scaleFactor) % 1 === 0 ? 0 : 1,
                  )}
                </span>
                <span className="text-gray-600">{ingredient.unit}</span>
                <span className="text-gray-800">{ingredient.name}</span>
              </li>
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
}

Register it with Tambo by updating the components array in src/lib/tambo.ts with the following entry:

tambo.ts
  {
    name: "RecipeCard",
    description: "A component that renders a recipe card",
    component: RecipeCard,
    propsSchema: z.object({
      title: z.string().describe("The title of the recipe"),
      description: z.string().describe("The description of the recipe"),
      prepTime: z.number().describe("The prep time of the recipe in minutes"),
      cookTime: z.number().describe("The cook time of the recipe in minutes"),
      originalServings: z
        .number()
        .describe("The original servings of the recipe"),
      ingredients: z
        .array(
          z.object({
            name: z.string().describe("The name of the ingredient"),
            amount: z.number().describe("The amount of the ingredient"),
            unit: z.string().describe("The unit of the ingredient"),
          })
        )
        .describe("The ingredients of the recipe"),
    }),
  },

Now refresh the browser page and send a message like "Show me a recipe" and you should see Tambo generate and stream in an instance of your RecipeCard component.

usage of RecipeCard

Add a tool

You might notice that when using our added RecipeCard component above, Tambo generates recipe data completely from scratch. To allow Tambo to retrieve the list of ingredients we actually have, we can add a tool to get them.

In src/lib/tambo.ts add the following entry to the 'tools' array:

tambo.ts
  {
    name: "get-available-ingredients",
    description:
      "Get a list of all the available ingredients that can be used in a recipe.",
    tool: () => [
      "pizza dough",
      "mozzarella cheese",
      "tomatoes",
      "basil",
      "olive oil",
      "chicken breast",
      "ground beef",
      "onions",
      "garlic",
      "bell peppers",
      "mushrooms",
      "pasta",
      "rice",
      "eggs",
      "bread",
    ],
    toolSchema: z.function().returns(z.array(z.string())),
  },

Now refresh the browser page and send a message like "Show me a recipe I can make" and you should see Tambo look for the available ingredients and then generate a RecipeCard using them.

usage of a tool

Going Further

This template app just scratches the surface of what you can build with Tambo. By using Tambo in creative ways you can make truly magical custom user experiences!