🔥 ZUNDER Alpha sprays its first sparks. See it in action

Tutorials

Build a ChatGPT Clone

Build a ChatGPT Clone with Nuxt 3

Creating a ChatGPT clone is something we can start with to get familiar with openai and llms. It seems challenging first, but with the right tools and clear instructions, it’s within reach for anyone. In this guide, we’ll walk you through building a ChatGPT-like app using Nuxt 3 and Zunder. We’ll use the Zunder UI to build our application even faster.

Setting Up a New Nuxt 3 Project

Let’s kick off by setting up a new Nuxt 3 project. Open your terminal and run the following command to create a new Nuxt app:

npx nuxi init my-chatgpt-clone
cd my-chatgpt-clone

After creating the project, install the dependencies:

pnpm install

Adding UI Component libraries

As mentioned earlier out stack is based on nuxt 3. So we’ll use the @nuxt/ui module along with @zunderai/ui for AI-specific components.

Step 1: Install @nuxt/ui

First, install the @nuxt/ui module:

npx nuxi@latest module add ui

And then @zunderai/ui ui extension.

pnpm add @zunderai/ui

Creating the ZChatPage Component

Before we're building the chat interface, let’s create a ZChatPage component that will serve as the layout for our chat application.

Create a new file in your components directory called ZChatPage.vue and add the following code:

components/ZChatPage.vue
<template>
    <div class="flex flex-col h-screen bg-gray-50 dark:bg-gray-900">
        <!-- Header -->
        <header class="bg-white dark:bg-gray-800 shadow">
            <UContainer>
                <div class="flex justify-between items-center py-4">
                    <h1 class="text-2xl text-gray-900 dark:text-white"><span class="font-bold">ZUNDER</span> <span class="font-light italic">AI CHAT</span></h1>
                    <div class="flex items-center space-x-4" />
                </div>
            </UContainer>
        </header>

        <!-- Main chat area -->
        <main class="flex-grow overflow-hidden">
            <UContainer class="h-full flex flex-col">
                <div class="flex-grow overflow-y-auto py-4">
                    <slot name="messages"></slot>
                </div>
                <div class="py-4">
                    <slot name="input"></slot>
                </div>
            </UContainer>
        </main>

        <!-- Footer -->
        <footer class="bg-white dark:bg-gray-800 shadow">
            <UContainer>
                <div class="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
                    <slot name="footer">My Zunder.ai Chat</slot>
                </div>
            </UContainer>
        </footer>
    </div>
</template>

I won't go deep into details of this component.

It creates a simple chat page layout with a header, main chat area, and footer.

Building the Initial index.vue Page

Step 1: Create a Simple Template

We’ll start by setting up the basic structure of our chat interface in pages/index.vue. This first step focuses on creating a simple template and logging interactions to the console. This way, we build the foundation without overwhelming details.

pages/index.vue
<template>
  <ZChatPage>
    <template #messages>
      <ZChatMessages :messages="chatMessages" />
    </template>
    <template #input>
      <ZChatInput @submit="handleSubmit" :loading="loading" placeholder="Type your message here..." />
    </template>
  </ZChatPage>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import type { ChatMessage } from '~/types/chat';

const chatMessages = ref<ChatMessage[]>([]);
const loading = ref(false);

watch(chatMessages, (newMessages) => {
  console.log('Chat messages updated:', newMessages);
}, { deep: true });

const handleSubmit = (text: string) => {
  // Log user input for now
  console.log('User input:', text);

  // Add a simple user message to chatMessages
  chatMessages.value.push({
    isUser: true,
    avatar: 'U',
    content: text,
  });

  console.log('Chat messages after user input:', chatMessages.value);
};
</script>

Please add also a typescript type for the chat messages. This prevents us to make mistakes when we are typing the chat messages.

types/chat.ts
export type ChatMessage = {
    isUser: boolean;
    avatar: string;
    content: string;
}

In this step, the chat interface is functional at a basic level. When the user submits a message, it logs the message to the console and updates the chat with the user’s input. This approach helps you understand the basic structure before diving into more complex features.

You already can try it out. Run the following command to start the development server:

pnpm run dev

And open your browser and navigate to http://localhost:3000.

Voila 🥳.

Step 2: Extending the Functionality

Now that we have the basics in place, let’s extend the functionality by setting up a simple server-side API to simulate AI responses.

Building the Server API

Step 1: Create a Simulated Chat API

Let’s first create a simulated chat API endpoint in server/api/chat-test.ts. This will allow us to test our chat interface with some predefined responses before connecting to a real AI service.

server/api/chat-test.ts
import { defineEventHandler } from "h3";

const simulatedResponses = [
  "Hello! How can I assist you today?",
  "That's an interesting question. Let me think about it for a moment.",
  "I'm sorry, I don't have enough information to answer that accurately. Could you provide more details?",
  "Based on what you've told me, I would suggest the following...",
  "That's a great point! Have you also considered...",
  "I'm afraid I don't have a definitive answer for that, but here's what I know...",
  "Let me summarize what we've discussed so far...",
  "Is there anything else you'd like to know about this topic?",
  "That's all the information I have on this subject. Is there something else I can help you with?",
  "I'm an AI assistant, so I don't have personal experiences, but I can provide information on that topic if you'd like.",
];

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const message = body.message;

  // Simulate a delay (between 1 and 3 seconds)
  const delay = Math.floor(Math.random() * 1000) + 1000;
  await new Promise((resolve) => setTimeout(resolve, delay));

  // Select a random response
  const responseIndex = Math.floor(Math.random() * simulatedResponses.length);
  const responseContent = simulatedResponses[responseIndex];

  // Return a simulated response
  return {
    content: `${responseContent}\n\nYou said: "${message}"`,
  };
});

This setup allows you to make simple API calls and test the chat interface with simulated responses. It’s a great way to ensure everything is functioning correctly before introducing more complexity.

Step 2: Connect the Frontend to the Simulated API

Update the handleSubmit function in index.vue to send requests to the simulated API:

pages/index.vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { ChatMessage } from '~/types/chat';

const chatMessages = ref<ChatMessage[]>([]);
const loading = ref(false);

watch(chatMessages, (newMessages) => {
  console.log('Chat messages updated:', newMessages);
}, { deep: true });

const handleSubmit = async (text: string) => {
  // Add user message
  chatMessages.value.push({
    isUser: true,
    avatar: 'U',
    content: text,
  });
  console.log('Chat messages after user input:', chatMessages.value);

  loading.value = true;

  try {
    const aiMessage: ChatMessage = await $fetch('/api/chat-test', {
      method: 'POST',
      body: { message: text },
    });
    chatMessages.value.push({
        isUser: false,
        avatar: "AI",
        content: aiMessage.content,
    });
  } catch (error) {
    console.error('Error sending message:', error);
  } finally {
    loading.value = false;
  }
};
</script>

This connection allows your chat interface to interact with the simulated API, making it feel like a real chat application.

Try it again in your browser. You can now see the chat messages in the browser with a simulated AI response.

Bringing in the Real AI with OpenAI

Now that the basic chat interface and simulated API are working, let’s connect the app to a real AI service using OpenAI.

Step 1: Create the OpenAI Chat API

Create a new server-side API endpoint in server/api/chat.ts that connects to OpenAI:

server/api/chat.ts
import OpenAI from "openai";
import { defineEventHandler, readBody } from "h3";

export default defineEventHandler(async (event) => {
  const {
    openaiApiKey,
    openaiModel,
  } = useRuntimeConfig()

  const openai = new OpenAI({ apiKey: openaiApiKey })
  const body = await readBody(event);
  console.log("Received body:", body);

  const messages = [];

  messages.push({
    role: 'system',
    content: 'You are a helpful assistant'
  });
  messages.push({ role: 'user', content: body.message });

  try {
    const completion = await openai.chat.completions.create({
      model: openaiModel,
      messages: messages,
    });

    return {
      content: completion.choices[0].message.content,
    };
  } catch (error) {
    console.error('Error calling OpenAI API:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Error processing chat request',
    });
  }
});

Step 2: Update the Frontend to Use the Real API

Finally, update the handleSubmit function in index.vue to send requests to the real OpenAI API:

pages/index.vue
<script setup lang="ts">
import { ref } from 'vue';

const chatMessages = ref<ChatMessage[]>([]);
const loading = ref(false);

const handleSubmit = async (text: string) => {
  // Add user message
  chatMessages.value.push({
    isUser: true,
    avatar: 'U',
    content: text,
  });
  console.log('Chat messages after user input:', chatMessages.value);

  loading.value = true;

  try {
    const aiMessage: ChatMessage = await $fetch('/api/chat', {
      method: 'POST',
      body: { message: text },
    });
    chatMessages.value.push({
        isUser: false,
        avatar: "AI",
        content: aiMessage.content,
    });
  } catch (error) {
    console.error('Error sending message:', error);
    // Handle error (e.g., show an error message to the user)
  } finally {
    loading.value = false;
  }
};
</script>

Now, your chat interface will be connected to OpenAI, allowing it to generate responses using a real AI model.

Bonus Step: Adding a Color Mode Toggle

As a bonus, let’s create a ColorModeButton.client.vue component to allow users to toggle between light and dark modes.

Step 1: Create the ColorModeButton.client.vue Component

Create a new file in your components directory called ColorModeButton.client.vue and add the following code:

components/ColorModeButton.client.vue
<script setup lang="ts">
const colorMode = useColorMode()

const isDark = computed(() => colorMode.value === 'dark')

const toggleColorMode = () => {
    colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
\</script>

<template>
    <UButton :icon="isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'" color="gray" variant="ghost"
        aria-label="Toggle color mode" @click="toggleColorMode" />
</template>

Step 2: Add the Color Mode Button to ZChatPage

Finally, integrate the ColorModeButton.client.vue into the ZChatPage component by replacing the placeholder with this actual functionality.

components/ZChatPage.vue
<template>
    <div class="flex flex-col h-screen bg-gray-50 dark:bg-gray-900">
        <!-- Header -->
        <header class="bg-white dark:bg-gray-800 shadow">
            <UContainer>
                <div class="flex justify-between items-center py-4">
                    <h1 class="text-2xl text-gray-900 dark:text-white"><span class="font-bold">ZUNDER</span> <span
                            class="font-light italic">AI CHAT</span></h1>
                    <div class="flex items-center space-x-4">
                        <ZColorModeButton />
                    </div>
                </div>
            </UContainer>
        </header>

        <!-- Main chat area -->
        <main class="flex-grow overflow-hidden">
            <UContainer class="h-full flex flex-col">
                <div class="flex-grow overflow-y-auto py-4">
                    <slot name="messages"></slot>
                </div>
                <div class="py-4">
                    <slot name="input"></slot>
                </div>
            </UContainer>
        </main>

        <!-- Footer -->
        <footer class="bg-white dark:bg-gray-800 shadow">
            <UContainer>
                <div class="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
                    © 2024 ZUNDERAI
                </div>
            </UContainer>
        </footer>
    </div>
</template>

Final Step: Deploying to Vercel

Once everything is ready and tested locally, it’s time to deploy your application to Vercel.

Step 1: Push Your Code to GitHub

Make sure your project is in a GitHub repository.

Step 2: Connect to Vercel

Go to Vercel’s website, connect your GitHub account, and import your repository.

Step 3: Deploy

Vercel will handle the deployment. You’ll get a live URL where your ChatGPT clone will be accessible.

Conclusion

By following these steps, you’ve created a basic ChatGPT clone using Nuxt 3, @nuxt/ui, and @zunderai/ui. You started with a simple page structure, tested with a simulated API, and then connected to a real AI service using OpenAI. This approach ensures you build a functional application without getting overwhelmed by complexity from the start.

Happy coding! 🎉