JamAI Base Docs
  • GETTING STARTED
    • Welcome to JamAI Base
      • Why Choose JamAI Base?
      • Key Features
      • Architecture
    • Use Case
      • Chatbot (Frontend Only)
      • Chatbot
      • Create a simple food recommender with JamAI and Flutter
      • Create a simple fitness planner app with JamAI and Next.js with streaming text.
      • Customer Service Chatbot
      • Women Clothing Reviews Analysis Dashboard
      • Geological Survey Investigation Report Generation
      • Medical Insurance Underwriting Automation
      • Medical Records Extraction
    • Frequently Asked Questions (FAQ)
    • Quick Start
      • Quick Start with Chat Table
      • Quick Start: Action Table (Multimodal)
      • ReactJS
      • Next JS
      • SvelteKit
      • Nuxt
      • NLUX (Frontend Only)
      • NLUX + Express.js
  • Using The Platform
    • Action Table
    • Chat Table
    • Knowledge Table
    • Supported Models
      • Which LLM Should You Choose?
      • Comparative Analysis of Large Language Models in Vision Tasks
    • Roadmap
  • 🦉API
    • OpenAPI
    • TS/JS SDK
  • 🦅SDK
    • Flutter
    • TS/JS
    • Python SDK Documentation
      • Quick Start with Chat Table
      • Quick Start: Action Table (Mutimodal)
        • Action Table - Image
        • Action Table - Audio
      • Quick Start: Knowledge Table File Upload
Powered by GitBook
On this page
  • What are we going to build?
  • Project setup
  • Create the Action Table
  • Try your prompts
  • Create your Next.js app
  • Initialize the project
  • Install necessary libraries
  • Create Route Handler
  • Build the UI and use the route handler
  • Run the App
  • Conclusion
  • Next Steps

Was this helpful?

  1. GETTING STARTED
  2. Use Case

Create a simple fitness planner app with JamAI and Next.js with streaming text.

PreviousCreate a simple food recommender with JamAI and FlutterNextCustomer Service Chatbot

Last updated 11 months ago

Was this helpful?

What are we going to build?

This project utilizes Next.js 14 and JamAI to create a personalized daily workout and meal plan based on user profiles (age, height, weight, sex, and desired body type). It leverages streaming for a better user experience.

The full source code of this project can be found on

Project setup

Create the Action Table

First we will create the fitness_planner Action table with the following input columns:

  • age: int

  • height: float

  • weight: float

  • sex: sting

  • preferred_body_type: string

Add workout output column with the following settings.

Prompt:

Suggest daily workout plan for a ${sex} of ${age} years old, height of ${height} centimeters and have a weight of ${weight} KG. The preferred body type is ${preferred_body_type}.

System Prompt:

You are a certified virtual gym trainer with a passion for helping people achieve their fitness goals. Your primary function is to design safe and effective personalized workout plans based on user input. Your response should consist only workout plan. The output should be in the Markdown format.

The model settings are left as default, but you can customize the parameters according to your prefference.

Add meal output column

Prompt:

suggest daily meal plan for a ${sex} of ${age} years old, height of ${height} centimeters and have a weight of ${weight} KG. The preferred body type is ${preferred_body_type}.

System Prompt:

You are an expert virtual nutritionist, you help people to plan their diet based on their age, height, weight, sex, preferred body type and so on. Your response should consist of only diet plan. The output should be in the Markdown format.

Try your prompts

Create your Next.js app

The app will have a single page with a form in the left side and 2 output box to display the output of JamAI in markdown format.

Initialize the project

Create new next.js project and move to that directory.

npx create-next-app@latest
cd (path/to/your/app)

Install JamAI SDK

npm install jamaibase

Add the secrets to .env.local file at the root of your project

NEXT_PUBLIC_JAMAI_BASEURL=http://api.jamaibase.com/
JAMAI_API_KEY=<your_jamai_sk_api_key>
JAMAI_PROJECT_ID=<your_proj_id>

Install necessary libraries

Install vercel AI SDK library to stream the output from route handler and react markdown to display markdown output.

npm i react-markdown ai

Create Route Handler

Create a route handler with the following code:

// src/app/api/get-fitness-suggestion/route/ts

import JamAI from "jamaibase";
import { NextRequest, NextResponse } from "next/server";
import { StreamingTextResponse } from "ai";
import { GenTableStreamChatCompletionChunk } from "jamaibase/resources/gen_tables/chat";

const jamai = new JamAI({
    baseURL: process.env.NEXT_PUBLIC_JAMAI_BASEURL!,
    apiKey: process.env.JAMAI_API_KEY,
    projectId: process.env.JAMAI_PROJECT_ID,
});

export async function POST(request: NextRequest) {
    const body = await request.json();
    try {
        let stream = await jamai.addRowStream({
            table_type: "action",
            data: [
                {
                    age: body.age,
                    height: body.height,
                    weight: body.weight,
                    sex: body.sex,
                    preferred_body_type: body.preferredBodyType,
                },
            ],
            table_id: "fitness_planner",
            reindex: null,
            concurrent: true,
        });

        return new StreamingTextResponse(streamText(stream));
    } catch (error: any) {
        console.error("Error fetching tables:", error.response);
        return NextResponse.json(
            { message: "Internal server error" },
            { status: 500 }
        );
    }
}

Concurrent is set to true, which means the output of the both columns will be generated concurrently.

Add custom streamText function:

function streamText(
    inputStream: ReadableStream<GenTableStreamChatCompletionChunk>
): ReadableStream<string> {
    const reader = inputStream.getReader();

    return new ReadableStream<string>({
        async start(controller) {
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    controller.close();
                    break;
                }
                // Extract the text content from the chunk
                const text = JSON.stringify(value);

                if (text) {
                    // Enqueue the text content as a chunk to the new stream
                    controller.enqueue(text);
                }
            }
        },
        cancel() {
            reader.releaseLock();
        },
    });
}

Build the UI and use the route handler

Add the following code to to page.tsx file:

// src/app/page.tsx

"use client";

import ReactMarkdown from "react-markdown";
import { ChangeEvent, useState } from "react";
import { processJsonStream } from "./utils";
import { GenTableStreamChatCompletionChunk } from "jamaibase/resources/gen_tables/chat";

export default function HomePage() {
    const [age, setAge] = useState<number | null>(null);
    const [weight, setWeight] = useState<number | null>(null);
    const [height, setHeight] = useState<number | null>(null);
    const [preferredBodyType, setPreferredBodyType] = useState<string>("");
    const [workoutPlan, setWorkoutPlan] = useState<string>("");
    const [mealPlan, setMealPlan] = useState<string>("");
    const [isLoading, setIsLoading] = useState<boolean>(false);

    const [sex, setSex] = useState("male");

    const handleSexChange = (e: ChangeEvent<HTMLSelectElement>) => {
        setSex(e.target.value);
    };

    const handleSubmit = async () => {
        setIsLoading(true);
        setWorkoutPlan("");
        setMealPlan("");
        if (age && weight && height && preferredBodyType && sex) {
            const response = await fetch(`/api/get-fitness-suggestion`, {
                method: "POST",

                body: JSON.stringify({
                    age,
                    weight,
                    height,
                    sex,
                    preferredBodyType,
                }),
            });
            if (response.ok && response.body) {
                const reader = response.body.getReader();
                processJsonStream<GenTableStreamChatCompletionChunk>(
                    reader,
                    (content) => {
                        if (content.output_column_name == "workout") {
                            setWorkoutPlan(
                                (prev) =>
                                    prev + content.choices[0]?.message.content
                            );
                        } else {
                            setMealPlan(
                                (prev) =>
                                    prev + content.choices[0]?.message.content
                            );
                        }
                    },
                    () => setIsLoading(false)
                );
            } else {
                alert("Something went wrong");
            }
        } else {
            alert("Please fill in all fields.");
            setIsLoading(false);
        }
    };

    return (
        <main className="p-12 ">
            <h1 className="text-3xl font-bold text-center mb-8 underline">
                My Fitness Planner
            </h1>
            <div className="grid grid-cols-3 mx-6 gap-x-6">
                <div className=" p-6 shadow-md rounded-md">
                    <h2 className="text-2xl font-bold mb-6">
                        Suggested Daily Workout Plan :
                    </h2>
                    <div className="mb-4">
                        <label className="block text-sm font-medium text-gray-700">
                            Age
                        </label>
                        <input
                            type="number"
                            required
                            className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
                            value={age || ""}
                            onChange={(e) => setAge(Number(e.target.value))}
                        />
                    </div>
                    <div className="mb-4">
                        <label className="block text-sm font-medium text-gray-700">
                            Weight (kg)
                        </label>
                        <input
                            type="number"
                            step="0.1"
                            required
                            className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
                            value={weight || ""}
                            onChange={(e) => setWeight(Number(e.target.value))}
                        />
                    </div>
                    <div className="mb-4">
                        <label className="block text-sm font-medium text-gray-700">
                            Height (cm)
                        </label>
                        <input
                            type="number"
                            step="0.1"
                            required
                            className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
                            value={height || ""}
                            onChange={(e) => setHeight(Number(e.target.value))}
                        />
                    </div>
                    <div className="mb-6">
                        <label
                            htmlFor="movieGenre"
                            className="block text-sm font-medium text-gray-700 mb-2"
                        >
                            Sex
                        </label>
                        <select
                            value={sex}
                            onChange={handleSexChange}
                            required
                            className="block w-full py-3 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-black"
                        >
                            <option value="male">Male</option>
                            <option value="female">Female</option>
                        </select>
                    </div>

                    <div className="mb-4">
                        <label className="block text-sm font-medium text-gray-700">
                            Preferred Body Type
                            <span className="text-gray-500">
                                (eg: lean, athletic, bulky)
                            </span>
                        </label>
                        <input
                            type="text"
                            required
                            className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
                            value={preferredBodyType}
                            onChange={(e) =>
                                setPreferredBodyType(e.target.value)
                            }
                        />
                    </div>
                    <button
                        disabled={isLoading}
                        onClick={handleSubmit}
                        className={`w-full  text-white py-2 px-4 rounded-md  ${
                            !isLoading
                                ? "bg-indigo-500 hover:bg-indigo-600"
                                : "bg-gray-500 hover:bg-gray-500 cursor-default"
                        }`}
                    >
                        Get Workout Plan
                    </button>
                </div>

                <div className="p-4 border  border-gray-300 rounded-md">
                    <h2 className="text-2xl font-bold mb-6">
                        Suggested Daily Workout Plan :
                    </h2>
                    <ReactMarkdown>{workoutPlan}</ReactMarkdown>
                </div>
                <div className="p-4 border  border-gray-300 rounded-md">
                    <h2 className="text-2xl font-bold mb-6">
                        Suggested Daily Meal Plan :
                    </h2>
                    <ReactMarkdown>{mealPlan}</ReactMarkdown>
                </div>
            </div>
        </main>
    );
}

Process JSON Stream

When sending streams, it's common for the chunks to get concentrated. So, if we want to parse them, they will fail. So, we will write a utility function to process the JSON stream on the client side.

// src/lib/utils.ts

export async function processJsonStream<T>(
    reader: ReadableStreamDefaultReader<Uint8Array>,
    processContent: (content: T) => void,
    handleComplete?: () => void,
    handleError?: (error: any) => void
) {
    const decoder = new TextDecoder("utf-8");
    let buffer = "";

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, { stream: true });
        buffer += chunk;

        // Split by the delimiter "}{" }
        let boundary = buffer.indexOf("}{");

        while (boundary !== -1) {
            const jsonString = buffer.substring(0, boundary + 1);
            buffer = buffer.substring(boundary + 1);

            try {
                const parsedChunk = JSON.parse(jsonString);
                processContent(parsedChunk);
            } catch (error) {
                console.error("Error parsing JSON chunk:", jsonString);
                handleError && handleError(error);
            }

            boundary = buffer.indexOf("}{");
        }

        // Attempt to parse whatever is left in the buffer
        try {
            const parsedChunk = JSON.parse(buffer);
            processContent(parsedChunk);
            buffer = "";
        } catch (error) {
            // Continue buffering since we might not have a complete JSON object yet
        }
    }

    if (buffer.trim()) {
        try {
            const parsedChunk = JSON.parse(buffer.trim());
            processContent(parsedChunk);
        } catch (error) {
            console.error("Error parsing remaining buffer:", buffer.trim());
            handleError && handleError(error);
        }
    }

    handleComplete && handleComplete();
}

Run the App

npm run dev

Conclusion

Once you get your app running, you can look at your Jamai Action table rows to review the data that you can access with your app.

Next Steps

We have demonstrated how easy it is to store data and use LLM's to update your cell values with prompts directly. This simple example demonstrates the simple use of an Action Table here in your app. If you have related data you want to leverage and make your app smarter, you can implement the Knowledge Table to make your Action Tables smarter with Retrieval Augmented Generation (RAG).

For example, you can add your menu catalog and let meal columns recommend meals based on your menu.

Once you're happy with the results, use the JamAi in your Next.js app.

Generate API Key -> Go to and collect API Key

Collect Project ID -> Go to and copy the project id.

As the SDK/API requires API Key, we will use it on the server side for secure interaction. So, we will write a that will allow us to create custom request handlers for a given route using the Web and APIs.

The above code first create a JamAI client and then use the funtion to add a new row to the action table and get the output as stream.

Unfortunately, the StreamingTextResponse method of only accepts texts but the chunk returned by the sdk is of GenTableStreamChatCompletionChunk type. Therefore, in order to stringify the chunk, use the following function.

JS/TS SDK
cloud.jamaibase.com/organization/secrets
cloud.jamaibase.com/project
route handler
Request
Response
addRowStream
vercel AI sdk
JamAIBase cookbook.
Fitness Planner Web Application.
creating action table
Fitness Planner app.