Trong bài viết này, tôi sẽ chia sẻ kinh nghiệm thực chiến khi xây dựng một AI Chat Component hoàn chỉnh bằng Vue.js 3, tích hợp trực tiếp với HolySheep AI API — nền tảng với chi phí thấp hơn tới 85% so với các provider khác.
Phân Tích Chi Phí AI API 2026 — Con Số Thực Tế
Khi tôi bắt đầu dự án chatbot cho khách hàng doanh nghiệp, điều đầu tiên cần làm là phân tích chi phí vận hành. Dưới đây là bảng so sánh giá Output token tháng 6/2026:
| Model | Giá/MTok Output | 10M Tokens/Tháng |
|---|---|---|
| GPT-4.1 | $8.00 | $80.00 |
| Claude Sonnet 4.5 | $15.00 | $150.00 |
| Gemini 2.5 Flash | $2.50 | $25.00 |
| DeepSeek V3.2 | $0.42 | $4.20 |
Tỷ giá ¥1 = $1 tại HolyShehep AI giúp bạn tiết kiệm đáng kể. Với cùng 10 triệu token mỗi tháng, DeepSeek V3.2 chỉ tốn $4.20 thay vì $150 với Claude. Đây là lý do tôi chọn HolySheep cho mọi dự án production.
Khởi Tạo Dự Án Vue.js 3
Đầu tiên, tạo project Vue.js 3 với Composition API — cách tiếp cận hiện đại và linh hoạt nhất:
npm create vue@latest ai-chat-app
cd ai-chat-app
npm install
Các dependencies cần thiết
npm install axios marked highlight.js
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Cấu trúc thư mục mà tôi sử dụng trong các dự án thực tế:
src/
├── components/
│ ├── ChatContainer.vue
│ ├── ChatMessage.vue
│ ├── ChatInput.vue
│ └── TypingIndicator.vue
├── composables/
│ └── useChatAPI.js
├── services/
│ └── api.js
├── assets/
│ └── main.css
├── App.vue
└── main.js
Composable Xử Lý Chat API
Đây là phần core logic — nơi tôi xử lý streaming response và error handling. Composables giúp tái sử dụng code hiệu quả:
// src/composables/useChatAPI.js
import { ref, readonly } from 'vue'
import axios from 'axios'
const HOLYSHEEP_BASE_URL = 'https://api.holysheep.ai/v1'
export function useChatAPI() {
const messages = ref([])
const isLoading = ref(false)
const error = ref(null)
const streamContent = ref('')
const apiKey = import.meta.env.VITE_HOLYSHEEP_API_KEY
const sendMessage = async (userMessage, model = 'deepseek-chat') => {
if (!userMessage.trim()) return
// Thêm tin nhắn user
messages.value.push({
role: 'user',
content: userMessage,
timestamp: Date.now()
})
isLoading.value = true
error.value = null
streamContent.value = ''
try {
const response = await axios.post(
${HOLYSHEEP_BASE_URL}/chat/completions,
{
model: model,
messages: messages.value.map(m => ({
role: m.role,
content: m.content
})),
stream: true
},
{
headers: {
'Authorization': Bearer ${apiKey},
'Content-Type': 'application/json'
},
responseType: 'stream',
onDownloadProgress: (progressEvent) => {
const lines = progressEvent.event.target.response
.split('\n')
.filter(line => line.trim())
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') continue
try {
const parsed = JSON.parse(data)
const content = parsed.choices?.[0]?.delta?.content || ''
if (content) {
streamContent.value += content
}
} catch (e) {
// Ignore parse errors for incomplete chunks
}
}
}
}
}
)
// Thêm assistant response vào messages
if (streamContent.value) {
messages.value.push({
role: 'assistant',
content: streamContent.value,
timestamp: Date.now()
})
}
} catch (err) {
error.value = err.response?.data?.error?.message || err.message
console.error('API Error:', err)
} finally {
isLoading.value = false
streamContent.value = ''
}
}
const clearMessages = () => {
messages.value = []
error.value = null
}
return {
messages: readonly(messages),
isLoading: readonly(isLoading),
error: readonly(error),
streamContent: readonly(streamContent),
sendMessage,
clearMessages
}
}
Component Chat Message với Syntax Highlighting
Tin nhắn AI cần hiển thị code với syntax highlighting đẹp mắt. Tôi dùng marked và highlight.js:
<!-- src/components/ChatMessage.vue -->
<template>
<div
class="flex mb-4"
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
>
<div
class="max-w-[75%] rounded-2xl px-4 py-3"
:class="message.role === 'user'
? 'bg-blue-600 text-white rounded-br-md'
: 'bg-gray-100 text-gray-800 rounded-bl-md'"
>
<div
class="prose prose-sm max-w-none"
v-html="renderedContent"
></div>
<div
class="text-xs mt-2 opacity-70"
:class="message.role === 'user' ? 'text-blue-100' : 'text-gray-400'"
>
{{ formatTime(message.timestamp) }}
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
const props = defineProps({
message: {
type: Object,
required: true
}
})
// Configure marked with highlight.js
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
breaks: true,
gfm: true
})
const renderedContent = computed(() => {
return marked.parse(props.message.content || '')
})
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('vi-VN', {
hour: '2-digit',
minute: '2-digit'
})
}
</script>
Component ChatInput với Auto-resize
Input component với tính năng auto-resize textarea giúp UX mượt mà hơn:
<!-- src/components/ChatInput.vue -->
<template>
<div class="border-t bg-white p-4">
<div class="flex gap-3 items-end max-w-4xl mx-auto">
<div class="flex-1 relative">
<textarea
ref="textareaRef"
v-model="inputText"
@keydown.enter.exact.prevent="handleSend"
@input="autoResize"
placeholder="Nhập tin nhắn... (Enter để gửi)"
rows="1"
class="w-full resize-none border border-gray-300 rounded-xl px-4 py-3
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:border-transparent max-h-40"
></textarea>
</div>
<button
@click="handleSend"
:disabled="!inputText.trim() || disabled"
class="px-6 py-3 bg-blue-600 text-white rounded-xl font-medium
hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed
transition-colors flex items-center gap-2"
>
<svg v-if="disabled" class="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span>Gửi</span>
</button>
</div>
<div class="text-center text-xs text-gray-400 mt-2">
Độ trễ trung bình: <50ms với HolySheep AI
</div>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const props = defineProps({
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['send'])
const inputText = ref('')
const textareaRef = ref(null)
const handleSend = () => {
if (inputText.value.trim() && !props.disabled) {
emit('send', inputText.value)
inputText.value = ''
nextTick(() => autoResize())
}
}
const autoResize = () => {
const textarea = textareaRef.value
if (textarea) {
textarea.style.height = 'auto'
textarea.style.height = Math.min(textarea.scrollHeight, 160) + 'px'
}
}
</script>
Tích Hợp Hoàn Chỉnh trong App.vue
Đây là cách tôi组装 tất cả components lại với nhau:
<!-- src/App.vue -->
<template>
<div class="min-h-screen bg-gray-50 flex flex-col">
<!-- Header -->
<header class="bg-white shadow-sm py-4 px-6">
<div class="max-w-4xl mx-auto flex justify-between items-center">
<h1 class="text-xl font-bold text-gray-800">AI Chat Demo</h1>
<div class="flex gap-2">
<select
v-model="selectedModel"
class="border rounded-lg px-3 py-1 text-sm"
>
<option value="deepseek-chat">DeepSeek V3.2 ($0.42/MTok)</option>
<option value="gpt-4.1">GPT-4.1 ($8/MTok)</option>
<option value="claude-sonnet-4.5">Claude Sonnet 4.5 ($15/MTok)</option>
<option value="gemini-2.5-flash">Gemini 2.5 Flash ($2.50/MTok)</option>
</select>
<button
@click="clearMessages"
class="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded-lg"
>
Xóa chat
</button>
</div>
</div>
</header>
<!-- Messages -->
<main class="flex-1 max-w-4xl w-full mx-auto p-4 overflow-y-auto">
<ChatMessage
v-for="(msg, index) in messages"
:key="index"
:message="msg"
/>
<ChatMessage
v-if="streamContent"
:message="{ role: 'assistant', content: streamContent, timestamp: Date.now() }"
/>
<div v-if="error" class="text-red-500 text-center py-4">
Lỗi: {{ error }}
</div>
</main>
<!-- Input -->
<ChatInput
:disabled="isLoading"
@send="handleSend"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useChatAPI } from './composables/useChatAPI'
import ChatMessage from './components/ChatMessage.vue'
import ChatInput from './components/ChatInput.vue'
const { messages, isLoading, error, streamContent, sendMessage, clearMessages } = useChatAPI()
const selectedModel = ref('deepseek-chat')
const handleSend = async (text) => {
await sendMessage(text, selectedModel.value)
}
</script>
Tính Năng Nâng Cao: System Prompt và Context Management
Trong các dự án thực tế, bạn cần context management để duy trì conversation flow. Đây là cách implement:
// src/composables/useChatWithContext.js
import { ref, computed } from 'vue'
import { useChatAPI } from './useChatAPI'
const SYSTEM_PROMPT = `Bạn là trợ lý AI chuyên nghiệp. Trả lời ngắn gọn, rõ ràng, có cấu trúc:
- Sử dụng markdown khi cần
- Code luôn có giải thích
- Nếu không biết, nói thẳng không invent`
export function useChatWithContext(maxMessages = 20) {
const {
messages, isLoading, error, streamContent,
sendMessage, clearMessages
} = useChatAPI()
const conversationHistory = computed(() => {
const history = [{ role: 'system', content: SYSTEM_PROMPT }]
const recentMessages = messages.value.slice(-maxMessages)
return [...history, ...recentMessages.map(m => ({
role: m.role,
content: m.content
}))]
})
const sendMessageWithContext = async (userMessage, model) => {
// Tự động thêm system prompt vào messages
if (messages.value.length === 0) {
messages.value.push({
role: 'system',
content: SYSTEM_PROMPT,
timestamp: Date.now()
})
}
await sendMessage(userMessage, model)
}
const estimatedCost = computed(() => {
let totalTokens = 0
messages.value.forEach(m => {
totalTokens += m.content.split(/\s+/).length * 1.3 // rough estimate
})
return {
tokens: Math.round(totalTokens),
gpt4Cost: (totalTokens / 1_000_000) * 8,
deepseekCost: (totalTokens / 1_000_000) * 0.42
}
})
return {
messages,
isLoading,
error,
streamContent,
conversationHistory,
estimatedCost,
sendMessage: sendMessageWithContext,
clearMessages
}
}
Lỗi Thường Gặp và Cách Khắc Phục
1. Lỗi CORS khi gọi API
Mô tả: Browser chặn request với lỗi "Access-Control-Allow-Origin"
// ❌ Không hoạt động - CORS error
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': Bearer ${apiKey} },
body: JSON.stringify({ model: 'deepseek-chat', messages: [...] })
})
// ✅ Giải pháp: Dùng proxy hoặc backend
// Option 1: Backend proxy (Node.js/Express)
app.post('/api/chat', async (req, res) => {
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': Bearer ${process.env.HOLYSHEEP_API_KEY},
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
})
res.json(await response.json())
})
// Option 2: Vue proxy config (vite.config.js)
export default defineConfig({
server: {
proxy: {
'/api/holysheep': {
target: 'https://api.holysheep.ai/v1',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/holysheep/, '')
}
}
}
})
2. Lỗi Streaming Response không hiển thị
Mô tả: Streaming hoạt động nhưng không cập nhật UI real-time
// ❌ Vấn đề: streamContent là readonly, không thể update trong callback
onDownloadProgress: (progressEvent) => {
streamContent.value += chunk // Lỗi!
}
// ✅ Giải pháp: Dùng biến tạm
const tempStreamContent = ref('')
const sendMessage = async (userMessage, model) => {
tempStreamContent.value = ''
// ... setup axios
onDownloadProgress: (progressEvent) => {
const chunk = processStreamChunk(progressEvent)
tempStreamContent.value += chunk
streamContent.value = tempStreamContent.value // Trigger reactivity
}
}
// Hoặc dùng EventSource cho non-streaming endpoint
const sendMessageSSE = async (userMessage, model) => {
const response = await fetch(${HOLYSHEEP_BASE_URL}/chat/completions, {
method: 'POST',
headers: {
'Authorization': Bearer ${apiKey},
'Content-Type': 'application/json'
},
body: JSON.stringify({ model, messages: [...], stream: true })
})
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)
// Parse SSE data: data: {"choices":[...]}\n\n
for (const line of chunk.split('\n')) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
const content = data.choices?.[0]?.delta?.content || ''
tempStreamContent.value += content
streamContent.value = tempStreamContent.value
}
}
}
}
3. Lỗi Context Window Exceeded
Mô tả: API trả về lỗi "Maximum context length exceeded"
// ❌ Không kiểm soát context - dễ exceed limit
const sendMessage = async (text) => {
messages.value.push({ role: 'user', content: text })
await api.send({ messages: messages.value }) // Càng ngày càng dài!
}
// ✅ Giải pháp: Rolling context window
const MAX_TOKENS_ESTIMATE = 6000 //留下一半 cho response
const sendMessageWithContextLimit = async (userMessage, model) => {
messages.value.push({ role: 'user', content: userMessage })