AI
Builder Hub
build-ai2026-03-1318 min

Build AI Chatbot Với Next.js: Từ Hello World Đến Streaming Response Như ChatGPT

Chatbot AI không còn là magic. Tutorial này hướng dẫn build chatbot thực sự với streaming responses, conversation history, và system prompt tùy chỉnh — dùng Next.js và OpenAI SDK.

Build AI Chatbot Với Next.js: Từ Hello World Đến Streaming Response Như ChatGPT

Điều làm ChatGPT cảm giác "sống" là streaming — chữ xuất hiện dần dần thay vì chờ toàn bộ response rồi mới hiện. Tutorial này build chatbot với streaming hoàn chỉnh, conversation history, và system prompt tùy chỉnh.


📌 TL;DR: Kết Quả Cuối Bài

  • Chatbot với giao diện chat đẹp (dark theme)
  • Streaming response — chữ hiện dần như ChatGPT
  • Conversation history — AI nhớ context trong session
  • System prompt tùy chỉnh — định nghĩa tính cách AI
  • Tech: Next.js 14 + Vercel AI SDK + OpenAI GPT-4o

Setup

npx create-next-app@latest ai-chatbot --typescript --tailwind --app
cd ai-chatbot

# Vercel AI SDK — wrapper tiện lợi cho streaming
npm install ai @ai-sdk/openai

File .env.local:

OPENAI_API_KEY=sk-proj-your-key-here

API Route Với Streaming

Tạo file app/api/chat/route.ts:

import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
import { NextRequest } from 'next/server'

// System prompt định nghĩa tính cách chatbot
const SYSTEM_PROMPT = `Bạn là AI Builder Hub Assistant — trợ lý thông minh giúp người dùng học và ứng dụng AI.

Phong cách:
- Thân thiện, rõ ràng, thực tế
- Ưu tiên ví dụ cụ thể hơn lý thuyết dài dòng  
- Khi không biết, nói thẳng "tôi không chắc về điều này"
- Ngôn ngữ: tiếng Việt là mặc định, trả lời cùng ngôn ngữ với user

Không được:
- Giả mạo thông tin
- Nói những câu generic như "AI đang thay đổi thế giới"
- Trả lời quá dài khi câu hỏi đơn giản`

export async function POST(request: NextRequest) {
  const { messages } = await request.json()
  
  // streamText từ Vercel AI SDK xử lý streaming tự động
  const result = await streamText({
    model: openai('gpt-4o'),
    system: SYSTEM_PROMPT,
    messages,
    maxTokens: 1000
  })
  
  // Trả về streaming response
  return result.toDataStreamResponse()
}

Giao Diện Chat

Tạo app/page.tsx:

'use client'

import { useChat } from 'ai/react'
import { useEffect, useRef } from 'react'

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat'
  })
  
  const messagesEndRef = useRef<HTMLDivElement>(null)
  
  // Auto-scroll xuống cuối khi có message mới
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  return (
    <div className="flex flex-col h-screen bg-gray-950 text-white">
      {/* Header */}
      <header className="border-b border-gray-800 px-6 py-4 flex items-center gap-3">
        <div className="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-center">
          🤖
        </div>
        <div>
          <h1 className="font-semibold">AI Builder Hub Assistant</h1>
          <p className="text-sm text-gray-400">GPT-4o · Streaming enabled</p>
        </div>
      </header>
      
      {/* Messages */}
      <div className="flex-1 overflow-y-auto px-4 py-6">
        {messages.length === 0 && (
          <div className="text-center text-gray-500 mt-20">
            <p className="text-4xl mb-4">💬</p>
            <p className="text-lg">Hỏi tôi bất cứ điều gì về AI...</p>
            <div className="mt-6 flex flex-wrap gap-2 justify-center">
              {['ChatGPT vs Claude', 'Bắt đầu với AI', 'Build AI App', 'Prompt hay'].map(s => (
                <button
                  key={s}
                  onClick={() => {
                    handleInputChange({ target: { value: s } } as React.ChangeEvent<HTMLInputElement>)
                  }}
                  className="text-sm px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-full transition-colors"
                >
                  {s}
                </button>
              ))}
            </div>
          </div>
        )}
        
        <div className="max-w-3xl mx-auto space-y-6">
          {messages.map((message) => (
            <div
              key={message.id}
              className={`flex gap-4 ${message.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}
            >
              {/* Avatar */}
              <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 text-sm ${
                message.role === 'user'
                  ? 'bg-blue-600'
                  : 'bg-gradient-to-r from-blue-500 to-cyan-500'
              }`}>
                {message.role === 'user' ? '👤' : '🤖'}
              </div>
              
              {/* Bubble */}
              <div className={`max-w-[80%] rounded-2xl px-4 py-3 ${
                message.role === 'user'
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-900 border border-gray-800 text-gray-100'
              }`}>
                <p className="whitespace-pre-wrap leading-relaxed">{message.content}</p>
              </div>
            </div>
          ))}
          
          {/* Loading indicator */}
          {isLoading && (
            <div className="flex gap-4">
              <div className="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-center text-sm">
                🤖
              </div>
              <div className="bg-gray-900 border border-gray-800 rounded-2xl px-4 py-3">
                <span className="text-gray-400 animate-pulse">Đang trả lời...</span>
              </div>
            </div>
          )}
          
          <div ref={messagesEndRef} />
        </div>
      </div>
      
      {/* Input */}
      <div className="border-t border-gray-800 p-4">
        <form onSubmit={handleSubmit} className="max-w-3xl mx-auto flex gap-3">
          <input
            value={input}
            onChange={handleInputChange}
            placeholder="Nhập câu hỏi... (Enter để gửi)"
            className="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 focus:outline-none focus:border-blue-500 transition-colors"
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading || !input.trim()}
            className="px-6 py-3 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:from-blue-600 hover:to-cyan-600 transition-all"
          >
            {isLoading ? '⟳' : '→'}
          </button>
        </form>
        <p className="text-center text-xs text-gray-600 mt-2">
          AI có thể mắc lỗi — luôn verify thông tin quan trọng
        </p>
      </div>
    </div>
  )
}

Chạy Và Test

npm run dev

Mở http://localhost:3000 và bắt đầu chat. Bạn sẽ thấy response streaming real-time.


Hiểu Cách Streaming Hoạt Động

User gửi message
     ↓
API Route: streamText() gọi OpenAI
     ↓
OpenAI trả về tokens TỪNG CÁI MỘT
     ↓
toDataStreamResponse() stream tokens về browser
     ↓
useChat() hook nhận từng token, update UI real-time
     ↓
User thấy chữ xuất hiện dần dần

Tại sao streaming quan trọng: Response thông thường chờ AI generate xong rồi mới show (5-15 giây). Streaming show từng từ ngay khi AI generate — cảm giác responsive hơn nhiều dù tổng thời gian như nhau.


Mở Rộng Tiếp

Lưu conversation history: Dùng localStorage hoặc database để giữ lịch sử giữa các sessions

Nhiều chatbot với system prompts khác nhau: Customer support bot, coding assistant, writing coach

Thêm file upload: Cho phép attach PDF để AI trả lời về file đó

Rate limiting: Tránh bị abuse nếu deploy public


Đọc thêm: