Commit Graph

229 Commits

Author SHA1 Message Date
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
yuanzonghao 99ad8d111e fix(play): resume in-progress game after OAuth full-page redirect
Google/GitHub OAuth is a full-page round-trip that unmounts the app and
destroys the in-memory Session (the server is stateless). Returning to
/play?card=m0 re-bootstrapped from the first-act JSON, restarting the
story from scene 1 — the user lost all progress. OTP login kept state
in-memory (no redirect) and was unaffected.

Mirror the homepage 89a5c54 OAuth state-loss fix: snapshot the exact
scene/beat/visited-beats/orientation/image into sessionStorage just
before the redirect, then restore it on mount after the round-trip
(verified signed in). Re-resolve the remote image URL to a fresh blob
(blob: URLs are revoked on unmount). The pending action that hit the
401 (choice / freeform / background-click) is replayed once the restored
state commits, so the player lands exactly where they were headed.

Quota fallback drops the user-uploaded style-reference image (~100KB)
and retries; voices are kept (continuity over rare quota miss). Failure
to restore (corrupt snapshot / not signed in) relinquishes the bootstrap
slot and falls back to normal card/preset/custom start instead of a
blank loading screen.

AuthModal gains an optional onBeforeOAuth callback fired synchronously
before signInWithOAuth navigates away (sessionStorage.setItem is sync).
2026-06-15 14:03:14 +08:00
Zonghao Yuan 7f263b2b14 fix(web): improve home page mobile compatibility (#80)
- 消除移动端横向滑动:重构「载入剧情」按钮的负偏移定位为 right-0 锚定的操作集群
- 加固 overflow-x 兜底(wrapper 层 overflow-x-hidden)
- 收住类别下拉面板宽度,避免靠右选择器展开时溢出
- 移动端质感优化:Header 图标适配、Hero 留白、提示框内边距、风格弹窗小屏布局
- 「载入剧情」按钮改毛玻璃半透明,避免打字机占位文字穿透
2026-06-15 13:16:01 +08:00
Zonghao Yuan da191dd7a2 fix(play): render AuthModal in immersive branch (#78)
手机竖屏 (orientation === 'portrait') 和桌面按 F 全屏
(presentation) 都会走 PlayInner 的 immersive 渲染分支,但该分支
加入时只带了 SettingsModal、漏掉了 AuthModal。导致这两条路径下
若 API 返回 401 触发 setAuthModalOpen(true),登录框不会被挂载,
用户无法登录继续游戏。

预设故事卡片入口 (onCardClick) 不做跳转前登录校验,未登录用户进
/play 后点选项即触发 401,在手机上复现该 bug。

补上与非 immersive 分支完全一致的 AuthModal 块,复用现有
authResolveRef 重试机制,登录成功后自动重放被拦截的请求。
2026-06-14 23:26:52 +08:00
Zonghao Yuan 74e87673d1 Merge pull request #77 from zonghaoyuan/feat/legal-pages
feat(web): add privacy & terms pages for Google OAuth verification
2026-06-14 23:06:54 +08:00
yuanzonghao d813d3dccf fix(web): clarify data transmission vs storage in legal pages
Distinguish between temporary server-side processing and persistent
storage to accurately reflect the actual data flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 23:04:22 +08:00
yuanzonghao b7ff39d467 feat(web): add privacy policy & terms pages, update homepage copy
Add /privacy and /terms pages for Google OAuth brand verification.
Update homepage: 内测→公测, remove sponsor text, refresh save tip,
simplify load button label, add footer legal links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 22:49:02 +08:00
Zonghao Yuan 4812d5b0b7 Merge pull request #75 from zonghaoyuan/worktree-update-roadmap
docs: update Roadmap with completed milestones and new directions
2026-06-14 22:47:24 +08:00
yuanzonghao f8c1d4a8f5 docs: use inline code formatting for .infiplot extension
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 17:55:49 +08:00
yuanzonghao 989f2a7872 docs: update Roadmap with completed milestones and new directions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 17:45:27 +08:00
Zonghao Yuan 0dea2f8e36 fix(ai-client): clean up regressions from OpenAI SDK migration and canvas frame fix (#74)
Three follow-ups to ef3b579 (OpenAI SDK migration) and ebe39ef (canvas frame):

- .env.example / config.ts / AGENTS.md: anthropic & google native protocols
  were removed with the Vercel AI SDK, but .env.example and AGENTS.md still
  advertised them. Rewrite the docs to point Claude/Gemini at their
  OpenAI-compatible endpoints (api.anthropic.com/v1,
  generativelanguage.googleapis.com/v1beta/openai), drop the dead Gemini
  "Nano Banana" image example, sync AGENTS.md (text/vision protocol list,
  image protocol list, the "OpenAI/Gemini via AI SDK" reference note), and
  append a short hint in readProvider() error message guiding
  anthropic/google users to openai_compatible instead of a bare rejection.

- chat.ts: drop the unsafe `as { prompt_tokens_details?: ... }` cast; read
  cached_tokens straight off the SDK's CompletionUsage type. Add a comment
  noting the OpenAI usage object reports cache reads only (no cache-write
  count), so the create cost the old AI SDK path logged is unrecoverable.

- PlayCanvas.tsx: revert <img key={imageUrl}> to key={imageUrl.slice(-48)}.
  The gpt-image/mock paths emit multi-MB data URIs; using the full string as
  React's reconciliation key adds avoidable diff overhead during the frequent
  re-renders. Matches the existing <audio> element's key convention.

Validation: pnpm typecheck passes. (pnpm lint fails on a pre-existing Next 16
`next lint` CLI issue, identical on staging — unrelated to this change.)
2026-06-14 13:36:19 +08:00
Zonghao Yuan 9157454b46 Merge pull request #73 from zonghaoyuan/fix/restore-server-tts-and-fot
fix(play): restore server TTS, FOT strip/merge, nudge, and blob cleanup
2026-06-14 13:11:58 +08:00
yuanzonghao 2f6e67bd80 fix(play): restore server TTS, FOT strip/merge, nudge, and blob cleanup
Reverts the regressions from b63b694 on the server-fallback path:

P0 — fetchBeatAudio non-BYO branch was a bare return; every non-BYO
user got silent playback regardless of server TTS config. Re-connect
to /api/beat-audio with the beatAudioAbortRef signal, count 204/!ok
as silence strikes, create a blob URL on success.

P1 — stripVoicesForTransport + mergeCharactersPreserveVoice were
deleted, so the server-fallback path re-sent ~160KB
referenceAudioBase64 per character on every request AND lost voices
for already-known characters after scene 1. Re-add both, applied
ONLY on the server-fallback branches in engineClient.ts (BYO
client-direct path untouched).

P3 — the aborted-before-store blob URL race had no revoke, leaking
one blob URL per cancelled synth. Re-add the else-if revoke.

P2 — handleSettingsSaved ignored ttsConfigured, so a BYO key entered
mid-session only took effect after a page reload. Re-add the ref/state
refresh + audio re-prefetch. Also restore the silence-nudge UI
(silenceStrikes counter, SILENCE_NUDGE_THRESHOLD, dismissible pill
beside the mute toggle) that surfaces BYO-key guidance when the
shared server key is being rate-limited.

Verified live: /api/beat-audio now returns 200 (was 0 calls under
the bug); audio plays after synth completes.
2026-06-14 13:09:09 +08:00
Zonghao Yuan 5a966627a6 Merge pull request #72 from zonghaoyuan/fix/settings-ui-polish
fix(web): unify settings model sections and refine home hint
2026-06-14 12:44:51 +08:00
yuanzonghao 54a0083e23 fix(web): unify settings model sections and refine home hint
- Rename "自带配音 Key" → "配音模型", drop the section-level "可选" badge,
  and switch its icon to fa-volume-high to match the other model sections
- Drop redundant manual letter-spacing and "·" separators from settings
  field labels (let .smallcaps tracking handle spacing)
- Move the CORS endpoint note to the top of the Models tab
- Home hint: reword to "输入想法", mention text/image/vision models + voice
  key, and add an AUTH_ENABLED-gated "测试期间,登录即可免费畅玩" line

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:24:22 +08:00
Zonghao Yuan c8ffd6443b feat(home): localize character portrait URLs in prebaked first-act JSONs (#71)
* feat(home): localize character portrait URLs in prebaked first-act JSONs

Runware CDN URLs expire, breaking character portraits in prebaked story
cards. Download all 144 portraits as static WebP assets and rewrite
first-act JSONs to reference local paths instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(scripts): add fetch timeout and simplify resize logic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 11:23:17 +08:00
yuanzonghao 0c83f5f2a8 chore: gitignore local-only OpenDeploy and pitch files
- Dockerfile.opendeploy: local OpenDeploy build that hardcodes the public
  image-proxy URL; kept out of the repo so a public fork doesn't route image
  traffic through our Cloudflare Worker.
- .opendeploy: OpenDeploy CLI local context/credentials dir.
- pitch/: local pitch materials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 00:56:54 +08:00
Zonghao Yuan d5ae45b943 Merge pull request #68 from zonghaoyuan/feat/supabase-auth
feat(auth): add Supabase auth with Google, GitHub, and email OTP login
2026-06-13 23:49:15 +08:00
yuanzonghao cb830f023d Merge origin/staging into feat/supabase-auth
Resolve conflicts: keep login_success alongside the new play_error /
play_visibility_lost analytics events; fold auth retry into the play-page
catch blocks so 401s open the login modal and are NOT tracked as play_error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 23:44:23 +08:00
yuanzonghao 11f5ca83ec fix(auth): reject control chars in OAuth callback next param
Defense-in-depth against header injection if the post-login redirect
target ever reaches a context that doesn't re-encode it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 23:19:44 +08:00
yuanzonghao 89a5c54065 fix(auth): address PR review and OAuth state-loss bugs
- proxy: await getUser() so refreshed session cookies land on the response
- callback: gate on AUTH_ENABLED, reject non-relative next (open redirect)
- page: snapshot + resume form and style image across the OAuth redirect;
  require login before the style-image vision parse
- play: wire authResolveRef so login retries the action that hit 401;
  dismissing the modal no longer re-fires it
- server: wrap cookie setAll in try/catch for read-only contexts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:27:51 +08:00
Zonghao Yuan e328d209e0 Merge pull request #69 from zonghaoyuan/feat/play-error-analytics
feat(play): add error observability analytics for mobile diagnostics
2026-06-13 19:27:31 +08:00
yuanzonghao ccdb4780d6 fix(play): throw AbortError on cancelled prefetch to avoid false analytics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:09:04 +08:00
yuanzonghao 0998f7c46a feat(play): add error observability analytics for mobile diagnostics
Track play_error and play_visibility_lost events via Umami to
distinguish mobile vs desktop failure modes. Each error event
captures orientation, connection type, visibility state, elapsed
time bucket, and error classification — all categorical, no free
text. Includes postJson "HTTP \d+" status parsing for the new
engineClient dual-path architecture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 18:57:38 +08:00
yuanzonghao 87a2f93edb feat(auth): add Supabase auth with Google, GitHub, and email OTP login
Introduce user registration/login gated behind optional NEXT_PUBLIC_SUPABASE_*
env vars (leave blank to disable — app behaves exactly as before). Adds
proxy.ts for automatic cookie session refresh, requireUser() API route
guards on all 7 compute-consuming routes, AuthModal (Google/GitHub OAuth +
6-digit email OTP), UserChip header component, and login_success analytics
event. Identity is fully decoupled from Session/engine — no type changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-13 17:33:55 +08:00
Zonghao Yuan b069313014 Merge pull request #67 from zonghaoyuan/fix/image-ready-gate
fix(play): gate scene transition on image decode
2026-06-13 17:32:01 +08:00
yuanzonghao a1b6848688 fix(play): guard decode callback against stale img ref
Verify imgRef.current === el before firing onImageReady, so a
late-resolving decode from a prior <img> element cannot trigger
the gate prematurely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-13 11:51:15 +08:00
Zonghao Yuan 2a2d58a64f Merge pull request #66 from zonghaoyuan/feat/painter-hedged-retry
feat(engine): add opt-in image timeout and scene-paint hedging
2026-06-13 11:44:41 +08:00
yuanzonghao e3ee3547e5 fix(play): gate scene transition on image decode
Keep the "transitioning" overlay visible until the <img> element's
bitmap is fully decoded, so the user never sees progressive paint
or a blank flash between scenes.

- Add onImageReady callback to PlayCanvas (<img onLoad> + decode())
- Delay setPhase("ready") until decode resolves (3s timeout fallback)
- Applied to all 4 scene entry paths: prebaked card, live /api/start,
  performSceneTransition, and recorded replay transition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-13 11:43:35 +08:00
yuanzonghao e68e7e1690 feat(engine): add opt-in image timeout and scene-paint hedging
IMAGE_TIMEOUT_MS sets a per-attempt hard deadline (AbortSignal.timeout);
IMAGE_HEDGE_MS races a second identical scene-paint request when the
first is still pending past the threshold. Both default to OFF when
unset, preserving historical behavior for self-hosted deploys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 11:21:47 +08:00
baizhi958216 c4ffc16498 Merge pull request #64 from zonghaoyuan/refactor/settings-modal
feat: add client-side model configuration and server fallback
2026-06-12 22:09:43 +08:00
baizhi958216 e6004020b5 Merge pull request #65 from zonghaoyuan/fix/play-canvas-stable-frame
fix(play): stabilize canvas frame during image swaps
2026-06-12 22:09:13 +08:00
baizhi958216 ebe39efcac fix(play): stabilize canvas frame during image swaps
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-12 22:02:49 +08:00
baizhi958216 299df0d098 feat(web): remove unuse openai native adapter 2026-06-11 16:56:11 +08:00
baizhi958216 5608b0fdd0 fix(engine): tolerate duplicated JSON outputs 2026-06-11 16:11:52 +08:00
baizhi958216 ef3b57953b refactor(ai-client): replace AI SDK adapters with OpenAI SDK 2026-06-11 16:11:44 +08:00
baizhi958216 6cd7d88326 feat(web): fallback to server API routes when no client-side model config is set
When a user has not configured their own model keys in localStorage,
engine calls now automatically route through /api/* server routes
instead of throwing "模型配置未设置". This lets Vercel deploys with
server-side environment variables work out of the box.

- Add lib/engineClient.ts as a unified client-side routing layer:
  checks localStorage for BYO config, falls back to POST /api/start,
  /api/scene, /api/vision, /api/classify-freeform, /api/insert-beat
- Update app/play/page.tsx to use engineClient instead of direct
  engine imports; remove buildEngineConfig()
- Update app/page.tsx style-image parsing to also fall back to
  /api/parse-style-image when no local model config exists

Signed-off-by: zhi <zhi@peropero.net>
2026-06-11 12:15:14 +08:00
baizhi958216 0f8e641c4c feat(web): merge SettingsModal and ModelSettingsModal with tab navigation
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 94973bc6c6 fix(tts): add non-null assertion in stepfun array access
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 b63b694940 refactor(play): use client-side engine API instead of direct fetch
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 ab2f42bc42 feat(web): merge TTS settings into ModelSettingsModal, remove from SettingsModal
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 6b11a225cd feat(web): add model settings button, modal, and client-side style image parsing
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 71216e1602 feat(ui): add ModelSettingsModal for configuring text/image/vision providers
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 759319bf28 feat(config): extract STYLE_EXTRACTION_PROMPT to shared lib for client reuse
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:13 +08:00
baizhi958216 a2dd5ad630 feat(config): add client-side model config storage and EngineConfig resolver
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:13 +08:00
baizhi958216 2088bae311 fix(tts): replace Buffer.from with browser-compatible arrayBufferToBase64 in stepfun
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:13 +08:00
Qi Chen e34306997a Merge pull request #63 from zonghaoyuan/feat/export-with-audio
feat(web): embed beat audio into gallery and infiplot exports
2026-06-11 09:36:42 +08:00
DESKTOP-I1T6TF3\Q 621f83c47b feat(web): embed beat audio into gallery and infiplot exports
Walk every speaking beat at export time, reuse current scene's beatAudioMap,
and synth the rest via BYO TTS or /api/beat-audio with concurrency 4. Show a
progress toast on the play page while collecting.

Gallery export keeps audio in a sidecar localStorage key so the first paint
is not blocked by JSON.parse-ing several MB of base64; the gallery lazy-loads
it after the first scene image, then plays per-beat audio with a mute toggle
persisted to localStorage. .infiplot share files embed audioByBeatId in the
doc itself (v2); on import the data URIs survive scene swaps and feed back
into the per-beat audio map so replayers hear the original voices for free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 09:29:16 +08:00
Zonghao Yuan a61a91060d Merge pull request #62 from zonghaoyuan/feat/home-import-tooltip-infiplot
feat(web): clarify home import button tooltip as "载入infiplot剧情"
2026-06-10 00:18:06 +08:00
Zonghao Yuan ba3001329b Merge pull request #61 from zonghaoyuan/chore/style-thumb-kyoani-shinkai
chore(home): swap Kyoto Animation and Shinkai style thumbnails
2026-06-10 00:13:00 +08:00