Skip to content

feat(everwith): add independent Claude frontend rebuild (local mock) - #26

Open
lishehao wants to merge 1 commit into
mainfrom
claude/everwith-frontend-rebuild
Open

lishehao wants to merge 1 commit into
mainfrom
claude/everwith-frontend-rebuild

Conversation

@lishehao

Copy link
Copy Markdown
Owner

这是什么

新增独立目录 everwith-claude-frontend/:依据 2026-10-13 的 Everwith 私人交接包重建的桌面网页前端,可以独立运行。仓库里其他目录一个字节都没改(frontend/、frontend2/、rpg_backend/ 等都不受影响)。

  • React 19 + Vite 7 + TypeScript strict;所有服务都是浏览器内的本地 mock,没有接 Luna / JEV / TTS / 账号 / 存储,页面上有“本地演示”标识。
  • 功能:好友与一对一私聊(流式分段、停止、可重试、结果未知先查原请求、旧流晚到丢弃、回退/纠错/重命名/重置/移除)、五位角色 Live2D、三款桌游(Royale / Splendor / CABO,准备 → 完整对局 → 结算 → 原桌再开)、故事(准备 → 游玩 → 回退 → 局中加好友)、最近、创作(类型 → 编辑 → 隔离试演 → 保存版本 → 显式分享)、账号与 Persona。
  • 交接包素材(角色立绘、Live2D 模型、Cubism SDK、牌桌美术)不在 git 里:npm run assets:sync -- --from <交接包目录> 会按字节复制并核对 sha256,缺图时降级为同一角色静态图 / 中性占位。
  • public/generated/batch-01/:46 个 GPT 生成图的缩放副本(故事封面与场景、角色房间背景、游戏封面、状态插画、作品封面、Persona 头像、图标)。权利标签:用户经 GPT 生成并提供;商用/再分发权未核实。来源、哈希、prompt 见 docs/GENERATED_ASSETS.json / .md。

怎么跑

cd everwith-claude-frontend
npm ci
npm run assets:sync -- --from "<path>/Everwith_Claude_Frontend_Handoff_2026-10-13"
npm run dev                       # http://127.0.0.1:5173
npm run check                     # tsc + 54 项单元测试
npx playwright install chromium && npm run test:e2e   # 29 项,约 45 分钟

验证(同一份应用代码,sha256 fde68002…,见 SOURCE_MANIFEST.txt)

  • 单元/集成 54/54;端到端 29/29(Playwright + headless Chromium + SwiftShader 软件 WebGL);干净目录复现通过,构建产物与测试所用构建逐字节相同。
  • 验收逐项见 ACCEPTANCE.md:12 PASS(mock) / 5 PASS(local) / 7 PARTIAL / 0 FAIL,没有 PASS-real。
  • 截图、三局完整录屏、证据包放在 Dropbox Everwith_Claude_Frontend_Rebuild_2026-10-13/,没有放进 git。

Review 建议从这里看

  • src/api/adapter.ts、src/api/client.ts:mock / real 适配层与响应校验
  • src/mock/server/*:本地模拟服务端(账号、好友、私聊流、故事、桌游权威状态、创作)
  • src/chat/stream-controller.ts:私聊流控制(generation / receipt / retry / lookup)
  • src/live2d/*:运行时、单 RAF 导演、舞台组件
  • src/features/games/*、src/games/engines/*:三款牌桌;引擎与交接包逐字节一致(PROVENANCE.json)
  • docs/GENERATED_ASSETS.md:生成素材用在哪里

已知缺口

真实中文输入法人工走查、口型与真实音频同步、故事内纠错、全部真实服务接入、真实 GPU 下 Live2D 观感、移动端、独立验收。

对 CI 的影响

现有 CI 只跑 backend(pytest)和 frontend2(tsc + build);新目录没有接入 CI,也不影响这两个任务。

🤖 Generated with Claude Code

https://claude.ai/code/session_01P2RywNAaCUZbaHNhH3So7E


Generated by Claude Code

Adds everwith-claude-frontend/: an independent, runnable desktop frontend
rebuilt from the 2026-10-13 private Everwith handoff package. Nothing outside
this directory changes; frontend/, frontend2/ and rpg_backend/ are untouched.

- React 19 + Vite 7 + TypeScript strict. Every service is an in-browser local
  mock (no real Luna / JEV / TTS / account / storage connection).
- Private chat, five Live2D characters, three board games (Royale, Splendor,
  CABO), stories, recent, creation studio, account.
- Handoff assets (portraits, Live2D models, Cubism SDK, game art) are not in
  git: `npm run assets:sync -- --from <handoff>` copies and hash-checks them.
- public/generated/batch-01: 46 resized copies of the user-provided GPT image
  batch v2-batch-01 (user-provided GPT output; commercial / redistribution
  rights unverified). Provenance: docs/GENERATED_ASSETS.json.
- Verified on this exact app code (sha256 fde68002…): 54/54 unit, 29/29 E2E
  (Playwright, headless Chromium + SwiftShader), clean-room reproduction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2RywNAaCUZbaHNhH3So7E
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T09:37:59.917618Z f492ce8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f492ce8257

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


let cache: DB | null = null
export function db(): DB {
if (cache) return cache

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reload shared state before writing from another tab

When two tabs have each called db(), both retain independent cached copies indefinitely; reloadDB() is never used and save() serializes the entire stale object. A later mutation in either tab therefore overwrites unrelated chats, games, stories, or works saved by the other tab, causing real cross-tab progress loss rather than the intended stale-revision conflict handling.

Useful? React with 👍 / 👎.

Comment on lines +96 to +99
useEffect(() => {
if (sync !== 'pending') return
const timer = setTimeout(() => { void flush() }, 700)
return () => clearTimeout(timer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Flush pending edits before cancelling the debounce

If a user edits a work and follows the Back link or otherwise leaves within the 700 ms debounce window, this cleanup cancels the only scheduled flush() and there is no unmount or navigation flush. The draft exists only in component memory at that point, so returning to the work silently loses the latest edits.

Useful? React with 👍 / 👎.

Comment on lines +82 to +83
const flush = useCallback(async () => {
if (flight.current || !dirty.current || !draftRef.current) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for an active draft upload before saving

When an autosave request is already in flight, flush() returns immediately, so save() and startPreview() proceed using the old base.current. If the user edits during that request and immediately saves or previews, the immutable version can capture the previous draft or the command can fail with a revision conflict even though the UI just requested a flush; the active upload needs to be awaited and any newly dirty draft flushed afterward.

Useful? React with 👍 / 👎.

Comment on lines +142 to +143
if (!want) { missing.push({rel, reason: 'not listed in SHA256SUMS'}); continue; }
if (!existsSync(source)) { missing.push({rel, reason: 'absent in handoff copy'}); continue; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove stale assets when the new handoff omits them

When syncing over an existing installation with an incomplete handoff, missing entries are merely recorded and the command still succeeds, while files copied by an earlier run remain at their destinations. Because the app loads those destination URLs directly, it uses stale assets instead of the documented missing-asset fallback, and the new lock file no longer checks those leftover files.

Useful? React with 👍 / 👎.

Comment on lines +91 to +93
f.epoch++; f.revision++
for (const c of Object.values(d.conversations)) if (c.friend_id === f.id && c.friend_epoch < f.epoch) c.status = 'read_only'
for (const s of Object.values(d.stories)) if (s.cast.some(r => r.friend_id === f.id)) s.status = 'read_only'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retire pending operations when resetting a friend

If Reset is confirmed while that friend's chat reply or story turn is still pending, these assignments make the old session read-only but do not cancel or stale its pending operation. The background tickers subsequently commit the reply or story turn anyway, so a supposedly frozen old conversation/run continues mutating after the epoch change; Remove has the same problem for chat operations.

Useful? React with 👍 / 👎.

Comment on lines +44 to +48
// The account may finish loading after the first render (reload): pick up its saved draft then.
useEffect(() => {
const saved = readJSON<string>(draftKey(accountId, sessionId), '')
if (saved) setDraftState(current => current || saved)
}, [accountId, sessionId])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Migrate story drafts when guest ownership changes

When a guest signs in, the story session is reassigned to the account but its composer draft remains stored under the guest-scoped key. This effect only reads the new account key and never copies the current or guest draft into it, so signing in and then refreshing or revisiting the merged run makes the unsent draft disappear.

Useful? React with 👍 / 👎.

Comment on lines +228 to +232
/** A frame threw (director keeps running). Many in a row → give up to the static fallback. */
private frameErrorRun = 0
noteFrameError(error: unknown) {
this.stats.lastError = error instanceof Error ? error.message : String(error)
if (++this.frameErrorRun > 90 && this.status === 'ready') { this.status = 'failed'; this.error = 'frame_failed' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate runtime failure to the static fallback

After repeated draw failures this changes only ModelRuntime.status; LiveStage copies the status into React state only when the initial load finishes and never observes later changes. Consequently the canvas remains selected and the same-character static image stays hidden after frame_failed (and similarly after a failed context restore), producing a permanently blank or frozen stage instead of the fallback promised by this comment.

Useful? React with 👍 / 👎.

Comment on lines +41 to +45
setSeats(prev => Array.from({length: players - 1}, (_, i) => {
if (prev[i]) return prev[i]
if (i === 0 && withFriend) {
const f = friends.find(x => x.id === withFriend)
if (f) return {kind: 'friend', friend_id: f.id, friend_epoch: f.epoch}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed the requested friend after the friend list loads

On a refreshed or directly opened gamePrepare link with ?with=<friend>, the game catalog normally resolves before the authenticated friend list, so this effect first fills seat 1 with a temporary character. When friends later arrive, prev[i] causes that temporary choice to be retained and the requested friend is never seated, silently breaking the Play Together/deep-link intent.

Useful? React with 👍 / 👎.

Comment on lines +30 to +31
// Same-source friends are separate people: listed with their own names and dates.
const same = (friends ?? []).filter(f => f.source_id === character.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match existing friends by source version

For a user-created character with multiple saved versions, this filter treats friends created from every version as matches for the currently displayed version. Visiting a v2 character page can therefore offer a chat with a v1-derived friend and list that older identity as if it came from v2, despite friend identity elsewhere being defined by both source_id and source_version.

Useful? React with 👍 / 👎.

Comment on lines +22 to +23
const load = useCallback(() => { setError(false); api.records.list(archived).then(r => setItems(r.items)).catch(() => setError(true)) }, [archived])
useEffect(() => { setItems(null); load() }, [load])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore record-list responses from the previous tab

If the user switches between Active and Archived while the prior /records request is still pending, both promises can call setItems because there is no cancellation or generation check. A slower response for the previous tab can then replace the new tab's rows, showing active records under Archived or vice versa until another reload.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants