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
- Use server-side proxy - Never expose API keys in client code
- Handle loading states - Show typing indicators during requests
- Error handling - Provide user-friendly error messages
- Auto-scroll - Scroll to bottom when new messages arrive
- 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