Commit Graph

287 Commits

Author SHA1 Message Date
Zonghao Yuan 4c0d033586 Merge pull request #110 from zonghaoyuan/feat/split-firstact-beats
refactor(presets): split mixed narration+dialogue beats
2026-06-24 19:30:42 +08:00
yuanzonghao f340ab69b5 refactor(presets): split mixed narration+dialogue beats into separate beats
Preset firstact JSONs had 37% of beats bundling narration and dialogue
on the same beat object, while the current engine (Paradigm D prose
splitter) produces strictly one-type-per-beat output. This mismatch
caused the preset card experience to feel different from actual gameplay.

Split 275 mixed beats across 94 files (120 total) into independent
narration→dialogue beat pairs, preserving all images, characters, voices,
and graph references. Beat count: 738→1013.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-24 19:06:43 +08:00
Zonghao Yuan eb79f3b272 Merge pull request #109 from zonghaoyuan/feat/play-font-size
style(play): increase dialogue and choice font sizes
2026-06-24 19:03:59 +08:00
Zonghao Yuan 56ed505554 Merge pull request #108 from zonghaoyuan/feat/feedback-fab
feat(web): add feedback FAB on homepage
2026-06-24 19:01:07 +08:00
Zonghao Yuan fa524c19e4 Merge pull request #105 from zonghaoyuan/feat/readme-update
docs(readme): update architecture, add OpenDeploy sponsor
2026-06-24 19:00:23 +08:00
Zonghao Yuan 3a16eb35b5 Merge pull request #107 from zonghaoyuan/fix/fullscreen-fkey-in-input
fix(play): skip fullscreen shortcut when typing in text inputs
2026-06-24 18:59:23 +08:00
yuanzonghao 94050b82c5 style(play): increase dialogue and choice font sizes by 3px
Bump all in-game text sizes for better readability:
- Dialogue body: 16/13/15px → 19/16/18px
- Narration: 15/12/14px → 18/15/17px
- Speaker name: 13/11/12px → 16/14/15px
- Choice label: 15/13/14px → 18/16/17px
- Choice index: 13/11px → 16/14px
- Freeform input: 14px → 17px
- Freeform button: 13px → 16px

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-24 18:59:21 +08:00
yuanzonghao 8964155b26 feat(web): add floating feedback FAB on homepage linking to Tally form
Collect beta user feedback via Tally.so — fixed-position button in the
bottom-right corner opens the form in a new tab. Ember-orange default,
clay-dark on hover, i18n labels for zh-CN / en / ja.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-24 18:59:08 +08:00
yuanzonghao 4603a7df71 docs(readme): update architecture, add OpenDeploy sponsor, fix LINUX DO link
- Remove deprecated Architect agent from pipeline diagrams (zh/en/ja SVGs)
  and README text — Writer now handles story architecture
- Add OpenDeploy as sponsor section and one-click deploy option
- Change LINUX DO badge link to the actual forum post
- Move Open Deploy to completed in Roadmap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-24 18:53:26 +08:00
yuanzonghao d4f6b18682 fix(play): skip fullscreen shortcut when typing in text inputs
The window-level 'F' keydown listener now ignores events from INPUT,
TEXTAREA, and contentEditable elements so freeform input is not
interrupted by the presentation-mode toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-24 18:51:26 +08:00
Zonghao Yuan ae4d9f8873 Merge pull request #103 from zonghaoyuan/feat/selfhost-font-awesome
perf(web): self-host Font Awesome instead of cdnjs CDN
2026-06-19 01:36:56 +08:00
yuanzonghao 8468d5da3a chore: revert auto-generated next-env.d.ts change
Drop the unintended path change (.next/dev/types → .next/types)
that `next build` auto-wrote — this file is managed by Next.js
and should not carry build-vs-dev path diffs across branches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-19 01:36:15 +08:00
yuanzonghao 05a5251658 perf(web): self-host Font Awesome instead of cdnjs CDN
Replace the external cdnjs.cloudflare.com <link> with a bundled
npm import so FA CSS+fonts ship as static assets through the
same Cloudflare CDN that serves the app — eliminates an extra
DNS lookup and third-party CDN dependency, improves reliability
for China users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-19 01:33:40 +08:00
yuanzonghao f8cfa90d4c chore: remove stale GALLERY_SECRET from .dev.vars.example
GALLERY_SECRET was removed in 64cf9c3 — .infiplot files now use
plaintext + SHA-256 integrity, no encryption secret needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 23:50:35 +08:00
Zonghao Yuan 69908862ef Merge pull request #101 from zonghaoyuan/staging
chore: merge staging into main
2026-06-18 23:21:38 +08:00
Zonghao Yuan 61ef027a0e fix(cla): route signatures to unprotected cla-signatures branch (#100)
The CLA bot run on PR #99 failed with 'Resource not accessible by
integration' because it tried to commit cla-signatures/version-1.json
to the branch-protection-protected main branch. The CLA Assistant
action cannot push to protected branches (contributor-assistant/
github-action#150); PAT identity is not granted bypass even though
the PAT owner is repo admin.

Fix: store signatures on a dedicated orphan branch 'cla-signatures'
that has no protection rules, so the PAT can commit freely. This is
the workaround recommended by the action maintainers and the dominant
community solution.

Also brings the staging copy of cla.yml up to parity with main (the
guard step + SHA pin from PR #93 had not been back-merged to staging):
- Add the 'fail on missing CLA_BOT_TOKEN' guard step
- Pin action to contributor-assistant/github-action@ca4a40a7... (v2.6.1);
  the old 'cla-assistant-action' path 404s and was a second reason the
  bot never fired
2026-06-18 23:19:20 +08:00
yuanzonghao 0a7076d5b9 fix(i18n): overhaul i18n with [locale] routing, SSR translations, and hreflang SEO
Rewrites the i18n system introduced in PR #94 to use Next.js App Router
[locale] dynamic segments with SSR-rendered translations and proper
middleware locale routing.

- Add middleware locale detection: / rewrites to /zh-CN/ internally,
  /en and /ja pass through, /zh-CN/... redirects to bare path
- Move all 7 pages under app/[locale]/ with SSR translation injection
- Fix server→client serialization: pre-evaluate function-valued
  translations (makeSerializable) to eliminate hydration flash
- Fix language switch key flash: use hard navigation with localStorage-
  only persistence, avoiding React state update before page reload
- Add <link rel="alternate" hreflang> tags for multilingual SEO
- Fix Supabase setAll overwriting locale rewrite response
- Trim locales from 22 to 3 (zh-CN/en/ja), delete 19 incomplete files
- LLM-translate 240 firstact game preset JSONs (en + ja, landscape +
  portrait) and story titles via gemini-3.5-flash
- Delete 11 one-off migration scripts and outdated i18n docs
- Add useLocalePath hook and navigation utilities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 23:16:17 +08:00
Zonghao Yuan 4a0072bffb Merge pull request #93 from zonghaoyuan/sync/staging-to-main
chore(release): sync staging → main
2026-06-18 22:52:45 +08:00
yuanzonghao 366b84e2fb chore(repo): harden CLA workflow — pin SHA, fix action repo, fail on missing token
Addresses pr-agent review on PR #93 (the two CLA-specific items). The
other 9 suggestions targeted code carried in by the sync (middleware,
gender-x) and are out of scope here.

- uses: contributor-assistant/cla-assistant-action@v2.6.1  (404 — wrong name)
+ uses: contributor-assistant/github-action@ca4a40a7...       (canonical, pinned)

  The action's real home is contributor-assistant/github-action; the
  'cla-assistant-action' path we had resolves to 404 and may itself be
  why the bot never fired on PR #92. Pin to the full SHA of v2.6.1 so a
  movable tag can't slip a malicious update under pull_request_target +
  a writable PAT. The repo is archived but v2.6.1 still functions.

- Add a guard step that FAILS the job on the canonical repo
  (zonghaoyuan/infiplot) when CLA_BOT_TOKEN is missing. Previously a
  missing token silently skipped the job and it 'succeeded' — dangerous
  once this becomes a required status check (a lost/expired token would
  let CLA enforcement degrade invisibly). Forks still skip cleanly.
2026-06-18 21:59:19 +08:00
yuanzonghao 941b54c3f8 fix(share): require at least 1 byte of content in .infiplot files
Reject header-only files (37 bytes, empty plaintext) at the unpack
boundary instead of letting them through as empty strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 21:56:38 +08:00
yuanzonghao 64cf9c330d refactor(share): remove GALLERY_SECRET, use plaintext + SHA-256 integrity for .infiplot files
The encrypted .infiplot format (AES-256-GCM via GALLERY_SECRET) provided no
meaningful security — the payload is AI-generated story content with no
credentials or PII, and the project is open source. Replace with plaintext +
SHA-256 integrity check (format v2). Story share is now always enabled without
requiring a server secret.

- galleryCrypto.ts: AES-256-GCM → plaintext + SHA-256 hash; remove secret param
- 4 API routes: remove GALLERY_SECRET guard and 503 fallback
- story-unpack: forward specific error messages (v1 compat, hash mismatch)
- gallery/page.tsx: remove stale AES-GCM comment
- AGENTS.md: document gallery-pack/gallery-unpack routes
- .env.example, wrangler.jsonc: remove GALLERY_SECRET references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 21:41:56 +08:00
Zonghao Yuan 03dccd7c74 chore(deploy): add keep_vars and remove placeholder vars from wrangler.jsonc (#97)
Add "keep_vars": true so Dashboard-set variables survive redeployments
(Vercel-like behavior). Remove empty-string placeholder vars — provider
config (TEXT_BASE_URL, IMAGE_*, VISION_*, TTS_*) is now managed entirely
via Dashboard. Update comments to document three variable categories
(secrets, runtime, build-time) and their setup locations.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 20:22:54 +08:00
Zonghao Yuan 1a7d8a21a9 chore(deploy): sanitize wrangler.jsonc for open source, hide story persistence UI (#96)
- Replace hardcoded API URLs and model names with empty-string placeholders
  so self-deployers set their own values via Dashboard
- Comment out D1/R2/KV bindings and placement region to prevent deploy
  failure for users who don't need these optional services
- Add comprehensive comments for secrets, optional tuning vars, and
  build-time NEXT_PUBLIC_* variables
- Hide /stories navigation link until auth integration is ready
  (all story persistence code is retained)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 18:57:17 +08:00
Zonghao Yuan 0e4c2ebef4 feat(engine): merge cloudflare-migration — paradigm D engine, BYOK proxy, story persistence (#95)
Squash-merge the cloudflare-migration branch (7 commits by Kai ki) into
staging with conflict resolution, feature integration, and bug fixes.

Engine:
- Paradigm D: single-stream Writer replacing dual-phase Plan/Beats
- Delete Architect agent; story bible generated via Writer <plan> tag
- Modular prompt architecture (segments/registry/builder)
- StreamRouter for tagged stream splitting (<plan>/<story>/<choices>)

Infrastructure:
- Cloudflare Workers deployment (wrangler.jsonc, OpenNext adapter)
- D1 database schema + Drizzle ORM (scaffolded, not yet active)
- R2 storage helpers (scaffolded, not yet active)
- Story persistence API routes + client-side persistence

BYOK (Bring Your Own Key):
- /api/llm/user-proxy with SSRF-protected LLM proxy (+ requireUser auth)
- CORS-aware fetch in ai-client: auto-detect CORS failure, fallback to
  server proxy transparently via OpenAI SDK custom fetch
- BYO config support added to classify-freeform and vision routes
- SettingsModal CORS privacy notice (keys never logged/stored)

SSE streaming:
- engineClient.ts: fetchSSE helper for progressive scene events
- startSession/requestScene accept optional emit callback
- Fix SSE error event field name (error → message) in scene/start routes

i18n integration:
- Wire buildLanguageDirective into paradigm D's prompt builder
- Update corsNotice i18n keys (zh-CN/en/ja) with CORS proxy privacy text
- Preserve Session.language + LanguageSwitcher from i18n commit

Co-authored-by: Kai ki <155355644+zbf1009@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 18:05:38 +08:00
Qi Chen 05bd7e229c Merge pull request #94 from zonghaoyuan/feat/gender-x
feat(i18n): language switcher with en/ja translations
2026-06-18 17:01:20 +08:00
DESKTOP-I1T6TF3\Q 2d35c1d9de feat(i18n): add language switcher with en/ja translations
- New client-side i18n via React Context (useI18n, tArray, I18nProvider)
- Catalog ships 21 locale stubs; only zh-CN/en/ja have reviewed translations
- Header language switcher (globe icon + short label) before settings gear
- All hardcoded Chinese UI text migrated to keys: typewriter, options,
  hints (with embedded gear icon via dangerouslySetInnerHTML), settings
  panel, footer/about, play page hints
- AI output language follows user-selected locale via trailing one-liner
  directive appended to Architect/Writer/CharacterDesigner/InsertBeat
  user messages (preserves system-prompt cacheability)
- Per-locale separator rule: zh uses middot between every glyph; en/ja
  use plain spaces
- Option value → i18n key suffix maps preserve Chinese as the underlying
  identifier so analytics unions and STYLE_MAP keys stay byte-stable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 16:54:35 +08:00
Zonghao Yuan 4abb57ec1f Merge pull request #89 from zonghaoyuan/feat/cla-mechanism
feat(repo): add CLA mechanism for external contributors
2026-06-18 11:05:26 +08:00
Zonghao Yuan 09844bd5db fix(web): migrate proxy.ts to edge middleware.ts for Cloudflare compat (#88)
Next.js 16 locks proxy.ts to the Node.js runtime, but OpenNext for
Cloudflare rejects Node.js middleware at build time ("Node.js middleware
is not currently supported", build.js exit 1). Rename to middleware.ts
with an explicit experimental-edge runtime so the Supabase SSR cookie
refresh runs on edge and stays deployable to both Vercel and Workers.

Supabase SSR only uses Web APIs (fetch, cookies), so it is
edge-compatible; the core getUser() refresh logic is unchanged. The
matcher excludes static assets by file extension (not by "contains a
dot") so future dotted dynamic routes (e.g. /u/john.doe) still get the
cookie refresh. getUser() is wrapped in try/catch so a transient
network error (rethrown by @supabase/auth-js) doesn't 500 or crash the
page request — the cookie simply isn't refreshed that round.

Note: runtime must be "experimental-edge", not "edge". Next.js 16 routes
the root middleware file through the pages-router static-info path, where
runtime "edge" throws E1015 at build ("Use runtime 'experimental-edge'
instead"). "experimental-edge" only warns; both are treated as edge by
isEdgeRuntime().

Verified: pnpm typecheck, pnpm build (Vercel), pnpm build:cf
(Cloudflare — Bundling middleware function -> OpenNext build complete,
node-middleware guard no longer fires).
2026-06-18 11:00:49 +08:00
Qi Chen 9ec21c46e7 Merge pull request #90 from zonghaoyuan/feat/gender-x
feat(options): add third gender option "X" for universal gender
2026-06-18 09:26:33 +08:00
DESKTOP-I1T6TF3\Q f1fe7964a2 feat(options): add third gender option "X" for universal gender
- Add "X" to GENDERS array in lib/options.ts
- Add example phrases for "X" gender (sci-fi themed)
- Make "X" use same preset cards as male gender
- Map "X" to "通用性别" when transmitting to AI
- Add "X" to DISPLAY_ORDER (same as male)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 09:18:50 +08:00
Zonghao Yuan 4c7bdcffa5 Merge pull request #87 from zonghaoyuan/worktree/update-readme-2
docs: sync README with recent changes (TTS, model support, Cloudflare)
2026-06-17 19:50:36 +08:00
yuanzonghao 6ee74a0680 chore(repo): address pr-agent review on CLA workflow
Address the valid points from pr-agent on PR #89, skip the inaccurate
ones (e.g. the $contributorName placeholder it suggested does not exist
in cla-assistant-action; create-file-commit-message fires before any
signer exists).

- cla.yml: move 'token configured?' check into job env and put the whole
  step-level if inside a single ${{ }} so && / || are evaluated as a
  boolean (step-level if cannot safely reference secrets.* directly)
- cla.yml: declare minimal explicit permissions (contents/pull-requests/
  issues/statuses) — this workflow runs on pull_request_target with a token
- cla.yml: drop the overly broad '*bot' allowlist wildcard; keep explicit
  bot + maintainer accounts only
- cla.yml: clean up the stray trailing '@' in create-file-commit-message
  (used once, at signature-store creation, before any signer exists)
- README{,.en,.ja}: clarify that the CLA is signed via a PR comment, not
  before opening the PR — matches the actual CONTRIBUTING flow
2026-06-17 19:49:48 +08:00
yuanzonghao 51afeb09ac docs: sync README with recent changes (TTS, model support, Claude caching)
- Model support: text/vision now OpenAI-compatible only (Claude/Gemini
  via their OpenAI-compatible endpoints); drop stale native-protocol copy
  and the anthropic/google rows from the *_PROVIDER table
- TTS: document StepFun (step-tts-2, paid, better quality) alongside
  Xiaomi MiMo (free) across zh/en/ja; update Vercel deploy envDescription
- Claude: note that direct Anthropic endpoints lack caching, recommend
  gateway for full experience
- Cloudflare: preserve existing one-click deploy (compat work in progress)
2026-06-17 18:26:36 +08:00
yuanzonghao 2b0b9c6f8d feat(repo): add CLA mechanism for external contributors
Introduce a Contributor License Agreement (CLA) so external contributions
can be licensed under AGPL-3.0 and any other terms (incl. closed-source),
keeping the AGPL-3.0 codebase usable in closed-source projects.

- CLA.md: authoritative English CLA (ICLA + employer authorization, v1.0)
- CLA.zh.md: non-binding Chinese reference translation
- CONTRIBUTING.md: bilingual contributing guide, points to CLA
- .github/workflows/cla.yml: self-hosted cla-assistant-action that records
  signatures into cla-signatures/version-1.json; exempts maintainers & bots
  via allowlist; skips when CLA_BOT_TOKEN is unset
- .github/PULL_REQUEST_TEMPLATE.md: guides contributors to sign
- README.{md,en.md,ja.md}: add License & contributing footer
- app/terms: note CLA requirement in the IP section

Enforcement requires repo-level setup (PAT secret + branch protection)
documented in cla.yml; not covered by this commit.
2026-06-17 12:23:46 +08:00
Zonghao Yuan 2cfe105878 Merge pull request #86 from zonghaoyuan/staging
release: staging → main (WEBP image format + header fixes)
2026-06-15 18:10:45 +08:00
Zonghao Yuan 98d8e83c92 Merge pull request #85 from zonghaoyuan/test/image-compression
perf(image): switch Runware output format from PNG to WEBP
2026-06-15 18:07:39 +08:00
yuanzonghao d0faa06cc1 perf(image): switch Runware output format from PNG to WEBP
WEBP produces ~90% smaller files than PNG at visually identical quality
(tested: 5.4MB → ~550KB per 1792×1024 image), significantly reducing
client download time for users on slower connections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 18:04:31 +08:00
Zonghao Yuan c452dcfeb0 Merge pull request #84 from zonghaoyuan/fix/header-icon-alignment
fix(web): hide login button when logged out to fix header icon alignment
2026-06-15 17:51:08 +08:00
yuanzonghao 60224ea33b fix(web): hide login button when logged out to fix header icon alignment
未登录时 UserChip 的登录按钮使用了深色背景配色(cream-50 色系),
在首页浅色背景上不可见但仍占据约 60px 宽度,导致齿轮/GitHub/X
图标被推离右边缘。改为未登录时不渲染 UserChip,登录入口保留在
开始游戏等需要认证的操作流程中。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 17:47:01 +08:00
yuanzonghao d29087a875 chore: add piolium/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 17:14:58 +08:00
Zonghao Yuan 272e940cf2 Merge pull request #83 from zonghaoyuan/staging
chore: sync staging → main
2026-06-15 16:49:47 +08:00
Zonghao Yuan dc08f64ec1 fix(web): 恢复移动端 Header 的 GitHub/X 链接 (#81)
* fix(web): restore GitHub/X links in mobile header

上一个 PR (#80) 为缓解 Header 移动端拥挤,给 GitHub/X 图标加了
hidden sm:inline-flex,导致手机端(<640px)看不到这两个入口。
横向溢出的真正元凶(导入按钮负偏移)已在 #80 修复,Header 全显示
不会重新引入左右滑动,故恢复图标在所有视口常驻显示。

* fix(web): 移动端 UserChip 只显示头像,宽度与其他图标按钮对齐

登录态 UserChip 之前是「头像 + 名字」的宽按钮(≈90px),
是齿轮/GitHub/X 图标按钮(≈16px)的 5-6 倍,导致 Header
右侧集群宽度在登录/未登录切换时剧烈变化,所有按钮位置大幅偏移。

改动:
- 去掉名字显示,只保留头像圆圈
- 头像尺寸 h-5→h-4(16px,与图标按钮一致)
- 按钮 padding 收窄至 p-0.5(总宽≈22px)
- 加 title 属性保留完整用户名(hover 可见)
- 移除开发过程中使用的 mock 登录态临时代码
2026-06-15 15:28:24 +08:00
yuanzonghao ba9f9c1342 Merge PR #79: feat(tts): StepFun voice selection via CharacterDesigner + provider-aware beat-audio
- StepFun voice selection: CharacterDesigner picks a preset voiceId from the
  32-entry catalog (zero extra LLM call); pickStepfunVoiceId remains as fallback.
- Prebaked homepage cards enriched with stepfunVoiceId (147 characters, gemini model).
- /api/tts-provider endpoint + client probe: skip the ~220KB Xiaomi reference
  audio when the server runs StepFun (saves Fast Origin Transfer bandwidth).
- Server-side resolveVoice normalization: re-provisions on provider mismatch.
- Removed hardcoded 1.2x speech playback speed (was for slow MiMo voice).
- Hardened voice-provider validation per PR-agent review.

Xiaomi path prompt is byte-identical to history (prompt-cache-preserving).
2026-06-15 15:08:21 +08:00
Zonghao Yuan 6e1ad55f1a Merge pull request #82 from zonghaoyuan/fix/play-oauth-resume
fix(play): resume in-progress game after OAuth full-page redirect
2026-06-15 14:59:46 +08:00
yuanzonghao 65b7daff0b fix(beat-audio): harden voice-provider validation and resolveVoice fast path
Address PR-agent review findings:

- resolveVoice fast path: replace ambiguous boolean comparison
  (voiceProvider === "stepfun") === serverStepfun with explicit
  per-provider equality checks. Prevents an undefined or unknown
  provider from matching the non-stepfun (xiaomi) branch by accident.

- /api/beat-audio route: reject requests whose voice.provider is present
  but not in the VALID_TTS_PROVIDERS whitelist (e.g. "azure"). Previously
  such a request would pass validation when fallback fields were also
  present, and resolveVoice might use the invalid voice directly instead
  of falling back to reprovision — producing a silent beat instead of a
  voiced one.
2026-06-15 14:33:46 +08:00
yuanzonghao 3a012d46bf fix(auth): harden snapshot paths per PR agent review
Address two suggestions from the PR agent review:

1. lib/authResume.ts — catch isAuthed() exceptions in
   consumeResumeSnapshot. The network/timeout path now returns
   null (snapshot already removed earlier to prevent the play-page
   bootstrap's retryBootstrap loop from re-entering this path).
   Document the intentional removeItem-before-isAuthed ordering.

2. components/AuthModal.tsx — wrap onBeforeOAuth in try-catch so
   a snapshot failure (e.g. sessionStorage blocked in privacy mode)
   does not abort the OAuth flow and leave the UI stuck in loading.
2026-06-15 14:32:04 +08:00
yuanzonghao 0166c5e0a9 chore(home): re-enrich firstact voiceIds with gemini model
Re-ran scripts/enrich-firstacts-stepfun.mjs with gemini-3.1-flash-lite-preview
as the TEXT model (was deepseek-v4-flash). The new picks better match the
mysterious / cool / melancholic archetypes common in the curated cards
(e.g. 夜煌 清冷空灵+悲怆决绝 → lengyanyujie 冷艳御姐, was youyanvsheng).

Only stepfunVoiceId values changed across 52 cards; voice (Xiaomi) /
imageUrl / scene untouched. 0 failures across the 147-character run.

run: pnpm enrich:firstacts [--force] [--portrait]
2026-06-15 14:03:34 +08:00
yuanzonghao 17341cbd4a feat(play): remove hardcoded 1.2x speech playback speed
The SPEECH_RATE=1.2 constant was added to speed up the somewhat slow MiMo
voicedesign voice. With StepFun preset voices (whose tempo is already
appropriate) and no per-provider logic, a global 1.2x is no longer the
right default. Remove the constant and all 4 of its uses:

- the constant declaration + comment
- two `el.playbackRate = SPEECH_RATE` assignments (audio now plays at 1.0)
- the typewriter pacing divisor (`/ SPEECH_RATE`) — audio and text both
  return to original duration, staying in lockstep

A future user-facing speech-speed setting (UI control + persisted pref)
would be a separate feature with a different shape; no placeholder kept.
2026-06-15 14:03:20 +08:00
yuanzonghao 6060d76b44 fix(auth): close two regressions from the resume refactor
Critical: play-page bootstrap infinite loop when AUTH_ENABLED and no
resume snapshot. The refactor changed the gate from
`if (AUTH_ENABLED && hasSnapshot)` to `if (AUTH_ENABLED)`, so any
snapshot-less /play entry (the common case — normal card/preset/custom
start) entered the async branch, got null from consumeResumeSnapshot,
bumped retryBootstrap, and re-ran the effect forever. Restored the
peek-before-await: only enter the async resume branch when a snapshot
actually exists; otherwise fall straight through to normal bootstrap.
Verified via control-flow simulation across all three paths (no
snapshot / snapshot + signed in / snapshot + not signed in).

Major: homepage auto-started a game after a bare OAuth login. Routing
persistPendingStart through AuthModal.onBeforeOAuth fired it for every
OAuth redirect, including bare logins via UserChip / StyleModal
onRequireAuth (where pendingAction is null and the user only wanted to
sign in). Guarded the snapshot on `pendingAction === "start"` so only
the mid-start flow persists; bare logins no longer resurrect the form
and auto-start on return.
2026-06-15 14:03:14 +08:00
yuanzonghao 8cdeb1592f refactor(auth): share OAuth-resume plumbing between home and play pages
Extract the page-agnostic resume primitives into lib/authResume.ts:
- isAuthed() — single login check (was duplicated in app/page.tsx)
- writeResumeSnapshot(key, primary, fallbacks) — quota-safe sessionStorage
  write with ordered lighter-payload fallbacks (was hand-rolledTry/catch
  in both pages)
- consumeResumeSnapshot<T>(key) — consume-once resume gate that verifies
  the user is signed in before returning the snapshot, else clears it

Both pages now share this plumbing while keeping their own snapshot shapes
and restore side effects (home: form fields + start(); play: Session +
restorePlayResume + deferred action replay).

Unify the persist trigger: home previously snapshotted eagerly inside
start() before opening the modal, while play snapshotted in
AuthModal.onBeforeOAuth at redirect time. Move home to the same
onBeforeOAuth trigger so both pages persist at the single OAuth-redirect
instant — the eager-snapshot special case is gone, and OTP (no redirect)
keeps its in-place onSuccess resume on both pages.

Net: -21 lines. Behavior preserved for OTP; OAuth resume now consistent.
2026-06-15 14:03:14 +08:00