Tạo form hỏi đáp tương tác trong vài phút và prompt chỉnh sửa
Mua gói Thành viên QuanTriMang Pro để trải nghiệm website không quảng cáo và sử dụng các tiện ích AI trên QuanTriMang.
Bạn muốn có một biểu mẫu thu thập câu hỏi cho trang cá nhân, lớp học hay dự án nhỏ, nhưng không muốn dựng cả môi trường lập trình chỉ để làm một cái form? Với vibe coding , bạn chỉ cần mô tả điều mình muốn cho Claude, kiểm tra kết quả ngay trên màn hình rồi nhờ nó chỉnh tiếp.
Bài này hướng dẫn bạn làm một form hỏi đáp tương tác hoàn chỉnh. Form có kiểm tra dữ liệu, thông báo toast, lịch sử câu hỏi và đủ gọn để chạy trong một file HTML duy nhất.
Vibe coding là gì?
Thuật ngữ “vibe coding” xuất hiện từ bài đăng của Andrej Karpathy vào ngày 2/2/2025. Karpathy từng là Giám đốc AI tại Tesla và là một trong những người sáng lập OpenAI. Cách làm này nghĩa là bạn mô tả ý định bằng ngôn ngữ tự nhiên rồi giao phần viết mã cho AI.
Trong thực tế, vai trò của bạn là người định hướng sản phẩm. Bạn nêu yêu cầu, chạy thử, nhận xét rồi yêu cầu sửa. Bạn cũng chả cần phải thuộc cú pháp, nhưng vẫn phải kiểm tra kết quả và chịu trách nhiệm về sản phẩm cuối cùng.

Vì sao form hỏi đáp là bài tập khởi đầu tốt?
Form hỏi đáp nhỏ nhưng chứa đủ các thành phần cơ bản của một ứng dụng web:
- Nhập liệu: Ô văn bản, danh sách chọn, vùng nội dung dài.
- Kiểm tra dữ liệu: Báo lỗi đúng chỗ, đúng lúc.
- Phản hồi trực quan: Thông báo thành công tự biến mất.
- Lưu trữ: Giữ lại lịch sử ngay trên trình duyệt.
- Bảo mật cơ bản: Xử lý nội dung người dùng nhập vào trước khi hiển thị.
Làm xong bài này, bạn có thể áp dụng cách tư duy tương tự cho form đăng ký, khảo sát hay phiếu góp ý.
Chuẩn bị trước khi bắt đầu
- Một tài khoản Claude tại claude.ai. Artifacts dùng được trên cả gói miễn phí.
- Bật tính năng Code execution and file creation trong Settings > Capabilities. Claude hiện không hỗ trợ artifacts khi tính năng này đang tắt.
- Một trình duyệt bất kỳ để mở file kết quả.
Artifact là gì? Claude tạo artifact khi nội dung đủ lớn và tự đứng được một mình, thường trên 15 dòng. Bạn thấy trang web chạy ngay bên cạnh khung chat, không cần chuyển tab hay cài thêm phần mềm.

Quy trình 3 vòng bao gồm: Prompt, kiểm tra, tinh chỉnh
Vòng 1: Gửi prompt tổng thể
Một prompt tốt cần có ba phần bao gồm: Vai trò của AI, công nghệ sử dụng và danh sách hành vi cụ thể. Bạn có thể dùng đoạn prompt sau để thử:
Hãy viết giúp tôi một trang web hoàn chỉnh (dạng Single-file HTML) sử dụng Tailwind CSS (qua CDN) và Vanilla JavaScript để làm một "Form hỏi đáp tương tác (Interactive Q&A / Feedback Form)".
Yêu cầu chi tiết:
1. Giao diện: Hiện đại, bo góc đẹp mắt (card layout), responsive trên mọi thiết bị.
2. Các trường trong form gồm:
- Họ tên (bắt buộc nhập)
- Email (bắt buộc nhập, phải đúng định dạng email)
- Chủ đề (Dropdown gồm các lựa chọn: Hỗ trợ kỹ thuật, Góp ý sản phẩm, Báo lỗi, Khác)
- Nội dung câu hỏi (bắt buộc nhập, textarea)
3. Tính năng & Logic:
- Kiểm tra validation phía client: Báo lỗi bằng viền đỏ và text cảnh báo ngay dưới input nếu để trống hoặc sai định dạng email khi bấm nút gửi.
- Khi submit thành công: Hiển thị một hiệu ứng thông báo dạng Toast (góc trên cùng bên phải, tự mất sau 3 giây).
- Lưu trữ dữ liệu: Đẩy câu hỏi vừa gửi vào một danh sách "Các câu hỏi đã gửi" hiển thị ngay bên dưới form. Sử dụng localStorage để dữ liệu không bị mất khi F5 lại trang.
- Có nút "Xóa toàn bộ lịch sử" để dọn dẹp localStorage nếu cần.
4. Trình bày: Code sạch sẽ, có chú thích (comments) rõ ràng các hàm xử lý sự kiện JavaScript.Lưu trữ: Lưu các câu hỏi đã gửi vào localStorage, hiển thị bên dưới form kèm thời gian. Có nút xóa từng câu hỏi và nút xóa toàn bộ lịch sử.

Prompt này có tác dụng vì nó mô tả cả hành vi (khi nào báo lỗi, khi nào tự ẩn thông báo), nên Claude không phải đoán.
Vòng 2: Chạy thử và kiểm tra

Trước khi nhờ sửa, hãy tự thử các tình huống sau:
- Bấm gửi khi để trống toàn bộ form.
- Nhập email thiếu dấu @.
- Nhập nội dung gần chạm giới hạn 500 ký tự.
- Gửi thành công rồi tải lại trang để xem lịch sử còn không.

Vòng 3: Tinh chỉnh

Sau khi thử, bạn gửi tiếp một yêu cầu bổ sung:
Giao diện ổn rồi. Hãy chỉnh thêm:
Mọi nội dung người dùng nhập phải được escape trước khi chèn vào danh sách để tránh lỗi XSS.
Khi chưa có câu hỏi nào, hiển thị trạng thái trống thân thiện kèm dòng "Chưa có câu hỏi nào".
Bọc thao tác đọc và ghi localStorage trong try/catch để trang không hỏng khi trình duyệt chặn lưu trữ.
Cách nhắn này theo tinh thần vibe coding: Mỗi lượt chỉ sửa vài điểm nhỏ nên dễ kiểm soát hơn khi tinh chỉnh bằng một prompt dài.
Prompt tinh chỉnh form gốc
Sau khi form gốc chạy ổn, bạn có thể gửi tiếp các prompt dưới đây vào cùng cuộc trò chuyện để Claude chỉnh sửa trên file hiện có. Mỗi lượt chỉ nên gửi một prompt (hoặc một nhóm nhỏ), thử xong rồi mới gửi tiếp để dễ phát hiện lỗi.
Lưu ý: Thay nội dung trong ngoặc vuông [ ] bằng thông tin của bạn trước khi gửi.
1. Chống XSS và xử lý lưu trữ an toàn
Hãy rà soát lại code hiện tại và chỉnh các điểm sau:
Mọi nội dung người dùng nhập (họ tên, email, chủ đề, câu hỏi) phải được escape trước khi chèn vào danh sách, để tránh lỗi XSS.
Bọc mọi thao tác đọc và ghi localStorage trong try/catch để trang không bị lỗi khi trình duyệt chặn lưu trữ hoặc dữ liệu cũ bị hỏng.
Cuối cùng, liệt kê ngắn gọn những chỗ bạn đã sửa.
Đây là lớp bảo vệ cơ bản nhất nên làm trước tiên. Yêu cầu liệt kê thay đổi giúp bạn biết chính xác Claude đã động vào chỗ nào.
2. Trạng thái trống và bộ đếm ký tự
Hãy bổ sung:
Khi danh sách "Các câu hỏi đã gửi" chưa có dữ liệu, hiển thị trạng thái trống thân thiện với dòng chữ "Chưa có câu hỏi nào" và ẩn nút "Xóa toàn bộ lịch sử".
Giới hạn nội dung câu hỏi tối đa [500] ký tự, có bộ đếm ký tự hiển thị ngay dưới ô nhập.
Nếu bổ sung thêm chỉnh sửa ở trên đây, người dùng không còn thấy một khoảng trống khó hiểu, và bộ đếm giúp họ biết mình còn bao nhiêu chỗ để viết.
3. Kiểm tra dữ liệu chặt chẽ hơn
Nâng cấp phần validation:
- Họ tên: ít nhất 2 ký tự, loại bỏ khoảng trắng thừa ở đầu và cuối.
- Nội dung: ít nhất [10] ký tự.
- Khi người dùng đang sửa một ô bị báo lỗi, xóa thông báo lỗi ngay khi dữ liệu hợp lệ, không đợi bấm gửi lại.
- Khi bấm gửi mà có lỗi, tự động đưa con trỏ về ô lỗi đầu tiên.
Giữ nguyên kiểu báo lỗi hiện tại (viền đỏ và dòng chữ cảnh báo bên dưới).
Prompt này giúp việc đưa con trỏ về ô lỗi đầu tiên tiết kiệm thao tác, đặc biệt trên điện thoại.
4. Xóa từng câu hỏi và hỏi lại trước khi xóa
Thêm nút "Xóa" cho từng câu hỏi trong danh sách. Khi bấm, hiện hộp xác nhận trước khi xóa, sau đó cập nhật localStorage và giao diện.
Đồng thời hiện thông báo toast "Đã xóa câu hỏi" sau khi xóa xong.
Người dùng có thể dọn từng mục mà không cần xóa toàn bộ lịch sử.
5. Tìm kiếm và lọc theo chủ đề
Thêm phía trên danh sách câu hỏi:
Một ô tìm kiếm lọc theo họ tên hoặc nội dung ngay khi gõ.
Một dropdown lọc theo chủ đề, mặc định là "Tất cả chủ đề".
Nếu không có kết quả phù hợp, hiển thị dòng "Không tìm thấy câu hỏi phù hợp". Không làm mất dữ liệu gốc trong localStorage.
Câu “không làm mất dữ liệu gốc” nhắc Claude chỉ lọc phần hiển thị chứ không xóa dữ liệu thật.
6. Chế độ tối (Dark mode)
Thêm nút chuyển giao diện sáng/tối ở góc trên bên phải. Mặc định theo cài đặt hệ thống của người dùng và ghi nhớ lựa chọn bằng localStorage.
Đảm bảo màu chữ, viền, thông báo lỗi và toast vẫn đọc rõ ở cả hai chế độ.
Prompt trên yêu cầu giữ độ tương phản giúp tránh tình trạng chữ chìm vào nền khi đổi chế độ.
7. Xuất lịch sử ra file
Thêm nút "Xuất dữ liệu" bên cạnh nút xóa lịch sử, cho phép tải toàn bộ các câu hỏi đã gửi về máy dưới dạng file CSV (có hỗ trợ tiếng Việt có dấu, mở được bằng Excel).
Ẩn nút này khi danh sách đang trống.
Prompt này giúp nhắc rõ tiếng Việt có dấu để file mở trong Excel không bị lỗi phông chữ.
8. Thân thiện hơn với bàn phím và người dùng khuyết tật
Cải thiện khả năng tiếp cận:
- Mỗi ô nhập có nhãn gắn đúng với ô (thuộc tính for và id).
- Thông báo lỗi và toast có thuộc tính aria phù hợp để trình đọc màn hình đọc được.
- Có thể thao tác toàn bộ form chỉ bằng bàn phím, và vòng sáng khi focus phải rõ ràng.
Hiệu quả: Form dùng được cho nhiều người hơn và nhìn chuyên nghiệp hơn.
9. Chống gửi trùng và gửi quá nhanh
Thêm một lớp chống spam đơn giản phía client:
Sau khi gửi thành công, khóa nút gửi trong 3 giây.
Thêm một ô ẩn (honeypot): nếu ô này có dữ liệu thì bỏ qua lần gửi.
Giải thích ngắn gọn cách hoạt động và nói rõ giới hạn của cách này.
Hiệu quả: Yêu cầu Claude nêu giới hạn giúp bạn không hiểu lầm rằng đây là biện pháp chống spam hoàn chỉnh.
10. Dọn mã và kiểm tra lần cuối
Hãy rà soát toàn bộ file một lần cuối:
Tìm lỗi tiềm ẩn, đoạn mã thừa hoặc lặp lại và gọn lại nếu cần, nhưng giữ nguyên toàn bộ chức năng.
Chú thích tiếng Việt rõ ràng cho từng hàm.
Liệt kê các tình huống tôi nên tự kiểm tra bằng tay trước khi dùng thật.
Không thêm tính năng mới.
Hiệu quả: Câu “không thêm tính năng mới” giữ Claude trong phạm vi dọn dẹp, tránh việc nó tự ý thay đổi những gì bạn đã duyệt.
Mẹo dùng bộ prompt hiệu quả
- Gửi từng prompt một: Sau mỗi lượt, hãy thử form trong khung xem trước rồi mới tiếp tục.
- Nêu rõ “giữ nguyên”: Thêm câu như “giữ nguyên giao diện hiện tại” để Claude không làm lại cả trang.
- Báo lỗi như người dùng: Nếu có lỗi, hãy mô tả bạn đã thao tác gì và thấy kết quả ra sao, rồi nhờ Claude tìm nguyên nhân.
- Lưu lại phiên bản tốt: Trước mỗi lần chỉnh lớn, hãy xuất file HTML để có bản dự phòng.
- Không dán dữ liệu thật vào chat: Với các prompt liên quan đến email hay khóa dịch vụ, hãy dùng chuỗi giữ chỗ như YOUR_ACCESS_KEY_HERE .
Mã nguồn hoàn chỉnh (sao chép và chạy)
Tạo một file tên index.html , dán toàn bộ đoạn mã dưới đây vào rồi mở bằng trình duyệt:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Form hỏi đáp tương tác</title>
<script src="https://cdn.tailwindcss.com"></script>
<style type="text/tailwindcss">
.field { @apply w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm outline-none transition focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200; }
.field.invalid { @apply border-rose-500 ring-2 ring-rose-100; }
</style>
</head>
<body class="min-h-screen bg-slate-100 px-4 py-10 text-slate-800">
<div id="toast" role="status" aria-live="polite"
class="pointer-events-none fixed right-4 top-4 z-50 translate-x-[120%] rounded-lg bg-emerald-600 px-4 py-3 text-sm font-medium text-white shadow-lg transition-transform duration-300"></div>
<main class="mx-auto max-w-xl space-y-6">
<header class="text-center">
<h1 class="text-2xl font-bold sm:text-3xl">Gửi câu hỏi cho chúng tôi</h1>
<p class="mt-2 text-sm text-slate-500">Câu hỏi của bạn được lưu ngay trên trình duyệt này.</p>
</header>
<section class="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200">
<form id="qa-form" class="space-y-5" novalidate>
<div>
<label for="name" class="mb-1 block text-sm font-medium">Họ và tên <span class="text-rose-500">*</span></label>
<input id="name" type="text" autocomplete="name" placeholder="Nguyễn Văn A" class="field">
<p id="name-error" class="mt-1 hidden text-xs text-rose-600"></p>
</div>
<div>
<label for="email" class="mb-1 block text-sm font-medium">Email <span class="text-rose-500">*</span></label>
<input id="email" type="email" autocomplete="email" placeholder="ban@example.com" class="field">
<p id="email-error" class="mt-1 hidden text-xs text-rose-600"></p>
</div>
<div>
<label for="topic" class="mb-1 block text-sm font-medium">Chủ đề</label>
<select id="topic" class="field">
<option>Hỗ trợ kỹ thuật</option>
<option>Góp ý</option>
<option>Báo lỗi</option>
<option>Khác</option>
</select>
</div>
<div>
<div class="mb-1 flex items-center justify-between">
<label for="message" class="text-sm font-medium">Nội dung <span class="text-rose-500">*</span></label>
<span id="counter" class="text-xs text-slate-400">0/500</span>
</div>
<textarea id="message" rows="4" maxlength="500" placeholder="Bạn muốn hỏi điều gì?" class="field"></textarea>
<p id="message-error" class="mt-1 hidden text-xs text-rose-600"></p>
</div>
<button type="submit" class="w-full rounded-lg bg-indigo-600 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
Gửi câu hỏi
</button>
</form>
</section>
<section class="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200">
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-bold">Câu hỏi đã gửi</h2>
<button id="clear-all" class="hidden text-xs font-medium text-rose-600 hover:underline">Xóa lịch sử</button>
</div>
<div id="list" class="space-y-3"></div>
</section>
</main>
<script>
const STORAGE_KEY = 'qa_history_v1';
const $ = (id) => document.getElementById(id);
const form = $('qa-form');
const fields = { name: $('name'), email: $('email'), message: $('message') };
const topic = $('topic');
const list = $('list');
const clearAll = $('clear-all');
const toast = $('toast');
let toastTimer;
const rules = {
name: (v) => (v.length >= 2 ? '' : 'Vui lòng nhập họ tên (ít nhất 2 ký tự).'),
email: (v) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? '' : 'Email chưa đúng định dạng.'),
message: (v) => (v.length >= 10 ? '' : 'Nội dung cần ít nhất 10 ký tự.')
};
function loadItems() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; }
catch { return []; }
}
function saveItems() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); }
catch { /* trình duyệt chặn lưu trữ: vẫn dùng được trong phiên hiện tại */ }
}
let items = loadItems();
function escapeHtml(text) {
return String(text)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function validateField(key) {
const msg = rules[key](fields[key].value.trim());
const err = $(key + '-error');
fields[key].classList.toggle('invalid', Boolean(msg));
err.textContent = msg;
err.classList.toggle('hidden', !msg);
return !msg;
}
function showToast(text) {
toast.textContent = text;
toast.classList.remove('translate-x-[120%]');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.add('translate-x-[120%]'), 3000);
}
function formatTime(iso) {
return new Date(iso).toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' });
}
function render() {
clearAll.classList.toggle('hidden', items.length === 0);
if (items.length === 0) {
list.innerHTML = '<p class="py-6 text-center text-sm text-slate-400">Chưa có câu hỏi nào.</p>';
return;
}
list.innerHTML = items.map((q) => `
<article class="rounded-xl border border-slate-100 bg-slate-50 p-4">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="truncate text-sm font-semibold">${escapeHtml(q.name)}</p>
<p class="truncate text-xs text-slate-400">${escapeHtml(q.email)}</p>
</div>
<span class="shrink-0 rounded-full bg-indigo-50 px-2.5 py-1 text-xs font-medium text-indigo-600">${escapeHtml(q.topic)}</span>
</div>
<p class="mt-2 whitespace-pre-line break-words text-sm text-slate-600">${escapeHtml(q.message)}</p>
<div class="mt-3 flex items-center justify-between text-xs text-slate-400">
<span>${formatTime(q.time)}</span>
<button data-id="${q.id}" class="font-medium text-rose-500 hover:underline">Xóa</button>
</div>
</article>`).join('');
}
Object.keys(fields).forEach((key) => {
fields[key].addEventListener('input', () => {
if (fields[key].classList.contains('invalid')) validateField(key);
if (key === 'message') $('counter').textContent = fields.message.value.length + '/500';
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const allValid = Object.keys(fields).map(validateField).every(Boolean);
if (!allValid) return;
items.unshift({
id: Date.now(),
name: fields.name.value.trim(),
email: fields.email.value.trim(),
topic: topic.value,
message: fields.message.value.trim(),
time: new Date().toISOString()
});
saveItems();
form.reset();
$('counter').textContent = '0/500';
render();
showToast('Đã gửi câu hỏi thành công!');
});
list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-id]');
if (!btn) return;
items = items.filter((q) => String(q.id) !== btn.dataset.id);
saveItems();
render();
});
clearAll.addEventListener('click', () => {
if (!confirm('Xóa toàn bộ lịch sử câu hỏi?')) return;
items = [];
saveItems();
render();
showToast('Đã xóa lịch sử.');
});
render();
</script>
</body>
</html>Cách xuất file và chạy trên máy

- Bấm Export ở góc trên khung xem trước và chọn HTML để tải file về.
- Đổi tên file thành html rồi nhấp đúp để mở bằng trình duyệt.
- Máy cần có Internet ở lần mở đầu vì Tailwind được nạp qua CDN.
Về lịch sử câu hỏi: Tính năng lưu trữ bền vững của artifacts chỉ có ở các gói trả phí. Nếu khung xem trước trong Claude không giữ được lịch sử sau khi tải lại trang, đó không phải lỗi mã. Bạn hãy mở file HTML đã xuất trên máy để kiểm tra lưu trữ trình duyệt.
Gặp lỗi thì xử lý thế nào?
- Hãy nhắn lại cho Claude đúng như người dùng báo lỗi. Ví dụ:
- Khi tôi bấm "Xóa" ở một câu hỏi thì danh sách không cập nhật. Hãy tìm nguyên nhân, sửa và giải thích ngắn gọn bạn đã thay đổi gì.
- Yêu cầu Claude giải thích thay đổi giúp bạn hiểu mã, và biết chỗ nào cần kiểm tra lại. Cách này cũng giúp bạn học dần mà không phải đọc hết từng dòng.
Lưu ý bảo mật khi vibe coding
Quy tắc vàng: Không dán API key, mật khẩu, chuỗi kết nối cơ sở dữ liệu hay dữ liệu khách hàng thật vào khung chat. Nếu cần minh họa, hãy dùng chuỗi giữ chỗ như YOUR_API_KEY_HERE.
- Đây không phải hệ thống gửi thư hay lưu máy chủ: Dữ liệu chỉ nằm trong localStorage của trình duyệt đang dùng. Muốn nhận câu hỏi từ người khác, bạn cần thêm phần máy chủ hoặc dịch vụ biểu mẫu, và phải xem xét kỹ vấn đề bảo mật.
- Không lưu thông tin nhạy cảm trong localStorage: Bất kỳ ai dùng chung máy đều có thể đọc được dữ liệu này.
- Luôn escape nội dung người dùng nhập: Đây là lý do mã mẫu có hàm escapeHtml , để chặn việc chèn thẻ HTML hoặc script độc hại vào danh sách.
- Đọc lại mã trước khi triển khai thật: AI viết nhanh nhưng vẫn có thể sót lỗi.
Chỉ với hai prompt, bạn đã có một form hỏi đáp tương tác đủ tính năng bao gồm kiểm tra dữ liệu, thông báo toast, lịch sử câu hỏi và chống chèn mã độc cơ bản. Điều quan trọng nhất của vibe coding là mô tả hành vi thật rõ, thử kỹ từng tình huống và không đưa thông tin bí mật vào khung chat. Khi quen tay, bạn có thể dùng chính quy trình này cho khảo sát, đăng ký sự kiện hay bảng theo dõi công việc.
Bạn nên đọc
Theo Nghị định 147/2024/ND-CP, bạn cần xác thực tài khoản trước khi sử dụng tính năng này. Chúng tôi sẽ gửi mã xác thực qua SMS hoặc Zalo tới số điện thoại mà bạn nhập dưới đây:
Hướng dẫn AI
AI Tools
Học IT
AI
Hàm Excel