Commit Graph

30 Commits

Author SHA1 Message Date
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 0abd5f1525 feat(play): add encrypted story sharing 2026-06-07 17:13:27 +08:00
yuanzonghao 4972243a93 fix: address PR Agent review findings across 6 files
Restrict PR Agent workflow to trusted collaborators on PR comments only,
fix UTF-8 byte counting in gallery-pack, correct portrait-to-landscape
fallback orientation, track inserted freeform beats in visitedBeatIds,
allow clearing stored TTS key, and guard empty-string fuzzy match in
style selector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 14:40:37 +08:00
yuanzonghao dc36b1fe9e feat(play): integrate vision click with unified settings modal
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>
2026-06-07 14:15:22 +08:00
yuanzonghao ae3dd17e6b feat(web): add player name, freeform input, and unified settings modal
- 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>
2026-06-07 12:37:50 +08:00
DESKTOP-I1T6TF3\Q b0b5630a25 feat(web): export interactive gallery + encrypted share file
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>
2026-06-07 12:08:37 +08:00
baizhi958216 5a7daa8452 feat(play): add history dialog
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-06 20:52:10 +08:00
yuanzonghao e88e988de3 fix(web): reduce FOT by stripping redundant voice data from transport
Three transport-only optimizations that cut per-session Vercel FOT by ~50-60%:

P0 — Server strips voice.referenceAudioBase64 from already-known characters
in /api/scene and /api/insert-beat responses (defense-in-depth).

P1 — Client strips all voice data from session before sending to
/api/scene, /api/vision, and /api/insert-beat. Voices are retained locally
and re-merged from responses via mergeCharactersPreserveVoice(). The engine
only needs character names + visualDescriptions for scene generation.

P3 — /api/beat-audio returns binary audio (Response with Content-Type)
instead of JSON-wrapped base64, saving ~33% encoding overhead. Client
converts to blob URLs; PlayCanvas accepts a single audioSrc prop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-05 00:24:34 +08:00
yuanzonghao 9fc83de276 feat(web,engine): portrait-orientation scene images for mobile full-bleed
Thread orientation (portrait|landscape) from client through API, engine,
and image gen. Portrait devices render 1024x1792 (9:16) full-bleed scenes;
desktop/landscape keeps 1792x1024 (16:9). Adds cover-aware click→image
coordinate mapping, session-locked orientation, a shared coerceOrientation
helper, and a choices overflow cap in portrait.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:30:54 +08:00
yuanzonghao f6226facbd fix(web): address PR #28 review — explicit clientTts boolean + BYO key prefix hint
Harden the BYO-mode signal at the API boundary (start/scene/insert-beat):
only clientTts === true drops server TTS, so a stray truthy non-boolean can't
silently disable it. Add a non-blocking prefix hint in TtsKeyModal that warns
when the pasted key prefix (tp-/sk-) mismatches the selected key type — a
mismatch hits the wrong endpoint and plays silently, the symptom BYO fixes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 16:58:55 +08:00
yuanzonghao b0b2e922d3 feat(web): optional bring-your-own Xiaomi MiMo TTS key (browser-side synthesis)
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>
2026-06-04 16:58:55 +08:00
DESKTOP-I1T6TF3\Q 592c82816a Revert "feat(loading): support typewriter story teaser during first scene generation"
This reverts commit 4e4e06ec8a.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q 587e1e4e7d Revert "fix(loading): use left-aligned text for typewriter teaser to prevent jitter"
This reverts commit e875ac8fd7.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q 3f45cd4e0f Revert "fix(loading): set w-full on teaser container to prevent horizontal shifting on first line"
This reverts commit 68999aca2a.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q d19baa2127 Revert "feat(loading): hide footer text when teaser appears and apply pulse animation to teaser text when typing completes"
This reverts commit 5e1a4656ed.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q a311c24f70 Revert "feat(loading): delay teaser slow-pulse animation by 1s after typewriter ends"
This reverts commit 1ac665ad88.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q 589bb31416 Revert "feat(loading): slow down teaser typing speed to 65ms and change fallback text to " 请等待\"
This reverts commit 05d9060dc2.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q a1f3750b6f Revert "feat(loading): make teaser title pulse together with body"
This reverts commit 7164c05b4e.
2026-06-04 15:13:03 +08:00
DESKTOP-I1T6TF3\Q 7164c05b4e feat(loading): make teaser title pulse together with body 2026-06-04 15:03:50 +08:00
DESKTOP-I1T6TF3\Q 05d9060dc2 feat(loading): slow down teaser typing speed to 65ms and change fallback text to " 请等待\ 2026-06-04 15:00:50 +08:00
DESKTOP-I1T6TF3\Q 1ac665ad88 feat(loading): delay teaser slow-pulse animation by 1s after typewriter ends 2026-06-04 14:58:57 +08:00
DESKTOP-I1T6TF3\Q 5e1a4656ed feat(loading): hide footer text when teaser appears and apply pulse animation to teaser text when typing completes 2026-06-04 14:56:06 +08:00
DESKTOP-I1T6TF3\Q 68999aca2a fix(loading): set w-full on teaser container to prevent horizontal shifting on first line 2026-06-04 14:51:12 +08:00
DESKTOP-I1T6TF3\Q e875ac8fd7 fix(loading): use left-aligned text for typewriter teaser to prevent jitter 2026-06-04 14:49:42 +08:00
DESKTOP-I1T6TF3\Q 4e4e06ec8a feat(loading): support typewriter story teaser during first scene generation 2026-06-04 14:40:35 +08:00
yuanzonghao 4bf05f6784 feat(web): add privacy-friendly Umami custom events
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>
2026-06-04 10:14:08 +08:00
yuanzonghao a18b91c48c fix(play): story-card clicks no longer trigger vision
Symptom: on a choice beat, clicking the dialogue/narration card fired
the vision ("识图") flow instead of doing nothing. Picking an option with
fast clicks that landed on the card repeatedly kicked off the expensive
/api/vision → insert-beat/scene chain — janky and confusing.

Root cause: the story-card <div> had `pointer-events-none`, so clicks
passed through to the background <img> onClick (handleImageClick), which
on choice beats calls onBackgroundClick → vision.

Fix: the card now owns its clicks (`pointer-events-auto` + handleCardClick):
  - mid-typing   → completes the text (VN skip affordance, unchanged)
  - continue beat → advances, as before
  - choice beat  → no-op (no vision)
Clicking the actual scene art still triggers vision; choice buttons
already had pointer-events-auto and are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:17:30 +08:00
DESKTOP-I1T6TF3\Q b5f73d8082 fix(play): scene image renders as 1px sliver while CDN bytes still arrive
When the Runware CDN download was slow (~10-20s over VPN / strict
networks, vs. the optimistic <2s the existing comment assumed), the
preload's 8s timeout fired and setImageUrl committed before the bytes
were actually decoded. The rendered <img> has w-auto h-auto and no
intrinsic aspect-ratio source — until the image loads the layout
collapses to roughly 1px tall, giving the "等了很久 → 一根线 → 突然
出图" jank.

Two compounding fixes:

  app/play/page.tsx       IMAGE_PRELOAD_TIMEOUT_MS  8000 → 20000.
                          Real CDN+decode usually finishes well before
                          this; pushing the ceiling out just stops the
                          window where we commit a half-loaded URL.

  components/PlayCanvas.tsx  Add width={1792} height={1024} HTML attrs
                          to the scene <img>. Doesn't affect rendered
                          size (still driven by w-auto h-auto and the
                          maxWidth/maxHeight in sizeStyle); the
                          browser uses them purely as an intrinsic
                          aspect-ratio source, so the placeholder box
                          reserves a 16:9-ish frame even mid-download.

Together: slow networks now mostly wait through preload; on the rare
genuine timeout the layout still holds shape instead of collapsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 07:24:42 +08:00
Zonghao Yuan 6ddbe7d377 feat: add privacy-friendly Umami page-view analytics (#15)
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.
2026-06-03 01:14:55 +08:00
Zonghao Yuan dc5ecd60f6 refactor: flatten monorepo to single web package (#12)
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>
2026-06-03 00:55:45 +08:00