AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Web Component vui vẻ cho upload, in, đăng nhập, OTP, captcha — mỗi thao tác là một trò chơi nhỏ
Translation pending
Developer toolsJavaScript · HTMLGitHub @bangluutruOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Thư viện Web Component vui vẻ cho các thao tác thường gặp — upload file, in, đăng nhập, nhập OTP, captcha… Mỗi thành phần làm việc thật, chỉ có lớp "da" là trò chơi. Dùng được ở mọi web (HTML thuần, React, Vue…), không phụ thuộc thư viện ngoài. Gói trỏ thẳng vào mã nguồn ES module (src/index.js), không cần build, chạy được với Vite/webpack/esbuild.Read the full README ↗
Thư viện Web Component vui vẻ cho các thao tác thường gặp — upload file, in, đăng nhập, nhập OTP, captcha… Mỗi thành phần làm việc thật, chỉ có lớp "da" là trò chơi. Dùng được ở mọi web (HTML thuần, React, Vue…), không phụ thuộc thư viện ngoài.
| Thành phần | Trạng thái |
|---|---|
<vv-basket-upload> — ném file vào rổ để upload | ✅ v0.1 |
<vv-slingshot-otp> — bắn từng số OTP vào ô bằng ná cao su | ✅ v0.1 |
<vv-halloween-login> — đăng nhập ngày ↔ đêm Halloween, đèn pin soi mật khẩu | ✅ v0.1 |
<vv-print> — máy in tham ăn: kéo giấy vào miệng máy in để in | ✅ v0.1 |
<vv-basket-shot> — ném một quả bóng vào rổ để xác nhận một việc (vd. trước khi in) | ✅ v0.2 |
<vv-fun-captcha> — "Tôi không phải robot" + trò kéo-thả, bên trong là bằng chứng công việc kiểm tra ở máy chủ | ✅ v0.1 |
Demo trực tuyến: https://ui-vuivui.bangluutru.workers.dev
npm install
npm run dev # trang demo: http://localhost:5180
npm test # test phần lõi
npm run build # thư viện → dist/
npm run deploy # build trang demo + đưa lên Cloudflare Workers (cần `wrangler login` và secret POW_SECRET)
npm i ../ui-vuivui # từ thư mục bên cạnh
# hoặc, khi đã có repo GitHub:
npm i github:bangluutru/ui-vuivui
Gói trỏ thẳng vào mã nguồn ES module (src/index.js), không cần build, chạy được với Vite/webpack/esbuild.
<vv-basket-upload>Kéo file vào thẻ → hiện đường chấm từ con trỏ tới rổ → thả tay, file thành quả bóng bay lên vành. Trong lúc upload, bóng lăn quanh vành (lắng dần theo tiến trình). Thành công → lọt lưới, bảng điểm +1. Thất bại → bật vành văng ra ("Trượt rổ!") và có nút Ném lại. Nhiều file → xếp hàng, ném lần lượt.
<script type="module">
import 'ui-vuivui'; // đăng ký các thẻ vv-*
import { xhrUploader } from 'ui-vuivui';
const up = document.querySelector('vv-basket-upload');
up.uploader = xhrUploader('/api/upload', { fieldName: 'file' });
up.addEventListener('vv-uploaded', (e) => console.log('Xong:', e.detail.result));
</script>
<vv-basket-upload multiple sound accept="image/*,.pdf" max-size="10MB"></vv-basket-upload>
Không dùng bundler: chạy npm run build, chép dist/ui-vuivui.iife.js rồi <script src="ui-vuivui.iife.js"></script>; các hàm nằm trong window.UiVuiVui.
| Thuộc tính | Ý nghĩa |
|---|---|
multiple | Cho chọn/thả nhiều file (không có → chỉ lấy file đầu) |
accept | Như <input type=file accept>: image/*, .pdf, application/pdf… |
max-size | Giới hạn mỗi file: 10MB, 512KB, hoặc số byte |
sound | Bật âm thanh (người dùng bật/tắt được bằng nút loa) |
lang | vi (mặc định) hoặc en |
disabled | Khóa thẻ |
uploader | Bắt buộc. (file, { onProgress, signal }) => Promise<kết quả>. Thiếu → mọi cú ném đều "trượt" với thông báo Chưa nối với máy chủ (thư viện không bao giờ giả vờ thành công). |
labels | Ghi đè chữ, vd. { cta: 'Ném hồ sơ', titleB: 'hồ sơ' } — xem src/basket-upload/labels.js |
files | Danh sách item hiện tại ({ id, name, size, status, progress, result, error }) |
addFiles(files) · removeFile(id) · retry(id) · clear() · openPicker() |
status: waiting → shooting → uploading → done | error. Xóa một file đang upload sẽ hủy request (signal).
| Sự kiện | detail |
|---|---|
vv-select | { items } — file vừa được nhận |
vv-uploaded | { item, result } |
vv-error | { item, error } |
vv-reject | { file, reason: 'type' | 'size' } |
vv-remove | { item } |
vv-drain | { items } — hàng chờ đã chạy hết |
xhrUploader(url, options)Dùng XMLHttpRequest vì chỉ XHR báo được tiến trình upload (fetch không làm được).
| Tùy chọn | Mặc định | |
|---|---|---|
method | POST | |
fieldName | file | tên trường trong multipart/form-data |
fields | {} | trường kèm theo |
headers | {} | object hoặc (file) => object (vd. token) |
withCredentials | false | gửi cookie khác tên miền |
body | multipart | raw = gửi thẳng nội dung file (URL ký sẵn của S3/R2) |
url cũng có thể là hàm (file) => string. Lỗi HTTP ném UploadHttpError (status, body).
Đặt biến CSS trên chính thẻ: --vv-accent, --vv-accent-2, --vv-ink, --vv-muted, --vv-paper, --vv-court, --vv-head, --vv-ok, --vv-bad, --vv-radius, --vv-font. Các phần ::part(card|header|dropzone|list|button|scoreboard|sound-button) cũng chỉnh được.
<vv-slingshot-otp>Giữ một số → số nạp vào túi ná · kéo để căng dây và nhắm (đường chấm xanh = sẽ trúng ô) · thả tay để bắn.
Trúng ô nào điền ô đó (bắn đè để sửa), trượt thì số quay về. Chạm nhẹ một số = tự bắn vào ô đang chờ.
Gõ phím, dán mã, Backspace và tự điền mã từ SMS (autocomplete="one-time-code") đều dùng được.
Đủ số → gọi verifier(code) một lần cho cả mã.
<script type="module">
import { jsonVerifier } from 'ui-vuivui';
const otp = document.querySelector('vv-slingshot-otp');
otp.verifier = jsonVerifier('/api/otp/verify'); // máy chủ nhận { code } → trả { ok, message? }
otp.addEventListener('vv-continue', () => (location.href = '/app'));
</script>
<vv-slingshot-otp length="6" name="Lan" sound></vv-slingshot-otp>
Bảo mật: thẻ không biết mã đúng và không báo đúng/sai từng chữ số (video gốc làm vậy — để lộ mã chỉ sau tối đa 60 lần thử). Giới hạn số lần thử, hết hạn mã… là việc của máy chủ.
| Thuộc tính HTML | |
|---|---|
length | số chữ số, 4–8 (mặc định 6) |
name | tên để chào ("Chào Lan,") |
sound · lang · disabled | như <vv-basket-upload> |
no-auto-submit | không tự gửi khi đủ số (người dùng bấm Xác nhận) |
autofocus | focus ô nhập khi hiện (bỏ qua trên máy cảm ứng để bàn phím ảo không che ná) |
| JS | |
|---|---|
verifier | Bắt buộc. (code: string) => Promise<boolean | {ok, message?}>. ok:false = mã sai (rung, xóa, nhập lại). Ném lỗi = lỗi mạng (giữ mã, nút Thử lại). Thiếu verifier → báo lỗi, không bao giờ "thành công". |
value · reset() · focus() · labels |
| Sự kiện | detail |
|---|---|
vv-complete | { code } — vừa đủ số, sắp kiểm tra |
vv-verified | { code, verdict } |
vv-rejected | { code, message } |
vv-error | { code, error } |
vv-continue | { code } — bấm nút trên màn hình thành công |
jsonVerifier(url, { field = 'code', headers, extra, credentials }): POST JSON; 2xx → { ok, message }; 400/401/422 kèm { ok:false } → mã sai; mã lỗi khác → ném VerifyHttpError.
<vv-halloween-login>Công tắc ☀/🌙 chuyển từ màn đăng nhập ban ngày sang đêm Halloween: khung bí ngô, đèn lồng, phù thủy bay, dơi, sói hú, nhện treo tơ. Ban đêm, nút con mắt thành đèn pin: luồng sáng xoay theo con trỏ (cảm ứng: chạm để nhắm) và mật khẩu chỉ đọc được ở chỗ đèn chiếu tới. Bí ngô bị chiếu thì sáng mặt; nhện sợ sáng nên leo lên.
<script type="module">
import { jsonAuthenticator } from 'ui-vuivui';
const login = document.querySelector('vv-halloween-login');
login.authenticator = jsonAuthenticator('/api/login'); // { email, password } → { ok, message? }
login.addEventListener('vv-login', () => (location.href = '/app'));
</script>
<vv-halloween-login theme="auto" forgot-href="/quen-mat-khau" signup-href="/dang-ky" sound></vv-halloween-login>
<form> thật với autocomplete="username" / "current-password" để trình quản lý mật khẩu tự điền / lưu được. CSS chèn vào <head> một lần, mọi lớp có tiền tố vvhl-.type="text" nhưng chữ bị che bằng mặt nạ hình nón, chỉ lộ trong luồng sáng. Tự tắt khi gửi form (trước khi gọi máy chủ), khi đổi ngày/đêm, khi chuyển tab hoặc chuyển focus ra ngoài.| Thuộc tính HTML | |
|---|---|
theme | day (mặc định) · night · auto (18h–6h là đêm) |
identifier | email (mặc định) hoặc username |
forgot-href · signup-href | link; bỏ trống → phát sự kiện vv-forgot / vv-signup |
sound · lang · disabled | như các thẻ khác |
| JS | |
|---|---|
authenticator | Bắt buộc. ({ identifier, password }) => Promise<boolean | {ok, message?}>. ok:false = sai thông tin (giữ email, xóa mật khẩu, rung). Ném lỗi = lỗi mạng (giữ nguyên, cho thử lại). Thiếu → báo lỗi, không bao giờ "thành công". |
theme · reset() · focus() · labels |
| Sự kiện | detail |
|---|---|
vv-login | { identifier, verdict } |
vv-login-failed | { identifier, message } |
vv-error | { identifier, error } |
vv-theme | { theme } |
vv-forgot · vv-signup | { identifier } · {} |
jsonAuthenticator(url, { identifierField = 'email', passwordField = 'password', headers, extra, credentials = 'same-origin' }): 2xx → { ok, message }; 400/401/403/422 kèm { ok:false } → sai thông tin; lỗi khác → ném AuthHttpError.
<vv-fun-captcha>Ô "Tôi là người, không phải robot" quen thuộc; bấm vào là hiện một trò kéo-thả nhỏ (cho mèo ăn cá · ném bóng vào rổ · đưa ong tới hoa). Cùng lúc, trình duyệt giải một đề bằng chứng công việc (proof-of-work) ở nền. Xong cả hai → ✓, token được đưa vào form như một ô nhập.
An ninh nằm ở máy chủ, không nằm ở trò chơi. Trò chơi chỉ để vui — chạm nhẹ hoặc nhấn Enter là qua (cố ý, để ai cũng dùng được). Cái làm khó bot là:
maxnumber lần (SHA-256) để tìm đáp án;Như vậy gửi hàng loạt sẽ tốn kém; nhưng một trình duyệt tự động quyết tâm vẫn vượt qua được trong chưa tới 1 giây. Muốn mạnh hơn: tăng độ khó, giới hạn tần suất theo IP, hoặc gắn ALTCHA / Cap / Cloudflare Turnstile qua solver.
Giao thức
vv-pow v1tự làm, không tương thích ALTCHA. Mô tả đầy đủ ở đầusrc/server/pow.js.
<form action="/binh-luan" method="post">
<textarea name="noi_dung" required></textarea>
<vv-fun-captcha name="captcha" required sound></vv-fun-captcha>
<button>Gửi</button>
</form>
<script type="module">
import { powSolver } from 'ui-vuivui';
document.querySelector('vv-fun-captcha').solver = powSolver('/api/pow/challenge');
</script>
Máy chủ (Cloudflare Worker, Node 20+, Deno, Bun — chỉ dùng Web Crypto):
import { createPowChallenge, verifyPow } from 'ui-vuivui/server';
// GET /api/pow/challenge
const ch = await createPowChallenge({ secret: env.POW_SECRET, maxnumber: 50000, ttlSeconds: 600 });
return Response.json({ ...ch, serverTime: Date.now() });
// Khi nhận form:
const r = await verifyPow(form.get('captcha'), { secret: env.POW_SECRET });
// r.ok → kiểm tra r.id CHƯA dùng (KV / DB), ghi nhớ tới r.expiresAt, rồi mới xử lý form
Xem worker/index.js cho ví dụ đầy đủ: token đã dùng được ghi vào một Durable Object (SQLite, có trong gói miễn phí của Cloudflare) để mọi bản Worker cùng hỏi một chỗ. Đừng nhớ trong biến bộ nhớ: Cloudflare chạy nhiều bản Worker song song, gửi lại token vào bản khác sẽ lọt (đã thử thật trên trang demo).
| Thuộc tính HTML | |
|---|---|
name · required | tham gia form như ô nhập (ElementInternals); required chặn gửi khi chưa xác minh |
game | cat · hoop · bee · bỏ trống = ngẫu nhiên |
prefetch | giải đề ngay khi trang tải (người dùng chơi xong là xác minh luôn) |
sound · lang · disabled | như các thẻ khác |
| JS | |
|---|---|
solver | Bắt buộc. ({ signal, onProgress }) => Promise<{ token, expiresIn?, receivedAt? } | string>. Thiếu → báo lỗi, không bao giờ "đã xác minh". |
value · verified · reset() | token chỉ dùng một lần — gọi reset() sau mỗi lần gửi form |
Sự kiện: vv-verified { token } · vv-expired · vv-error { error }. Hạn dùng tính theo giờ máy chủ (serverTime) nên đồng hồ máy người dùng lệch cũng không sao. form.reset() và khôi phục form (back/forward) đều xóa token cũ.
<vv-print>Kéo tờ giấy vào miệng máy in (hoặc chạm nhẹ / nhấn Enter / bấm In ngay) → máy in nhai giấy, rè rè, nhả trang in ra → hộp thoại in của trình duyệt mở. Tài liệu được chuẩn bị song song với hoạt cảnh (~1 giây).
<!-- một phần trang (giữ nguyên chữ người dùng đã gõ) -->
<vv-print target="#phieu" doc-title="Phiếu bài tập"></vv-print>
<!-- một tài liệu, cho thả / chọn file -->
<vv-print src="/de/toan-3.pdf" accept-files sound></vv-print>
<!-- cả trang -->
<vv-print></vv-print>
| Thuộc tính HTML | |
|---|---|
target | CSS selector của phần cần in — in thẳng trang đang mở, ẩn mọi thứ khác bằng @media print |
src | đường dẫn PDF / ảnh / văn bản / trang HTML tĩnh — tải về rồi in trong iframe ẩn. Đuôi .pdf mà nội dung là HTML (đường dẫn sai trên web kiểu app một trang) → báo lỗi, không in |
accept-files | cho thả / chọn file PDF, PNG, JPEG, WebP, GIF, SVG, văn bản (nhận diện theo byte đầu; không nhận HTML) |
doc-title · sound · lang · disabled |
| Sự kiện | detail |
|---|---|
vv-print-dialog | { source } — hộp thoại in vừa mở |
vv-print-closed | { source } — hộp thoại đã đóng (không biết người dùng bấm In hay Hủy) |
vv-print-sent | { source, result } — đã gửi qua printHandler |
vv-print-error | { source, error } |
An toàn: HTML/ảnh/văn bản in trong iframe sandbox không có allow-scripts — file tải về không chạy được mã với quyền của trang. Văn bản được thoát ký tự; ảnh (kể cả SVG) luôn qua <img>.
Giới hạn của trình duyệt: luôn có hộp thoại in (không web nào được in im lặng). In PDF từ iframe chạy tốt trên Chrome; Safari / Firefox / iPhone có thể khác — với PDF, thẻ luôn hiện thêm link Mở trong tab mới.
tools/print-agent.mjs (Node, macOS/Linux) nhận file từ trang web và gọi lp:
node tools/print-agent.mjs --origin https://ui-vuivui.bangluutru.workers.dev
import { agentPrintHandler } from 'ui-vuivui';
el.printHandler = agentPrintHandler({ token: 'mã-bí-mật-nó-in-ra' });
Chỉ nghe ở 127.0.0.1; mã bí mật mới mỗi lần chạy (gửi trong header); chỉ nhận từ --origin đã liệt kê; chỉ in file PDF / PNG / JPEG / GIF / văn bản (kiểm tra byte đầu, tối đa 20 MB); gọi lp <file> không qua shell, không nhận tuỳ chọn từ trang web. In phiếu / cả trang vẫn dùng hộp thoại (trình duyệt không tạo được PDF từ trang). Chrome có thể hỏi quyền "truy cập mạng nội bộ"; Safari có thể chặn trang https gọi http://127.0.0.1.
<vv-basket-shot>Ném một quả bóng vào rổ để xác nhận một việc — ví dụ trước khi mở hộp thoại in. Giữ bóng và di chuyển tự do trong sân; cú ném luôn hướng lên, về phía rổ và chỉ phụ thuộc chỗ bóng đứng: đứng ở khoảng cách hợp lý → vào (đường chấm xanh), gần quá → quá đà, xa quá → hụt, ngay dưới rổ → đập đáy vành; chữ hướng dẫn nói nên lùi hay tiến; thả tay để ném; bóng phải rơi xuống qua vành mới tính — có thể chạm vành hoặc trượt. Chạm nhẹ quả bóng / nhấn Enter → ném chuẩn, luôn vào.
<vv-basket-shot lang="ja" sound></vv-basket-shot>
<script type="module">
import 'ui-vuivui/basket-shot';
document.querySelector('vv-basket-shot').addEventListener('vv-score', () => window.print());
</script>
Sự kiện: vv-score { method: 'throw'|'tap'|'keyboard', shots } · vv-miss { result: 'rim'|'block'|'air'|'short', shots }. Phương thức reset(). Ngôn ngữ: vi · ja · en.
src/core/) làm việc và phát sự kiện; "da" chỉ là hoạt cảnh. Không bao giờ giả vờ thành công.sound.MIT — xem LICENSE.