A small Next.js app that helps you read Japanese text by adding furigana (readings) on top of kanji using OpenAI. It supports:
- Ruby-based furigana rendering (HTML
<ruby><rt>tags) - Two display modes: always show vs. hover to see furigana
- Local history sidebar with editable titles (saved in
localStorage)
- Node.js 20+ recommended
- pnpm (preferred) or npm/yarn
- An OpenAI API key
From the project root:
pnpm install
# or
npm installCreate .env.local in the project root:
OPENAI_API_KEY=sk-...
# Optional: choose which OpenAI model to use for furigana generation
# Recommended:
# - gpt-5.2 or gpt-5.2-pro (best accuracy)
# - gpt-5-mini (cheaper GPT‑5)
# - gpt-4o (default, good balance)
# Budget:
# - gpt-4o-mini (cheapest, may skip some kanji)
OPENAI_MODEL=gpt-4oNote: Do not commit
.env.localto git.
pnpm dev
# or
npm run devOpen http://localhost:3000 in your browser.
The main layout is split into:
- Left sidebar – history of previous requests
- Right panel – either the input form or the furigana display
When you first open the app (or click New in the sidebar header):
- The right panel shows an input form:
- A large textarea labeled (visually hidden) “Japanese paragraph”
- A Get furigana button
- Type or paste Japanese text (sentences, paragraphs) and click Get furigana.
Behind the scenes:
- The app calls
POST /api/furiganawith your text. - The API uses OpenAI to generate HTML like:
<ruby>今日<rt>きょう</rt></ruby>は<ruby>天気<rt>てんき</rt></ruby>がいい。
- The HTML is sanitized and stored in local history.
After a successful request:
- A new history item appears in the sidebar.
- The right panel switches to show the furigana display for that item.
The sidebar:
- Stays fixed to the viewport height and does not scroll with the page.
- Shows a header:
- Title: History
- A New button to start a new input session.
- Below the header:
- A scrollable list of history entries (stored in
localStorage).
- A scrollable list of history entries (stored in
History entry behavior:
- Each entry shows:
- Either a custom name (if you set one), or
- A truncated version of the original input text.
- You can:
- Click an entry to show its furigana on the right.
- Double‑click the text (or click the pencil icon) to rename it.
- Use the trash icon to delete the entry.
When a history item is selected (or after submission), the right panel shows:
- A mode selector (tabs) at the top:
- Always show furigana – readings (rt) are always visible above kanji.
- Hover to see furigana – readings are hidden until you hover over kanji.
- Below the tabs:
- The Japanese paragraph rendered using
<ruby>/<rt>tags generated by OpenAI.
- The Japanese paragraph rendered using
Implementation details:
- HTML from the API is sanitized to keep only
<ruby>and<rt>and escape everything else. - CSS controls the two modes:
- Always show:
rtis visible by default. - Hover:
rtis hidden until you hover over the correspondingruby.
- Always show:
At any time you can:
- Click a history item to switch to its reading.
- Click New in the sidebar to return to the input form and create a new history entry.
The current app is an MVP: history is stored only in localStorage and there is no authentication. The implementation is intentionally migration‑friendly so we can move to a production setup later.
Based on the plan in .cursor/plans/ai_japanese_reading_assistant_d4573b6c.plan.md, future work is organized into three main areas:
Goal: improve perceived performance and UX by streaming furigana HTML as it is generated, instead of waiting for the full response.
Planned steps:
- Update
POST /api/furigana(or add a new route) to use streaming responses from the OpenAI Chat Completions API. - On the client, replace the single
fetch/res.json()call with a stream reader (e.g.ReadableStream+TextDecoder), progressively building the HTML string. - Render a streaming furigana display:
- Show partial
<ruby><rt>output as it arrives. - Keep the same sanitizer (
sanitize-furigana-html) in the loop so only safe<ruby>/<rt>tags are rendered.
- Show partial
- Optionally show a small “Streaming…” indicator while the response is not complete.
This keeps the UI responsive for long paragraphs and gives immediate feedback while the model is still generating.
Goal: move history from localStorage to a server‑side store so users can keep their reading history across devices.
Planned steps:
- Add Upstash Redis via Vercel Marketplace.
- Expose history via API routes:
GET /api/history– fetchHistoryItem[]for the current user.POST /api/history(or integrate intoPOST /api/furigana) – append a new history item.- Optional
DELETE /api/history– clear or delete specific history items.
- Use a user id key (e.g.
user:{userId}) in Redis; value is a JSON array of history items. - Swap the history client (
lib/history-client.ts) to call these APIs instead of using localStorage:getHistory()→fetch('/api/history')appendHistoryItem()→ POST to/api/historydeleteHistoryItem()→ DELETE to/api/history
The UI (useHistory hook + sidebar) does not need major changes; it keeps calling the history client.
Goal: restrict persistent history to logged‑in users, while keeping the reader usable without an account.
Planned steps:
- Add authentication (e.g. Google OAuth via NextAuth/Auth.js or similar).
- When logged in:
- Show the history sidebar.
- Use
session.user.idas theuserIdfor Redis keys. - Read/write history via the new history API routes.
- When logged out:
- Keep the input form + furigana display fully usable.
- Hide or disable the history sidebar (e.g. show “Sign in to save history”).
- Do not persist history server‑side.
Behavior:
- On logout, users can still:
- Paste Japanese text.
- Click Get furigana.
- See the furigana display for the current input.
- But they won’t:
- See or save persistent history.
Migration path (high‑level):
- Introduce Upstash Redis +
/api/history, while still using a placeholder user id (e.g. anonymous cookie) – history client switches to server APIs. - Add Google login, and start using the real authenticated
userIdfor Redis keys. - Enforce “no history when logged out”:
- Hide or empty the sidebar when unauthenticated.
- Only use history APIs when the user is logged in.
This allows the current MVP to evolve into a production‑ready Japanese reading assistant with persistent, user‑scoped history and optional authentication, without rewriting the core UI.