ChatIQ
ContactSolutionsDocsBlogDashboard
Sign inSign up
ContactSolutionsDocsBlogDashboard
Sign inSign up
ChatIQ

Build reliable AI chatbots powered by your team’s knowledge. Secure multi-tenant architecture, instant document ingestion, and guided analytics out of the box.

Product
How it worksFeaturesDemo
Resources
SolutionsBlogDocsContactDashboard
Legal & Preferences
Terms of ServicePrivacy PolicySecurityDPA
© 2026 ChatIQ. All rights reserved.Made with care in distributed workspaces worldwide.

Vue.js Integration

Integrate ChatIQ chatbots into your Vue.js application with composables and components.

Vue.js Integration

Integrate ChatIQ chatbots into your Vue.js application using composables and reusable components.


Overview

This guide covers:

  • Creating a Vue composable for chat functionality
  • Building reusable chat components
  • Handling streaming responses
  • Managing conversation state with Vue reactivity

Note: This guide uses Vue 3 with Composition API. For Vue 2, you'll need to adapt the examples.


Prerequisites

  • Vue 3.x (Composition API)
  • An API key from your ChatIQ dashboard
  • A backend proxy endpoint (recommended) or server-side API access

Step 1: Set Up Environment Variables

Create a .env file (for your backend):

CHATIQ_API_KEY=sk_live_your_api_key_here
CHATIQ_BOT_SLUG=your-bot-slug
CHATIQ_API_URL=https://chatiq.io/api

Step 2: Create a Backend Proxy

Create an API route that proxies requests to ChatIQ (keeps API key secure):

Nuxt.js API Route Example

// server/api/chatbot.post.ts
import { defineEventHandler, readBody } from 'h3';

export default defineEventHandler(async (event) => {
  const { message, conversation_id } = await readBody(event);

  const response = await fetch('https://chatiq.io/api/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.CHATIQ_API_KEY}`,
    },
    body: JSON.stringify({
      message,
      bot_slug: process.env.CHATIQ_BOT_SLUG,
      stream: true,
      conversation_id: conversation_id || null,
    }),
  });

  return new Response(response.body, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
});

Express.js Example

// server.js
import express from 'express';

const app = express();
app.use(express.json());

app.post('/api/chatbot', async (req, res) => {
  const { message, conversation_id } = req.body;

  const response = await fetch('https://chatiq.io/api/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.CHATIQ_API_KEY}`,
    },
    body: JSON.stringify({
      message,
      bot_slug: process.env.CHATIQ_BOT_SLUG,
      stream: true,
      conversation_id: conversation_id || null,
    }),
  });

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    res.write(chunk);
  }

  res.end();
});

app.listen(3000);

Step 3: Create a Chat Composable

Create a composable to manage chat state and API calls:

// composables/useChatIQ.ts
import { ref, computed } from 'vue';

interface Message {
  role: 'user' | 'assistant';
  content: string;
}

interface UseChatIQOptions {
  apiUrl?: string;
  initialMessages?: Message[];
}

export function useChatIQ(options: UseChatIQOptions = {}) {
  const { apiUrl = '/api/chatbot', initialMessages = [] } = options;

  const messages = ref<Message[]>(initialMessages);
  const isLoading = ref(false);
  const error = ref<Error | null>(null);
  const conversationId = ref<string | null>(null);

  const sendMessage = async (message: string) => {
    if (isLoading.value || !message.trim()) return;

    isLoading.value = true;
    error.value = null;

    // Add user message immediately
    messages.value.push({
      role: 'user',
      content: message,
    });

    // Create placeholder for assistant response
    const assistantIndex = messages.value.length;
    messages.value.push({
      role: 'assistant',
      content: '',
    });

    try {
      const response = await fetch(apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          message,
          conversation_id: conversationId.value,
          stream: true,
        }),
      });

      if (!response.body) {
        throw new Error('No response body');
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullResponse = '';
      let receivedConversationId: string | null = null;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value, { stream: true });
        const lines = chunk.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = line.slice(6).trim();

            if (data === '[DONE]') {
              if (receivedConversationId) {
                conversationId.value = receivedConversationId;
              }
              break;
            }

            try {
              const parsed = JSON.parse(data);

              if (parsed.conversationId) {
                receivedConversationId = parsed.conversationId;
              }

              const content = parsed.choices?.[0]?.delta?.content;
              if (content) {
                fullResponse += content;
                messages.value[assistantIndex].content = fullResponse;
              }
            } catch (e) {
              // Skip invalid JSON
            }
          }
        }
      }
    } catch (err) {
      error.value = err instanceof Error ? err : new Error('Unknown error');
      messages.value[assistantIndex].content =
        'Sorry, something went wrong. Please try again.';
    } finally {
      isLoading.value = false;
    }
  };

  const clearMessages = () => {
    messages.value = [];
    conversationId.value = null;
    error.value = null;
  };

  return {
    messages,
    sendMessage,
    clearMessages,
    isLoading,
    error,
    conversationId,
  };
}

Step 4: Create a Chat Component

Create a reusable chat component:

<!-- components/ChatWidget.vue -->
<template>
  <div class="chat-container">
    <div class="chat-messages" ref="messagesContainer">
      <div v-if="messages.length === 0" class="empty-state">
        <p>Start a conversation...</p>
      </div>
      <div
        v-for="(msg, idx) in messages"
        :key="idx"
        :class="['message', msg.role]"
      >
        <div class="message-content">{{ msg.content }}</div>
      </div>
      <div v-if="isLoading && messages.length > 0" class="message assistant">
        <div class="typing-indicator">
          <span></span>
          <span></span>
          <span></span>
        </div>
      </div>
      <div v-if="error" class="error-message">
        Error: {{ error.message }}
      </div>
    </div>

    <form @submit.prevent="handleSubmit" class="chat-input">
      <input
        v-model="input"
        type="text"
        placeholder="Type your message..."
        :disabled="isLoading"
      />
      <button type="submit" :disabled="isLoading || !input.trim()">
        {{ isLoading ? 'Sending...' : 'Send' }}
      </button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, watch } from 'vue';
import { useChatIQ } from '../composables/useChatIQ';

const { messages, sendMessage, isLoading, error } = useChatIQ();
const input = ref('');
const messagesContainer = ref<HTMLElement | null>(null);

const handleSubmit = async () => {
  if (!input.value.trim() || isLoading.value) return;

  const message = input.value;
  input.value = '';

  await sendMessage(message);
  await nextTick();
  scrollToBottom();
};

const scrollToBottom = () => {
  if (messagesContainer.value) {
    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
  }
};

watch(messages, () => {
  nextTick(() => {
    scrollToBottom();
  });
}, { deep: true });
</script>

<style scoped>
.chat-container {
  display: flex;
  flex-direction: column;
  max-width: 600px;
  height: 600px;
  margin: 0 auto;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  background: #f9f9f9;
}

.empty-state {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #999;
}

.message {
  margin-bottom: 15px;
  padding: 12px;
  border-radius: 8px;
  max-width: 80%;
  word-wrap: break-word;
}

.message.user {
  background: #007bff;
  color: white;
  margin-left: auto;
  text-align: right;
}

.message.assistant {
  background: white;
  border: 1px solid #ddd;
  margin-right: auto;
}

.message-content {
  white-space: pre-wrap;
}

.typing-indicator {
  display: flex;
  gap: 4px;
  padding: 8px;
}

.typing-indicator span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #999;
  animation: typing 1.4s infinite;
}

.typing-indicator span:nth-child(2) {
  animation-delay: 0.2s;
}

.typing-indicator span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes typing {
  0%, 60%, 100% {
    transform: translateY(0);
    opacity: 0.7;
  }
  30% {
    transform: translateY(-10px);
    opacity: 1;
  }
}

.chat-input {
  display: flex;
  padding: 15px;
  background: white;
  border-top: 1px solid #ddd;
  gap: 10px;
}

.chat-input input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.chat-input input:focus {
  outline: none;
  border-color: #007bff;
}

.chat-input button {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.chat-input button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.error-message {
  padding: 10px;
  background: #fee;
  color: #c33;
  border-radius: 4px;
  margin-bottom: 15px;
}
</style>

Step 5: Use in Your App

Use the component in your Vue app:

<!-- App.vue -->
<template>
  <div id="app">
    <header>
      <h1>ChatIQ Vue Example</h1>
    </header>
    <main>
      <ChatWidget />
    </main>
  </div>
</template>

<script setup lang="ts">
import ChatWidget from './components/ChatWidget.vue';
</script>

Step 6: Nuxt.js Integration

For Nuxt.js applications, you can use the composable directly:

<!-- pages/chat.vue -->
<template>
  <div>
    <ChatWidget />
  </div>
</template>

<script setup>
// Nuxt auto-imports composables from composables/ directory
// useChatIQ is automatically available
</script>

Advanced Features

Add Message Timestamps

interface Message {
  role: 'user' | 'assistant';
  content: string;
  timestamp?: Date;
}

// In composable
messages.value.push({
  role: 'user',
  content: message,
  timestamp: new Date(),
});

Conversation History Persistence

import { watch } from 'vue';

// Save to localStorage
watch(messages, (newMessages) => {
  if (newMessages.length > 0) {
    localStorage.setItem('chatMessages', JSON.stringify(newMessages));
    localStorage.setItem('conversationId', conversationId.value || '');
  }
}, { deep: true });

// Load on mount
onMounted(() => {
  const saved = localStorage.getItem('chatMessages');
  const savedId = localStorage.getItem('conversationId');
  if (saved) {
    messages.value = JSON.parse(saved);
    if (savedId) conversationId.value = savedId;
  }
});

Pinia Store (Optional)

For more complex state management:

// stores/chat.ts
import { defineStore } from 'pinia';

export const useChatStore = defineStore('chat', {
  state: () => ({
    messages: [] as Message[],
    isLoading: false,
    error: null as Error | null,
    conversationId: null as string | null,
  }),

  actions: {
    async sendMessage(message: string) {
      // Implementation similar to composable
    },
  },
});

TypeScript Support

Create types for better type safety:

// types/chat.ts
export interface Message {
  role: 'user' | 'assistant';
  content: string;
  timestamp?: Date;
}

export interface ChatIQOptions {
  apiUrl?: string;
  botSlug?: string;
  apiKey?: string;
  stream?: boolean;
}

Best Practices

  1. Use server-side proxy - Never expose API keys in client code
  2. Handle loading states - Show typing indicators during requests
  3. Error handling - Provide user-friendly error messages
  4. Auto-scroll - Scroll to bottom when new messages arrive
  5. Conversation persistence - Save conversation state for better UX

Next Steps

  • Add message reactions or feedback
  • Implement file upload support
  • Add voice input/output
  • Customize styling to match your brand
  • Integrate with Vue Router for chat history

See Also

  • React Integration - React implementation
  • Next.js Integration - Next.js specific guide
  • Streaming Example - Streaming implementation details