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.
- 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>
- 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>
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>
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>
Replace the auto-generated kyoani / shinkai style thumbnails with hand-picked
reference frames. Source PNGs were center-cropped to square and re-encoded as
512x512 WEBP (~41KB each) to match the existing thumbnail format. Bumps the
shared cache-buster from v5 to v6 so existing browsers fetch the new files.
The home-page file-import button accepts .infiplot story files. The
tooltip now spells out the file type so users distinguish it from
"开始剧情"/"载入预设" affordances on the same screen.
- Add Content-Length pre-check to story-pack and story-unpack routes
to reject oversized payloads before buffering the body
- Suppress internal error details in story-unpack catch (was leaking
e.message to the client)
- Strengthen sceneIndex validation: require non-negative integer
- Guard against undefined storyState when replaying shared stories
- Fix prefetch regression: remove currentBeat?.id from useEffect deps
that was re-triggering all change-scene prefetches on every beat
- Fix double detach: use else-if so the second replay detach guard
doesn't fire redundantly after the first already detached
- Align client file-size limit by format (.json 12MB, .infiplot 13MB)
- Move "载入剧情" import button next to "开始" with hover tooltip
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge vision-click toggle into the shared SettingsModal alongside
player name and TTS key configuration. Remove standalone TtsKeyModal.
Add settings gear button to PlayCanvas dialogue card and header.
Fix fullscreen settings modal not rendering in immersive mode.
Voice toggle uses standard CategorySelect dropdown matching other
tab bar options.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 14 new painting styles sourced from preset story card generation
scripts: Dunhuang fresco, Persian miniature, Byzantine mosaic, stained
glass, vaporwave, vector illustration, low poly, pop art, glitch art,
papercut, steampunk, xianxia fantasy, dark fairytale, and urban fantasy.
Reorder all 36 styles into logical visual categories (anime → cinematic
→ Eastern traditional → Western traditional → genre → digital → handcraft)
for easier browsing. Update "auto" thumbnail to a 3×3 composite grid and
"custom" thumbnail to a paintbrush-on-canvas concept image.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Player name: stored in localStorage, injected into Architect/Writer/InsertBeat
prompts so NPCs address the player by name, displayed in dialogue UI
- Freeform input: compact button at choice nodes expands to text input, LLM
classifier routes to insert-beat (interactive NPC response) or change-scene
- SettingsModal: unified panel merging player name, voice toggle (with
collapsible TTS key section), replacing the old TtsKeyModal
- Insert-beat upgrade: prompt now requires NPC reaction when characters are
present, shared by both freeform and Vision paths
- IME guard: isComposing check on freeform input to prevent CJK mid-composition
submission
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a "导出图集" action at the bottom-right of the play canvas that
snapshots the current session into localStorage and opens
/gallery#id=<id> in a new tab — the original play page keeps running
untouched. In parallel, sends the doc to /api/gallery-pack and
downloads the result as a binary .infiplot file the player can send
to a friend.
The snapshot pulls in:
- Every visited scene's image + beat graph + recorded visit trail
- All AI-prefetched alternate scenes (a new resolvedPrefetchesRef in
PlayInner captures each prefetch as it resolves, so abandoned
branches the engine already paid to generate are kept)
- Character names + basePortraitUrl (voice base64 / styleReference
are stripped — they aren't needed for replay)
/gallery is a no-network interactive replay:
- Per-beat advance and per-choice navigation. Picked choices are
highlighted; unpicked choices are clickable when an alternate was
prefetched, greyed otherwise.
- Stack-based navigation for stepping into branches with one-tap
"返回主线" to collapse back to the main path.
- Top-bar batch download for scene images (including unique
AI-prefetched branch scenes, deduped against the main path) and
character portraits. Fetched with a per-file AbortController + 20s
timeout in a small concurrency pool, then clicked serially.
Prevents one slow CDN response from stranding the busy button.
- In-progress hint banner reminding the player to allow the
browser's "multiple downloads" prompt.
- F-key fullscreen with a top toolbar that auto-retracts after the
initial glance and pops back down on cursor approach.
- Per-scene dialogue panel (fa-clock-rotate-left, matching the
in-game history affordance).
- "导入分享文件" entry on the empty/error state — accepts a friend's
.infiplot, posts to /api/gallery-unpack, renders the decrypted doc.
Share-file format (.infiplot):
- AES-256-GCM via Web Crypto (portable to Cloudflare Workers).
- Layout: 4-byte magic "IFPL" + 1-byte version + 12-byte nonce +
ciphertext (includes 16-byte auth tag).
- Key derived from GALLERY_SECRET via SHA-256.
- GCM's auth tag gives tamper-detection for free; any flip in the
ciphertext/nonce surfaces as "文件校验失败" — same error as wrong-key,
so the distinction can't leak server config.
- Stateless: server keeps no record of issued files.
- GALLERY_SECRET unset → /api/gallery-pack returns 503, the play page
silently skips the share-file download, local view still works.
Rotating the secret invalidates every previously-issued file.
Retention: trimGalleryExports keeps only the 2 most recent localStorage
docs; older ones are evicted before each write so quota stays flat
regardless of how many times the player exports. Share files live on
the player's own disk — no retention concern.
Adds 'gallery_export' to the analytics event schema (scene_count only —
no free text).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add isComposing guard to the homepage prompt textarea so CJK users
no longer accidentally submit while composing. Also show a subtle
"Enter 发送 · Shift+Enter 换行" hint when the input has content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When user picks "自动", the client sends styleGuide="auto" to the
server. The orchestrator then runs a lightweight style-selector LLM
call in parallel with the Architect — both only depend on worldSetting,
so there is zero added latency. The selector picks the best-matching
preset from STYLE_MAP based on genre, mood, and setting.
Also moves STYLE_MAP from page.tsx to lib/options.ts so it can be
shared between client and server.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused `isAuto` variable after magic-wand button removal
- Add focus-visible ring to style cards for keyboard accessibility
- Update DEFAULT_STYLE comment to match actual fallback (吉卜力)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rework custom style view: fixed modal height to match grid view, move
upload and preset-import controls to bottom toolbar alongside cancel/save,
textarea fills remaining space. Add bordered style to cancel button,
improve disabled save button visibility, remove per-card magic-wand
customize button, and add placeholder hint about English prompts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite all 20 STYLE_MAP prompts with precise art terminology (sfumato,
feibai, bokashi, broken-color, etc.) and richer color/texture descriptions.
KyoAni prompt now references Beyond the Boundary and Sound Euphonium;
Ghibli references Spirited Away and Howl's Moving Castle. Regenerate all
style thumbnails using a two-step pipeline: DeepSeek picks an optimal
visual-novel scene per style, then Runware renders it. Add cache-busting
query param (thumbV) to thumbnail URLs. Include gen-style-thumbs.ts script
for future regeneration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redesign the painting-style picker inspired by Pollo AI: widen modal to
1400px, show styles as square thumbnail cards in a 4-column grid with
name labels below, add ember glow hover effect, and split custom-style
editing into its own view. Simplify style names (e.g. "京阿尼细腻日常" →
"京阿尼"), add 22 .webp preview thumbnails, and remove the per-preset
override mechanism in favor of a cleaner grid + custom flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The BYO (Bring Your Own) API key configuration for LLM and image
generation will be re-implemented via Cloudflare Workers. Remove
the client-side implementation to prepare for that migration.
TTS (text-to-speech) BYO key support is intentionally preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden BYO API header against SSRF and input abuse
- Add lib/validateUrl.ts with HTTPS-only + public-IP enforcement,
provider allowlist, IPv6 rejection, and userinfo-in-URL blocking.
- Add lib/byoHeaders.ts — single source of truth for client-side BYO
header construction (deduplicates app/page.tsx & app/play/page.tsx).
- config.ts: validate BYO endpoints via isPublicUrl(), cap header at
2 KB, truncate apiKey/model strings, sanitize log output.
- fetchWithRetry: default redirect to "manual" to block 302-to-intranet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): address Copilot review — trim endpoint, strip control chars, drop unused import
- safeEndpoint: trim whitespace before URL validation
- safeString: strip ASCII control characters to prevent header injection
- play/page.tsx: remove unused BYO_STORAGE_KEY import
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Public users share one server TTS key, so Xiaomi's per-key RPM/TPM limits
cause silent playback under concurrency. This adds an OPTIONAL path: a user
can store their own Xiaomi MiMo key in the browser and synthesize voice
client-side against Xiaomi's CORS-open endpoints. The key lives only in
localStorage and is never sent to or logged by our server; the shared server
key still serves everyone who does not opt in.
- components/TtsKeyModal.tsx: shared key modal (key-family + region picker),
reused by both the home and play pages
- app/play/page.tsx: silence nudge moved beside the mute toggle; modal opens
in place instead of redirecting to the home page
- app/page.tsx: home page consumes the shared modal + readStoredTtsConfig
- lib/clientTtsConfig.ts, lib/ttsPresets.ts: browser config + region presets
- app/api/{start,scene,insert-beat}: thread per-request voice; lib/types update
- docs/xiaomi-tts-key.md + README note
Verified with tsc --noEmit (exit 0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address the Copilot review on #26.
#1 The game_start / art_style_select payload fields were typed as bare
`string`, so free text could still slip through despite the "content-free
by construction" claim. Add lib/options.ts as the single source of truth
for the selector option sets (`as const` → literal-union types), have the
home OPTS render from those arrays, and type the analytics fields from the
derived unions (gender/art_style/plot_style/pacing/style) plus a template
type for `card`. Free text now fails to compile; no casts at call sites.
#2 The /play heartbeat scheduled its 30s interval unconditionally. Gate the
effect on the same NEXT_PUBLIC_UMAMI_* env used for script injection, so
nothing is scheduled when the tracker is off (visibility check kept — a
hidden tab still never emits).
#3 choice_select no longer emits a -1 choice_index: skip the event when the
index can't be resolved instead of polluting the index distribution.
Verified with tsc (exit 0) and a throwaway negative test: free text in any
of the six fields raises TS2322, valid enum/template values compile.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Instrument the play flow with 9 content-free custom events (game_start,
art_style_select, style_image_upload, scene_reached, choice_select,
vision_click, tts_toggle, fullscreen_toggle, play_heartbeat) to measure
retention, engagement depth and session duration.
Privacy is enforced by construction, not convention:
- lib/analytics.ts types each event with a discriminated union, so a
payload has no slot for free text — prompts, world guides, uploaded
images and vision output can never reach analytics (compile-time
guarantee, not a comment).
- track() no-ops without window.umami and never throws into the app.
- coarse 30s heartbeat fires only while the tab is visible.
- script stays gated on NEXT_PUBLIC_UMAMI_* env (blank → no script),
honours Do-Not-Track, and locks to an exact data-domains allowlist.
- one-line on-site disclosure with a link, shown only when tracking is on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two coordinated additions to the 绘画风格 modal so the user can shape
the styleGuide that ultimately feeds every painter/director agent,
without ever mutating the source-of-truth STYLE_MAP:
1. New "自定义" entry sits right under "自动" — opens an inline
textarea where the user can write a free-form styleGuide (mix of
Chinese / English, sent verbatim to the image model). Stored as
in-memory state on HomePage (customStyleGuide), so refresh clears
it — fits the "one-shot session" semantics of this UI.
2. Every preset card now exposes a small pencil on the right of its
prompt area. Clicking it inlines a textarea pre-filled with the
current effective prompt (override if any, else STYLE_MAP value).
Saving writes to styleOverrides[name] — a separate in-memory
record keyed by preset name. STYLE_MAP is never written to.
start() selects the styleGuide with this priority:
customStyleGuide (when 自动→自定义)
> styleOverrides[artStyle]
> STYLE_MAP[artStyle]
> STYLE_MAP[DEFAULT_STYLE]
UX polish in the same change:
- 标题永远只读 (only the prompt is editable)
- 只读 prompt 行去掉边框/底色,回归纯文字 + 右上铅笔
- 「自动」项无 prompt 可编辑,标题下直接放一行说明
- 编辑态 textarea 用 ember 边框作为"正在编辑"视觉反馈
- 「保存并选用」一并 onPick + close;「还原默认」清除该预设的 override
- 搜索框同时匹配标题/原名/prompt 内容
- 移除「自由输入」标签 (now visually redundant with the pencil affordance)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
m14 (极简杀机) is currently a 14.7KB placeholder while m18 (数据幽灵)
got a real curated cover this round — promote 数据幽灵 into the front
row and demote 极简杀机 back to its original neighborhood so the visible
首屏 only shows finished art.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coupled changes so the user's preferred male cards (复古未来梦,
社团存亡日, 黄昏归途, 极简杀机, 辐射新娘, 霓虹义体, 月光下的约定,
花魁的刀) actually appear in the visual front row:
1. Add a DISPLAY_ORDER indirection. STORIES, covers (m{i}.webp),
prebaked first-acts (firstact/m{i}.json) and prompts.json are all
keyed on the original array index — renaming them would touch
dozens of static assets. DISPLAY_ORDER instead lets the homepage
iterate cards in a curated order while still resolving each card's
assets via its original index. Editing one line re-shuffles the
gallery.
2. Switch the gallery wrapper from CSS multi-column (columns-N) to
grid (grid-cols-N). columns fills column-first (top-of-col-1, then
bottom-of-col-1, then top-of-col-2...) so the first eight entries
of DISPLAY_ORDER ended up stacked down the leftmost column instead
of across the top row. Grid fills row-first, which is what "visual
front row" actually means. Cards are already fixed at aspect-ratio
4/5 so row heights stay uniform — no masonry effect lost.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related fixes so the home start button actually reflects what the
user sees:
1. Lift the Typewriter's current phrase index up to HomePage so start()
can read which example is on screen right now. When the textarea is
empty, start() now substitutes that phrase as the user's story seed —
"what you see is what you play", instead of the previous behavior
where an empty input produced a generic worldSetting with no plot
direction and the model invented something unrelated.
2. Restructure the worldSetting string so the user prompt (or the
chosen Typewriter phrase) sits at the top, alone, wrapped in a
strong directive ("必须以此为剧情主线,不要偏离"). Before, the seed
was a single line sandwiched between the gender/style/pace boilerplate
and the generic "edit with dramatic tension" tail, which the Architect
tended to skim past when expanding the bible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After bed4dc5 renamed style keys to include the (Image N参考) suffix,
the home start() still resolved 「自动」 against the legacy bare name
「京阿尼细腻日常」, leaving styleGuide undefined and tripping the
/api/start required-field check on the default click.
Fall back to "Galgame CG 梦幻光影" — a key that actually exists in
STYLE_MAP — so the default path resolves cleanly without changing the
behavior of explicitly selected styles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Regenerate 60 covers (30 male + 30 female) via FLUX with story-specific
prompts, replacing the prior gender-shared set
- Crop covers to 4:5 (960×1200) via sharp attention cover; matches new
homepage card aspectRatio
- Persist all 60 prompts to public/home/prompts.json so the prebake step
can reuse the cover's exact visual anchor (per-card styleGuide) and the
first-act scene visually carries over from the poster the player clicked
- Restore /play?card= prebaked instant-play path on homepage card click
- Add OpenAI-compatible image route in ai-client for non-Runware endpoints
- Hide Next.js dev indicators globally; tweak F-key fullscreen label
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cookieless, env-gated page-view tracking via Umami. The <Analytics />
component injects the script only when NEXT_PUBLIC_UMAMI_SRC and
NEXT_PUBLIC_UMAMI_WEBSITE_ID are both set, so local dev and forks send
nothing to our instance. Adds .env.example docs (section 6) and a
homepage footer privacy disclosure. No Cookie consent banner needed.
Flatten the pnpm monorepo (apps/web + packages/*) into a single web package at the repo root.
- Move app/lib/components/scripts/public to root; drop apps/web and packages/* wrappers
- Rewrite tsconfig paths (@infiplot/*) to ./lib/*; turbopack.root = __dirname
- Update Vercel (no root-directory) and Cloudflare (pnpm build:cf at root) deploy paths
- Regenerate pnpm-lock.yaml to drop stale workspace importers
- Bump engines.node to >=22 to match wrangler
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>