Commit Graph

15 Commits

Author SHA1 Message Date
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
DESKTOP-I1T6TF3\Q b805b1d9c2 fix(play): scene image renders progressively from top → CF Worker proxy
Symptom: in Chrome on certain networks the scene <img> renders row-by-row
from top to bottom — "层层加载" — instead of appearing atomically.

Root cause (confirmed via DevTools):
  - Chrome opportunistically opens HTTP/3 (QUIC) to im.runware.ai.
  - QUIC streams to Runware sometimes error mid-transfer:
      net::ERR_QUIC_PROTOCOL_ERROR
    HTTP-level status stays 200 (response headers received), but bytes are
    truncated. The browser paints whatever PNG bytes it has so far → visible
    row-by-row decode.
  - The earlier preloadImage()+decode() trick can't fix this — neither
    HTTP-cache reuse nor sync decode helps when the bytes themselves were
    never fully delivered.

Two-tier fix:

1. Client: fetch → Blob → URL.createObjectURL() (app/play/page.tsx)
     - <img src> only ever points to a blob: URL whose bytes are 100%
       resident in the JS heap. No network-backed src = no possibility of
       progressive paint.
     - Module-level blobUrlCache keys by original URL so speculative
       prefetch + the eventual commit share one fetch.
     - Old blobs are URL.revokeObjectURL()'d on scene swap + unmount to
       release memory.

2. Network: optional Cloudflare Worker proxy (worker/)
     - Browser ↔ Worker is HTTP/2 over CF edge (extremely stable).
     - Worker ↔ Runware is a server-to-server fetch (no QUIC fragility,
       Cloudflare's backbone handles transit).
     - Worker buffers the full upstream response → client never sees a
       half-stream.
     - Bonus: CF edge cache (cacheEverything, 1y TTL) on Runware UUIDs;
       Access-Control-Allow-Origin: * so client fetch() can't hit CORS.
     - Hardened: only proxies im.runware.ai, only GET/HEAD/OPTIONS, all
       other hosts/methods → 403/405.

Wired via NEXT_PUBLIC_IMAGE_PROXY_URL (inlined at build). Empty → no proxy
→ direct fetch (which still uses the blob path, just exposed to QUIC).

──────────────────────────────────────────────────────────────────────
Deploy steps (one-time, do this AFTER pulling this commit):

  1. Install wrangler globally:
       npm i -g wrangler

  2. Log in to Cloudflare (opens browser for OAuth):
       wrangler login

  3. From the worker/ directory, deploy:
       cd worker
       wrangler deploy

     wrangler will print the deployed URL, e.g.
       https://infiplot-image-proxy.<your-cf-username>.workers.dev

  4. Paste that URL into .env.local for local dev:
       NEXT_PUBLIC_IMAGE_PROXY_URL=https://infiplot-image-proxy.<...>.workers.dev
     …and into Vercel project settings (Environment Variables) for prod.
     NEXT_PUBLIC_ vars are inlined at build time, so the URL bakes into
     the bundle on the next deploy/dev-server restart.

  5. Restart dev server (pnpm dev) so the new env baked in. Generate a
     scene; Network tab should show requests going to *.workers.dev
     instead of im.runware.ai, no ERR_QUIC_PROTOCOL_ERROR, image renders
     atomically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 22:50:48 +08:00
DESKTOP-I1T6TF3\Q 347ab297d5 feat(web,engine): custom style — image upload, AI-extract prompt, painter ref
自定义画风入口里加上传按钮:客户端把图缩到 512px webp(base64),传到新
路由 /api/parse-style-image,vision LLM 解析成英文 style prompt 回填 textarea;
图本身随 sessionStorage → /api/start → Session.styleReferenceImage 透传,
painter.collectReferenceImages 把它置于 slot 0,整局每一幕都作为 reference
图锚定画风(brush / color / mood),比 priorScene 优先级更高。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 19:15:19 +08:00
yuanzonghao 3fa3da5378 chore(play): remove session-id readout and decorative footer mark
The session-id slice shown in the play header was an opaque timestamp
that reads as noise to players. The footer's "Ⅰ · Ⅰ" was a leftover
decorative mark after its sibling controls were moved above the canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:00:16 +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
DESKTOP-I1T6TF3\Q ea861b7c99 feat(web): style modal — custom prompt + per-preset override, inline edit
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>
2026-06-03 07:24:42 +08:00
Zonghao Yuan a333b21759 feat(web): add favicon (IP monogram SVG) (#18)
站点此前没有 favicon,浏览器显示默认地球图标。新增 app/icon.svg:
站点同款 Cormorant Garamond 衬线的「IP」字母组合 —— I 直立 (clay #2D1810)、
P 斜体 (ember #D97A2E),米白圆角底,呼应 README 的 wordmark。字形以矢量轮廓
内嵌(从 Cormorant wght 600 / italic 400 实例化抽取),不依赖 webfont,各处渲染一致。
Next.js 16 见到 app/icon.svg 会自动注入 <link rel="icon">,无需改动 layout.tsx。

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:23:34 +08:00
DESKTOP-I1T6TF3\Q 1bcea098d5 chore(web): swap 数据幽灵 and 极简杀机 positions in male DISPLAY_ORDER
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>
2026-06-03 04:13:51 +08:00
DESKTOP-I1T6TF3\Q c90929ce4b feat(web): pin curated male cards to the front + row-first grid layout
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>
2026-06-03 04:11:26 +08:00
DESKTOP-I1T6TF3\Q d241300ed6 fix(web): fall back to current Typewriter phrase + frontload it in worldSetting
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>
2026-06-03 03:51:51 +08:00
DESKTOP-I1T6TF3\Q eb9b875454 fix(web): use existing STYLE_MAP key for home 「自动」 fallback
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>
2026-06-03 03:37:49 +08:00
DESKTOP-I1T6TF3\Q bed4dc5a8f feat(web): gender-differentiated 4:5 covers + per-card styleGuide prebake
- 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>
2026-06-03 02:26:35 +08:00
DESKTOP-I1T6TF3\Q 820a5f7e87 feat(web): refactor home preset cards to 16:9 poster style with titles and tags below cover 2026-06-03 02:25:02 +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