Lexiverse
Lexiverse - AI contextual reading engine for English learners. Your next article is your next review.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Monkeytype, but Singaporean.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A Singlish typing test in the style of monkeytype. Pick a mode and a duration, type the prompt, get your wpm and accuracy. Dark, minimal, no accounts — just wpm lah. An optional global leaderboard (a small FastAPI + SQLite backend) lets you post a nickname and score; the core test still runs fully client-side without it.
Live modes:
lah, shiok, kopi, paynow, bto, hdb, …)ang mo kio — north south line)gd nite swt drms — msn status)The results screen shows a pace-over-time graph (running wpm per second, with an × on every second that contained a wrong keystroke) and the singlish you typed — each word from the run, with a hover tooltip giving its meaning and a click-through to its dictionary entry (Wiktionary, or Wikipedia for the institutions).
output: "export") — no server, no API routes (the optional
leaderboard is a separate backend in leaderboard-api/)lib/ (wpm/accuracy, pace, challenges, corpora, records)mainnpm install
npm run dev # dev server at http://localhost:3000
Other scripts:
npm run build # production build -> static export in ./out
npm test # unit tests for lib/ (wpm math, pace, challenges, corpora, records)
To preview the exact static output Vercel serves:
npm run build
npx serve out # or: python3 -m http.server -d out 8000
The leaderboard talks to a separate backend. Point the frontend at it with an env var (defaults to the production API when unset):
# .env.local
NEXT_PUBLIC_LEADERBOARD_API=http://localhost:8000
Run the backend locally from leaderboard-api/:
pip install -r leaderboard-api/requirements.txt
KOPITYPE_DB=./leaderboard.db uvicorn main:app --port 8000 # from leaderboard-api/
KOPITYPE_DB sets the SQLite path (default /data/leaderboard.db, meant for a
mounted volume in the container). The board shows each player's best run per mode +
duration, so multiple attempts don't crowd it out.
The API accepts known corpus IDs and 15/30/60-second runs, rejects blank names,
and caps raw WPM at 1,000. SQLite enforces 10 submissions per client per minute
and 200 total per minute across workers sharing the database. Expired counters
are removed during submissions. CORS defaults to kopitype.com, www.kopitype.com
and localhost:3000; override CORS_ORIGINS with exact comma-separated origins.
IP quotas use the ASGI peer address, never arbitrary forwarded headers. Configure Uvicorn's trusted proxy IPs only for your actual reverse proxy; otherwise users behind a proxy share its quota. A shared NAT also shares a quota. CORS and quotas reduce browser misuse and spam; scores remain client-reported, are not verified proof of a typing run, and nicknames are not authenticated identities. Strong anti-cheat requires server-issued challenges and run validation as a follow-up.
When the API runs in Docker behind a reverse proxy on the same host (nginx →
127.0.0.1:8124), every request reaches the container from the Docker bridge
gateway (typically 172.17.0.1), so without extra config all players share one
10-per-minute quota. Tell Uvicorn to trust that hop's X-Forwarded-For with
its FORWARDED_ALLOW_IPS env var, and publish the port on loopback only so
nobody can reach the container directly and forge the header:
docker run -d --name kopitype-leaderboard \
-p 127.0.0.1:8124:8124 \
-e FORWARDED_ALLOW_IPS=172.17.0.1 \
-v "$PWD/data:/data" kopitype-leaderboard
Backend regressions: install leaderboard-api/requirements.txt plus httpx, then
run cd leaderboard-api && python -m unittest test_api.
The whole test lives on one page (app/page.tsx → components/TypingTest.tsx), a small
state machine: idle → running → finished.
<input> captures typing. Its value is the current word's buffer, so it
works with both physical keyboards and mobile soft keyboards (we read value changes,
not keycodes). A space commits the word. Backspace erases within the current word,
and — monkeytype-style — crosses back into the previous word only if that word was
committed with an error; correctly typed words are locked.components/WordStream.tsx renders per-character state: pending / correct /
incorrect / extra, with a caret, and scrolls as you descend the lines.lib/wpm.ts holds the pure, unit-tested math: wpm = (correct chars / 5) / minutes,
plus raw wpm and accuracy.components/Stats.tsx shows the result, your
previous score, and your personal best (per mode + duration, in localStorage —
still no accounts). Tab (while typing or on the results screen) or the restart
button starts a fresh test; Shift+Tab and Escape are never hijacked, so
keyboard navigation always works.lib/sound.ts (WebAudio, no
audio assets) behind a "sound" toggle that's remembered in localStorage.lib/pace.ts, pure + tested); the results screen renders them as an SVG line
chart (components/PaceChart.tsx — hover crosshair, keyboard arrows, no chart
library).data/glossary.json maps terms to short meanings (plus an optional link);
components/GlossaryWords.tsx turns the words you attempted into hoverable,
clickable chips.Click custom in the mode bar, type a phrase (up to 300 chars), share the link.
The phrase travels base64url-encoded in the URL fragment (#c=…) — no backend, no
storage, nothing expires. Opening a challenge link loads a fixed-phrase test that
ends when the phrase does; the timer counts up and the wpm is computed from elapsed
time. Challenges don't touch personal bests. lib/challenge.ts owns the
encode/decode (validated + normalized on the way in — never trust a URL).
Click submit in the mode bar to send in words, quotes, or meanings. With no
backend, the review queue is the repo itself: the form opens a prefilled GitHub
issue (labelled corpus-submission) containing a ready-to-paste JSON snippet.
Nothing goes live until a maintainer vets it and commits it into data/ —
crowdsourced input, curated output.
This is deliberately a two-step change (say you want a "kopi" mode):
Drop a JSON file in data/:
["kopi", "teh", "milo", "kosong"]
{ "text": string, "source": string }:
[{ "text": "kopi c kosong gau one cup dabao", "source": "kopi order" }]
Register it in the one MODES array in lib/corpus.ts:
import kopi from "../data/kopi.json";
export const MODES: ModeDef[] = [
// ...existing modes...
{ id: "kopi", label: "kopi", type: "words", words: kopi as string[] },
];
That's it — it shows up in the mode bar automatically. Add gated: true to make a mode ask
for a one-tap confirm before it switches in (that's how vulgar.json is wired). Word-mode tokens must
be single words (no spaces); put any multi-word terms in a quotes corpus instead.
| File | Entries | Shape |
|---|---|---|
data/words.json | ~245 | single lowercase tokens |
data/sg.json | ~97 | single lowercase tokens (everyday SG) |
data/quotes.json | 72 | { text, source } phrases |
data/mrt.json | ~122 | { text, source } station → line |
data/xmm.json | 33 | { text, source } vowel-less texts |
data/vulgar.json | 44 | single tokens, gated |
data/glossary.json | ~217 | term: { meaning, link? } for the results-screen glossary |
The corpora are hand-written and meant to be human-readable (one entry per line) so they're easy to vet and extend — which is also how crowdsourced submissions land (see above).
Accounts, themes, multiplayer, punctuation/numbers toggles, i18n. (Personal bests, sound, the pace graph, custom challenges, and the glossary all landed post-v1 — local-only or in-URL. The global leaderboard came later and is the one piece with a backend; everything else still runs without one.)