Automatically Generate AI Chat Titles with Animation in Nuxt

Learn how to use AI to automatically generate catchy, three-word chat titles based on the first user message in your Nuxt app. Add polish and personality with a smooth typewriter animation for an engaging user experience.

Michael Thiessen
Nuxt 3

The Mastering Nuxt FullStack Unleashed Course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view course

This article is based on a lesson from Mastering Nuxt: Full Stack Unleashed.

When you build a chat application, the little details matter.

One of those details is how you name each conversation. Instead of asking users to type a title, why not let AI do the work? And while we're at it, let's make the result pop with a fun animation.

Let's walk through how you can use an AI-powered endpoint to generate chat titles based on the user's first message. We'll also add a typewriter animation to make those titles stand out.

aititle.gif

Here's what we'll do:

  • Update the AI prompt to generate better titles.
  • Wire up the frontend so new chats get AI-generated titles automatically.
  • Add a typewriter animation for visual flair.

Let's dive in.

Step 1: Improve the AI Title Generation Prompt

The first step is to make sure our AI generates concise and relevant titles. We want each title to capture the essence of the first message, but keep it short: three words or less.

Here's how we update the AI service:

// layers/chat/server/services/ai-service.ts

export async function generateChatTitle(
  model: LanguageModelV1,
  firstMessage: string
): Promise<string> {
  const response = await generateText({
    model,
    messages: [
      {
        role: 'system',
        content:
          'You are a helpful assistant that generates concise, descriptive titles for chat conversations. Generate a title that captures the essence of the first message in 3 short words or less.',
      },
      {
        role: 'user',
        content: firstMessage,
      },
    ],
  })
  return response.text.trim()
}

The generateChatTitle function takes two arguments: the AI model and the user's first message. We pass a system prompt that tells the AI to act as an assistant that creates short, descriptive titles. Then, we include the user's message as the user prompt.

By asking for "three short words or less," we get crisp, relevant titles.

Step 2: Update the Chat Creation Flow

Now, we want our frontend to use this AI endpoint whenever a new chat starts. This means updating the composables that handle chat creation and messaging.

Updating the useChats Composable

Previously, new chats were created locally with a generic title like "Chat 1." Now, we want to create chats via the server, so the backend can assign a title.

Here's the updated method:

// layers/chat/app/composables/useChats.ts

async function createChat(
  options: { projectId?: string; title?: string } = {}
) {
  const newChat = await $fetch<Chat>('/api/chats', {
    method: 'POST',
    body: {
      title: options.title,
      projectId: options.projectId,
    },
  })

  chats.value.push(newChat)

  return newChat
}

Instead of generating the chat locally, we make a POST request to /api/chats. This lets the server handle things like assigning an ID and (eventually) updating the title.

The createChatAndNavigate method can then use this to create a chat and navigate to it.

Adding Title Generation to the useChat Composable

Next, let's add a method that can request an AI-generated title for a chat. We'll call this when the user sends their first message.

Here's the new method:

// layers/chat/app/composables/useChat.ts

async function generateChatTitle(message: string) {
  if (!chat.value) return

  const updatedChat = await $fetch<Chat>(
    `/api/chats/${chatId}/title`,
    {
      method: 'POST',
      body: {
        message,
      },
    }
  )
  chat.value.title = updatedChat.title
}

This function sends the user's first message to the server, which responds with an updated chat object containing the new title. We then update the local chat's title.

Integrating Title Generation into Message Sending

Now, let's make sure we call this function at the right time: when the user sends the very first message in a chat.

Here's how we update the sendMessage method:

// layers/chat/app/composables/useChat.ts

async function sendMessage(message: string) {
  if (!chat.value) return

  if (messages.value.length === 0) {
    generateChatTitle(message)
  }

  const newMessage = await $fetch<ChatMessage>(
    `/api/chats/${chatId}/messages`,
    {
      method: 'POST',
      body: {
        content: message,
        role: 'user',
      },
    }
  )
  messages.value.push(newMessage)

  const aiResponse = await $fetch<ChatMessage>(
    `/api/chats/${chatId}/messages/generate`,
    {
      method: 'POST',
    }
  )
  messages.value.push(aiResponse)

  chat.value.updatedAt = new Date()
}

Notice that we call generateChatTitle(message) only if this is the first message (messages.value.length === 0). We don't use await here, so the title generation runs in parallel with sending the message and getting the AI's response. This way, the UI doesn't wait for the title to be ready before showing the message.

Step 3: Add a Typewriter Animation for Titles

A static title is fine, but a little animation can make your app feel more alive. Let's add a typewriter effect to the chat title.

Move the Typewriter Component

Here's the full component:

<!-- layers/base/app/components/TypewriterText.vue -->
<template>
  <span ref="textEl" class="typewriter-text">{{
    text
  }}</span>
</template>

<script setup lang="ts">
import { watch, ref } from 'vue'

const props = defineProps<{
  text: string
}>()

const textEl = ref<HTMLElement | null>(null)

const animate = (el: HTMLElement) => {
  const chars = el.textContent?.length || 0
  el.animate(
    [
      { clipPath: 'inset(0 100% 0 0)' },
      { clipPath: 'inset(0 0 0 0)' },
    ],
    {
      duration: Math.min(chars * 50, 1000), // Cap at 1 second
      easing: 'steps(' + chars + ', end)',
    }
  )
}

// Watch for text changes and trigger animation
watch(
  () => props.text,
  () => {
    if (textEl.value) {
      animate(textEl.value)
    }
  }
)
</script>

<style scoped>
.typewriter-text {
  display: inline-block;
  position: relative;
  white-space: nowrap;
}
</style>

This component animates the text using a CSS clip-path and the Web Animations API. When the text prop changes, it triggers the animation.

Let's break down how this works.

The animation begins with clip-path: inset(0 100% 0 0), which initially hides all text by creating a clipping region that's 0% from the top, 100% from the right, 0% from the bottom, and 0% from the left - essentially making the visible area zero width.

Then the animation transitions to clip-path: inset(0 0 0 0), which reveals the full text by removing the clipping mask entirely.

The magic happens with the steps() easing function.

This is what creates the typewriter effect. It divides the animation into discrete steps (one per character) rather than a smooth transition, making each character appear individually one after another.

We calculate the animation duration based on the number of characters (50ms per character), but cap it at 1 second to ensure longer texts don't animate too slowly. This gives us a consistent and pleasant experience regardless of title length.

Finally, the .typewriter-text styles ensure the text doesn't wrap and maintains proper positioning during the animation, keeping everything clean and aligned.

Use the Typewriter Animation in the Chat Window

Now, let's update the chat window to use this animated title.

<!-- layers/chat/app/components/ChatWindow.vue -->
<template>
  <div ref="scrollContainer" class="scroll-container">
    <UContainer class="chat-container">
      <!-- ... -->
      <template v-else>
        <div class="chat-header">
          <h1 class="title">
            <TypewriterText
              :text="chat.title || 'Untitled Chat'"
            />
          </h1>
        </div>
        <!-- ... -->
      </template>
    </UContainer>
  </div>
</template>

Instead of displaying the title as plain text, we wrap it in the TypewriterText component. Now, whenever the title changes (for example, when the AI generates a new one), it animates in with a typewriter effect.

What Happens Now?

When a user starts a new chat and sends their first message, the following happens:

  1. The message is sent as usual.
  2. In the background, the app requests an AI-generated title for the chat.
  3. When the title arrives, it updates in the UI with a typewriter animation.

This makes the chat experience feel smarter and more lively. Users get a relevant, catchy title for each conversation, and the animation draws attention to the new title.

Wrapping Up

By letting AI generate chat titles and adding a bit of animation, you make your chat app more helpful and fun to use. Users don't have to think up titles, and the animated effect gives your interface a little extra personality.

You can take this idea further!

Maybe add more animations, or let users edit the AI-generated title if they want. But even this simple improvement goes a long way toward making your app feel polished and engaging.

Michael Thiessen
Michael is a passionate full time Vue.js and Nuxt.js educator. His weekly newsletter is sent to over 11,000 Vue developers, and he has written over a hundred articles for his blog and VueSchool.

Follow MasteringNuxt on