Skip to content

Upload vrm file#232

Open
MorevPro wants to merge 5 commits intosemperai:masterfrom
MorevPro:upload-vrm-file
Open

Upload vrm file#232
MorevPro wants to merge 5 commits intosemperai:masterfrom
MorevPro:upload-vrm-file

Conversation

@MorevPro
Copy link

@MorevPro MorevPro commented Feb 22, 2026

upload VRM model to local storage IndexedDB

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Russian language localization to the user interface
    • Enhanced VRM character model file processing with improved handling and storage workflows
  • Bug Fixes

    • Improved VRM animation transitions to prevent runtime errors during model playback
  • Chores

    • Updated project configuration to exclude IDE-specific directories

@vercel
Copy link

vercel bot commented Feb 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
amica Error Error Feb 22, 2026 9:55pm

Request Review

@vercel
Copy link

vercel bot commented Feb 22, 2026

Someone is attempting to deploy a commit to the heyamica Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

The changes introduce Russian language support, enhance VRM file management through asynchronous store operations, and add browser console helpers for IndexedDB debugging. Updates include new props for VRM file handling, async method signatures, a new appendVrm action, and validation utilities.

Changes

Cohort / File(s) Summary
Internationalization
i18next-parser.config.mjs, src/i18n/langs.ts, src/i18n/locales/ru/common.json
Added Russian (ru) locale support: expanded parser config to include 'ru', imported Russian translations in langs.ts, and created new Russian locale file with 151 translation key-value pairs.
VRM Store Infrastructure
src/features/vrmStore/vrmDataProvider.ts, src/features/vrmStore/vrmStoreContext.tsx, src/features/vrmStore/vrmStoreReducer.ts
Refactored VRM store to use asynchronous flows: addItem now returns Promise, introduced addVrmFromStored context method, added appendVrm action type, and converted reducer logic to async with enhanced logging and IndexedDB integration.
VRM Settings & UI
src/components/settings.tsx, src/components/settings/CharacterModelPage.tsx, src/components/settings/common.tsx
Updated VRM file input handling: replaced handleClickOpenVrmFile with onPickVrmFile(file: File), added processVrmFile logic, introduced hashCodeLarge utility for efficient hashing, and updated accepted MIME types for VRM files.
VRM Viewer
src/features/vrmViewer/model.ts, src/features/vrmViewer/viewerContext.ts
Added nullability guards for animation transitions: made destAction optional in fadeToAction, exported viewer instance, and added early-return handling for undefined actions.
VRM Console Helpers
scripts/console-indexeddb-vrm-helper.js, src/utils/vrmIndexedDBConsoleHelper.ts, src/pages/_app.tsx
Created development console helpers for IndexedDB VRM management: added browser helper script for direct console operations (list, addTest, addFromFile, pickAndAdd), implemented utility module with applyAndLoad functionality, and wired attachment to App component on mount.
Configuration & Build
.gitignore
Added IDE-related ignore entries: .idea/ and .vscode/ directories.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Settings as Settings Component
    participant Reducer as VRM Store Reducer
    participant Provider as VRM Data Provider
    participant DB as IndexedDB<br/>(AmicaVrmDatabase)

    User->>Settings: Select VRM file
    Settings->>Settings: processVrmFile(file)
    Settings->>Settings: hashCodeLarge(content)
    Settings->>Reducer: addFile(hash, blob)
    Reducer->>Reducer: Check hash exists
    alt Hash not found
        Reducer->>Reducer: Create VrmData<br/>(hash, url, thumb, local)
        Reducer->>Provider: addItem(hash, vrmData)
        Provider->>DB: put(record)
        DB-->>Provider: success
        Provider->>Provider: log success
        Provider-->>Reducer: Promise resolved
        Reducer->>Reducer: Append to vrmList
        Reducer-->>Settings: Callback with updated list
    else Hash exists
        Reducer-->>Settings: Return current list
    end
Loading
sequenceDiagram
    actor Dev as Developer
    participant Console as Browser Console
    participant Helper as VRM Console Helper
    participant DB as IndexedDB<br/>(AmicaVrmDatabase)
    participant Viewer as VRM Viewer

    Dev->>Console: window.__vrmIndexedDB.pickAndAdd()
    Console->>Helper: pickAndAdd()
    Helper->>Helper: File picker
    Dev->>Helper: Select .vrm file
    Helper->>Helper: addFromFile(file)
    Helper->>Helper: Read as data URL
    Helper->>Helper: hashCodeLarge(content)
    Helper->>DB: put(hash, vrmData)
    DB-->>Helper: success
    Helper-->>Console: {hash, size}
    Dev->>Console: window.__vrmIndexedDB.applyAndLoad(hash)
    Console->>Helper: applyAndLoad(hash)
    Helper->>DB: get(hash)
    DB-->>Helper: vrmData record
    Helper->>Helper: updateConfig(vrm_hash, vrm_url)
    Helper->>Viewer: Load VRM
    Viewer-->>Helper: VRM loaded
    Helper-->>Console: success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hare bounces through IndexedDB,
Storing VRMs with hash and glee,
Async flows now gracefully cascade,
Russian words in files newly made,
Console helpers spring to life,
Making VRM management less strife! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Upload vrm file' accurately reflects the main objective of the PR, which is to enable users to upload VRM model files to local storage IndexedDB. This is the primary feature being introduced across the codebase changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (13)
.gitignore (1)

46-47: Consider a more granular .vscode/ ignore rule.

Ignoring .vscode/ entirely will also suppress potentially useful committed workspace configs like extensions.json (recommended extensions) or launch.json (shared debug configs). A common convention is to ignore only personal/machine-specific files:

🔧 Suggested refinement
 # IDEs
 .idea/
-.vscode/
+.vscode/*
+!.vscode/extensions.json
+!.vscode/launch.json
+!.vscode/settings.json

This is entirely optional — if the team has no intention of committing any VS Code workspace files, the blanket ignore is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 46 - 47, The .gitignore currently excludes the
entire .vscode/ directory; instead refine it to ignore only
personal/machine-specific VS Code files so shared workspace configs (e.g.,
extensions.json, launch.json) can be committed. Update the ignore rules for the
.vscode folder to exclude transient/user-specific patterns (like workspace
storage, *.code-workspace user files, settings.local) while adding explicit
negation entries to allow shared files such as .vscode/extensions.json and
.vscode/launch.json to be tracked; reference the existing .vscode/ entry in the
diff and replace it with the more granular ignore/allow patterns.
src/features/vrmViewer/viewerContext.ts (1)

7-7: Consider dependency injection instead of a direct singleton export.

Exporting viewer as a bare module-level singleton bypasses the React Context mechanism and tightly couples all importers — notably vrmDataProvider.ts and vrmIndexedDBConsoleHelper.ts — to this one fixed instance. If viewer is ever re-created (e.g., unit tests needing a mock, or a future "reset viewer" feature), direct importers will silently retain a reference to the old object rather than picking up the new one through context.

For non-React modules that legitimately cannot call useContext, the cleaner pattern is to receive viewer as a function parameter:

-export { viewer };
-// vrmDataProvider.ts / vrmIndexedDBConsoleHelper.ts
-import { viewer } from "@/features/vrmViewer/viewerContext";
+// Accept viewer as a parameter instead
+export function someAction(viewer: Viewer, ...) { ... }

If the coupling is accepted as a deliberate trade-off (the singleton is intentionally process-scoped), at minimum add a comment on the export explaining that this is an escape hatch for non-React callers, so future maintainers don't accidentally widen its usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmViewer/viewerContext.ts` at line 7, The module currently
exports a bare singleton "viewer" which couples all importers; replace that
export with a controlled accessor pattern: add an
initializeViewer(viewerInstance) function that stores the instance internally
and a getViewer() function that returns the current instance (throwing or
returning undefined if not initialized), then update non-React callers
(vrmDataProvider.ts and vrmIndexedDBConsoleHelper.ts) to accept a viewer
parameter where possible or call getViewer() as an explicit escape hatch; if you
decide to keep a process-scoped singleton instead, add a clear comment on the
export explaining it is an intentional escape hatch for non-React modules to
avoid accidental wider use.
src/features/vrmViewer/model.ts (1)

407-407: idleAction ?? undefined is a no-op — simplify to just idleAction.

idleAction is captured from this._currentAction, which is typed THREE.AnimationAction | undefined. Because the right-hand side of ?? is itself undefined, idleAction ?? undefined always evaluates to idleAction, making the nullish-coalescing expression redundant.

♻️ Proposed simplification
-      this.fadeToAction(idleAction ?? undefined, 1);
+      this.fadeToAction(idleAction, 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmViewer/model.ts` at line 407, The expression idleAction ??
undefined is redundant because idleAction (from this._currentAction) is already
typed as THREE.AnimationAction | undefined; update the call to fadeToAction to
pass idleAction directly (i.e., replace this.fadeToAction(idleAction ??
undefined, 1) with this.fadeToAction(idleAction, 1)) so the nullish-coalescing
is removed; keep references to the same variables and method names (idleAction,
this._currentAction, fadeToAction).
src/i18n/locales/ru/common.json (1)

37-37: "Load VRM" and "Upload VRM" share the same Russian translation.

Both keys map to "Загрузить VRM". While "загрузить" is colloquially used for both actions in Russian, these are semantically distinct operations in the UI (loading a model into the viewer vs. uploading a file to IndexedDB). If both labels appear near each other, Russian users won't be able to distinguish them.

Consider differentiating, e.g.:

-  "Load VRM": "Загрузить VRM",
+  "Load VRM": "Открыть VRM",

leaving "Upload VRM": "Загрузить VRM" for the file upload action.

Also applies to: 121-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/ru/common.json` at line 37, The Russian translation for the
keys "Load VRM" and "Upload VRM" in ru/common.json are identical ("Загрузить
VRM"); change one so the UI distinguishes loading a model into the viewer from
uploading a file to IndexedDB—e.g., keep "Upload VRM" as "Загрузить VRM" and
change "Load VRM" to a distinct phrase like "Загрузить в просмотрщик" or
"Открыть VRM" so users can tell the actions apart; update the "Load VRM" entry
accordingly (affects the "Load VRM" and "Upload VRM" keys).
src/utils/vrmIndexedDBConsoleHelper.ts (2)

119-160: Misleading indentation in doLoad makes the control flow hard to follow.

The body of doLoad (lines 121-159) is indented at the same level as the enclosing applyAndLoad, making it look like the return new Promise(...) belongs to applyAndLoad rather than doLoad. Re-indent the inner function body by one level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/vrmIndexedDBConsoleHelper.ts` around lines 119 - 160, Re-indent the
inner function doLoad so its entire body (the return new Promise(...) block and
nested handlers) is indented one level deeper than applyAndLoad, making it
visually and structurally clear that the Promise belongs to doLoad; locate the
function declaration function applyAndLoad(hash?: string) and the nested const
doLoad = (h: string) => { ... }, then adjust indentation of the doLoad body
(including the indexedDB.open handlers, req.onsuccess/req.onerror, updateConfig
and viewer.loadVrm calls) so it is nested under doLoad rather than aligned with
applyAndLoad.

74-80: Hash computation is duplicated from hashCodeLarge in common.tsx.

The hash algorithm (sample start/end + length) is implemented identically in hashCodeLarge (src/components/settings/common.tsx lines 112-120) and also in scripts/console-indexeddb-vrm-helper.js lines 68-72. Import and reuse hashCodeLarge here to avoid drift between implementations.

♻️ Proposed refactor
+import { hashCodeLarge } from "@/components/settings/common";
 ...
     reader.onload = () => {
       const vrmData = reader.result;
       if (typeof vrmData !== "string") {
         reject(new Error("Ожидалась строка data URL"));
         return;
       }
-      const len = vrmData.length;
-      const S = 100000;
-      let h = 0;
-      for (let i = 0; i < Math.min(S, len); i++) h = ((h << 5) - h + vrmData.charCodeAt(i)) << 0;
-      for (let i = Math.max(0, len - S); i < len; i++) h = ((h << 5) - h + vrmData.charCodeAt(i)) << 0;
-      h = ((h << 5) - h + len) << 0;
-      const hash = String(h);
+      const hash = hashCodeLarge(vrmData);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/vrmIndexedDBConsoleHelper.ts` around lines 74 - 80, Replace the
duplicated hash computation in src/utils/vrmIndexedDBConsoleHelper.ts with a
call to the existing hashCodeLarge function from
src/components/settings/common.tsx: remove the manual hashing loop and import
hashCodeLarge, then compute hash via hashCodeLarge(vrmData) (convert to string
if necessary). Ensure the import matches the exported name/signature of
hashCodeLarge and adjust types/exports if needed so the helper can reuse the
central implementation without redefining the algorithm.
src/features/vrmStore/vrmDataProvider.ts (1)

17-21: LGTM — addItem now properly returns a Promise.

Good change to allow callers to await persistence. One minor note: the success log on every put (line 19) will be noisy in production. Consider gating it behind a debug flag or reducing to console.debug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmDataProvider.ts` around lines 17 - 21, The success
log in addItem (the console.log in the .then after this.db.vrms.put) is noisy;
change it to a lower-verbosity logger or gate it behind a debug flag. Locate the
addItem method and replace the console.log call with console.debug (or wrap it
in a conditional using a debug/verbose flag your app uses) so normal production
runs don’t emit that success message while still allowing developers to enable
it when needed.
src/features/vrmStore/vrmStoreContext.tsx (1)

34-58: Verbose debug logging in production code.

Lines 34, 36, 38, 42, 48, 51, 57-58 add console.log/console.warn/console.error calls with Russian text. These are useful during development but will be noisy in production. Consider using console.debug for non-error messages or gating behind a dev check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreContext.tsx` around lines 34 - 58, Replace
verbose console logging inside the VRM add flow with development-only or
debug-level logs: change non-error console.log/console.warn calls in the
vrmListAddFile handler (the block calling vrmListDispatch, viewer.loadVrm,
updateConfig, viewer.getScreenshotBlob and the inner
vrmListDispatch/updateVrmThumb callbacks) to console.debug or wrap them with a
dev-check (e.g. if (process.env.NODE_ENV !== "production") { ... }) so they are
suppressed in production; keep the catch block using console.error for actual
errors (viewer.loadVrm .catch) but ensure it still logs the error object. Ensure
references to viewer.loadVrm, vrmListDispatch, updateConfig, and
viewer.getScreenshotBlob are preserved when making these replacements.
scripts/console-indexeddb-vrm-helper.js (1)

1-119: This script is largely redundant with the TS console helper.

src/utils/vrmIndexedDBConsoleHelper.ts already attaches the same API to window.__vrmIndexedDB in dev mode (and includes the additional applyAndLoad method this file lacks). Consider whether this standalone script is still needed. If it's intended for production debugging (where the TS helper is gated by isDev), document that distinction clearly in the file header; otherwise, remove it to avoid maintaining two diverging copies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/console-indexeddb-vrm-helper.js` around lines 1 - 119, This file
duplicates the TS console helper API (functions listVrms, addTestRecord,
addVrmFromFile, addVrmViaFilePicker and the window.__vrmIndexedDB export) and
risks divergence; either delete this JS file or clearly document why it must
exist in production (e.g., add a header stating it’s the production fallback
when src/utils/vrmIndexedDBConsoleHelper.ts is gated by isDev) and then align it
with the TS helper by adding the missing applyAndLoad behavior and matching
API/console message so both stay consistent. Ensure the chosen action is
reflected in the repository (remove file if redundant, or add the header and
necessary function parity if kept).
src/components/settings.tsx (1)

935-942: Remove the orphaned VRM file input element and associated handler.

The hidden VRM file input (lines 935-942) along with vrmFileInputRef (line 191) and handleChangeVrmFile (line 240) are unused. vrmFileInputRef is never triggered via .click(), and CharacterModelPage handles VRM file selection independently through its own programmatic file input in handleLoadVrmClick.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/settings.tsx` around lines 935 - 942, Remove the unused hidden
VRM file input and its orphaned handler by deleting the <input> with id
"vrm-file-input" and removing the related symbols vrmFileInputRef and
handleChangeVrmFile from the component; ensure CharacterModelPage's existing VRM
selection flow (handleLoadVrmClick) remains the single source of truth for VRM
file selection so no other code references vrmFileInputRef or
handleChangeVrmFile after removal.
src/features/vrmStore/vrmStoreReducer.ts (3)

5-6: Redundant bare import on Line 5.

Line 5 (import "@/utils/blobDataUtils") is a side-effect-only import, but Line 6 already imports the named exports from the same module. Unless blobDataUtils has module-level side effects that must run independently of the named imports (unlikely for a utility module), this bare import is unnecessary.

Suggested fix
-import "@/utils/blobDataUtils";
-import { Base64ToBlob, BlobToBase64 } from "@/utils/blobDataUtils";
+import { Base64ToBlob, BlobToBase64 } from "@/utils/blobDataUtils";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 5 - 6, Remove the
redundant side-effect import of the module "@/utils/blobDataUtils" and keep the
explicit named imports Base64ToBlob and BlobToBase64; specifically, delete the
bare import line `import "@/utils/blobDataUtils";` so only `import {
Base64ToBlob, BlobToBase64 } from "@/utils/blobDataUtils";` remains (ensure no
other code in the file relies on module-level side effects from that module
before removing).

33-36: appendVrm case lacks duplicate guard.

The reducer blindly appends action.vrmData to state. If a caller dispatches appendVrm more than once with the same hash (e.g., due to a double-click or race condition), duplicates accumulate in the list. Consider guarding with a hash-existence check, consistent with the check in addItem.

Suggested defensive check
         case VrmStoreActionType.appendVrm:
-            if (action.vrmData)
-                newState = [...state, action.vrmData];
+            if (action.vrmData && !state.some(v => v.hashEquals(action.vrmData!.getHash())))
+                newState = [...state, action.vrmData];
             break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 33 - 36, The appendVrm
reducer case (VrmStoreActionType.appendVrm) currently pushes action.vrmData into
state unconditionally; add a defensive duplicate guard similar to addItem by
checking action.vrmData?.hash (or the same unique identifier used in addItem)
against existing items in state and only create newState = [...state,
action.vrmData] if no item with that hash exists; keep the rest of the switch
unchanged and ensure you handle null/undefined action.vrmData the same way it’s
done now.

26-52: Side effects in reducer — pre-existing but worth noting.

React reducers are expected to be pure functions. Both addItem and LoadFromLocalStorage perform async I/O (IndexedDB, blob conversion) and rely on callbacks to eventually update state. This works today because the callback presumably dispatches a new action, but it makes the reducer non-deterministic and harder to reason about. As this pattern is now being extended (new appendVrm path), consider migrating the async logic into the context layer (e.g., useEffect or a middleware pattern) for future maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 26 - 52, The reducer
vrmStoreReducer contains side-effecting async calls (addItem and
LoadFromLocalStorage) which breaks reducer purity; remove any direct calls to
addItem and LoadFromLocalStorage from vrmStoreReducer and instead make the
reducer only handle synchronous state transitions (e.g., appendVrm,
updateVrmThumb, setVrmList); move the async logic that interacts with
IndexedDB/blob conversion into the context layer or a middleware/effect
(useEffect or a thunk) that listens for actions of type
VrmStoreActionType.addItem or VrmStoreActionType.loadFromLocalStorage, performs
the async work, and dispatches a follow-up action (e.g., appendVrm or
setVrmList) with the result so vrmStoreReducer remains pure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/settings.tsx`:
- Around line 207-209: The code is saving the entire base64 VRM into
localStorage via updateConfig("vrm_url", dataUrl), which will exceed quota;
instead remove or stop writing the full dataUrl to vrm_url and only persist the
short identifiers: call updateConfig("vrm_save_type", "local") and
updateConfig("vrm_hash", hash) (keep vrm_url empty or a short pointer), and
change any other occurrences (the same pattern at the other occurrence around
the 215-217 block) so that VRM binary stays in IndexedDB and the app resolves
the full VRM from IndexedDB at load time using vrm_hash; ensure updateConfig is
only used for small strings and that loading code looks up the VRM from
IndexedDB when vrm_save_type === "local".
- Around line 205-211: When handling the case where the VRM already exists (the
exists branch using viewer.loadVrm and updateConfig), also update the React
state so the UI reflects the selection: call setVrmHash(hash),
setVrmUrl(dataUrl), setVrmSaveType("local") and setSettingsUpdated(true) after
viewer.loadVrm resolves (or immediately after updateConfig) to mirror
CharacterModelPage's click handler behavior; ensure you reference the same state
setters (setVrmHash, setVrmUrl, setVrmSaveType, setSettingsUpdated) and keep the
existing updateConfig("vrm_save_type","vrm_hash","vrm_url") and
viewer.loadVrm(...) logic.
- Around line 212-232: After successfully storing and loading a new VRM in the
vrmDataProvider.addItem(...) promise chain (the block that calls updateConfig
and addVrmFromStored and then viewer.loadVrm), update the React state the same
way as the "already exists" path: call setVrmHash(hash), setVrmUrl(dataUrl),
setVrmSaveType("local"), and setSettingsUpdated(true) after addVrmFromStored (or
immediately after updateConfig calls) so the settings UI reflects the new VRM;
ensure these calls are placed before or inside the subsequent then that triggers
getScreenshotBlob so state is settled when the thumbnail/save flow continues.

In `@src/components/settings/common.tsx`:
- Around line 110-120: The current hashCodeLarge function (and constant
SAMPLE_SIZE) samples only ends of the base64 string and is collision-prone for
VRM files; replace it with an async, collision-resistant SHA-256 computation
that digests the full file ArrayBuffer using crypto.subtle.digest. Change the
call site to read the file with FileReader.readAsArrayBuffer (instead of
readAsDataURL), convert the resulting ArrayBuffer to a hex (or base64) string,
and return that from an async function (e.g., computeSha256Hash or an
async-replaced hashCodeLarge); update any IndexedDB put usage to await the async
hash before using it as the primary key so writes no longer risk silent
overwrites.

In `@src/features/vrmStore/vrmStoreContext.tsx`:
- Around line 63-68: addVrmFromStored currently always dispatches
VrmStoreActionType.appendVrm which lets duplicates be appended; before calling
vrmListDispatch inside addVrmFromStored, check the current VRM list state for an
existing item with the same hash (compare against state entries' hash/id) and
only dispatch if no match is found; use the existing state accessor (the context
state where addVrmFromStored is defined) to perform the duplicate check, and
keep constructing the VrmData only when you intend to dispatch to avoid
unnecessary object creation.

In `@src/features/vrmStore/vrmStoreReducer.ts`:
- Around line 60-97: The addItem function performs async work while closing over
the reducer's vrmList, causing stale-state races when multiple files are added
concurrently; move the async flow out of the reducer (or stop relying on the
captured vrmList) by making addItem synchronous only (create blob/url and
dispatch an "enqueueUpload" action) and perform
BlobToBase64/hashCodeLarge/vrmDataProvider.addItem in the context/dispatch layer
(or a side-effect handler) so the final state update uses the current store;
alternatively change the callback to dispatch an incremental append action
(e.g., appendVrm) that merges a new VrmData by current state rather than
replacing it, ensuring functions VrmData, addItem, vrmDataProvider.addItem and
the callback use the store-merge reducer instead of writing based on the
captured vrmList.
- Around line 90-92: In addItem, when the hash already exists you currently only
log and never call the callback; change the else branch to invoke the provided
callback (callback) with the current items and an indicator that the upload was
a duplicate (e.g., callback(items, { duplicate: true })) so the caller/user gets
feedback, and optionally also dispatch a user-facing notification (e.g.,
dispatch or call a notifyDuplicate action) to surface the duplicate to the UI.
- Around line 61-95: Update the Russian console messages in the addItem logic to
English to improve international maintainability: replace all
console.log/console.error strings inside the addItem flow (the messages around
"начало", "blob создан", "BlobToBase64 вернул...", "ошибка hashCode", "уже в
списке?", "IndexedDB записан", etc.) with clear English equivalents, keeping the
same context and variables (references to url, hash, exists) and preserving
error objects passed to console.error; do the same for the final .catch(...)
message so BlobToBase64/hash/IndexedDB errors are logged in English while
keeping the existing calls to BlobToBase64, hashCodeLarge,
vrmDataProvider.addItem and the callback unchanged.
- Around line 60-98: The addItem function creates an object URL (const url =
window.URL.createObjectURL(blob)) but never revokes it on early exits; update
addItem so URL.revokeObjectURL(url) is called whenever you return early or
encounter an error: specifically revoke url before returning when BlobToBase64
yields invalid data, inside the catch block for BlobToBase64/.then chain, and in
the branch where exists === true (the "model already exists" path); for the
successful path (after vrmDataProvider.addItem and after invoking callback({
url, ... })) either revoke the URL after the consumer no longer needs it or
document that the caller is responsible—ensure revocations reference the same
url variable so no objectURL is leaked.

In `@src/utils/vrmIndexedDBConsoleHelper.ts`:
- Around line 139-141: The code stores a full base64 VRM data URL into
localStorage via updateConfig("vrm_url", ...) which will exceed browser storage
quotas; instead, remove the updateConfig("vrm_url", record.vrmData) write and
only persist the vrm_hash and vrm_save_type ("local") as you already do
(updateConfig("vrm_hash", h) and updateConfig("vrm_save_type", "local")). Keep
the VRM blob/data in IndexedDB (the code that created/stored record.vrmData) and
change any consumers that read config.vrm_url to load the VRM at runtime from
IndexedDB by vrm_hash using your IndexedDB helper functions, so config only
holds the hash and not the full data URL.

---

Nitpick comments:
In @.gitignore:
- Around line 46-47: The .gitignore currently excludes the entire .vscode/
directory; instead refine it to ignore only personal/machine-specific VS Code
files so shared workspace configs (e.g., extensions.json, launch.json) can be
committed. Update the ignore rules for the .vscode folder to exclude
transient/user-specific patterns (like workspace storage, *.code-workspace user
files, settings.local) while adding explicit negation entries to allow shared
files such as .vscode/extensions.json and .vscode/launch.json to be tracked;
reference the existing .vscode/ entry in the diff and replace it with the more
granular ignore/allow patterns.

In `@scripts/console-indexeddb-vrm-helper.js`:
- Around line 1-119: This file duplicates the TS console helper API (functions
listVrms, addTestRecord, addVrmFromFile, addVrmViaFilePicker and the
window.__vrmIndexedDB export) and risks divergence; either delete this JS file
or clearly document why it must exist in production (e.g., add a header stating
it’s the production fallback when src/utils/vrmIndexedDBConsoleHelper.ts is
gated by isDev) and then align it with the TS helper by adding the missing
applyAndLoad behavior and matching API/console message so both stay consistent.
Ensure the chosen action is reflected in the repository (remove file if
redundant, or add the header and necessary function parity if kept).

In `@src/components/settings.tsx`:
- Around line 935-942: Remove the unused hidden VRM file input and its orphaned
handler by deleting the <input> with id "vrm-file-input" and removing the
related symbols vrmFileInputRef and handleChangeVrmFile from the component;
ensure CharacterModelPage's existing VRM selection flow (handleLoadVrmClick)
remains the single source of truth for VRM file selection so no other code
references vrmFileInputRef or handleChangeVrmFile after removal.

In `@src/features/vrmStore/vrmDataProvider.ts`:
- Around line 17-21: The success log in addItem (the console.log in the .then
after this.db.vrms.put) is noisy; change it to a lower-verbosity logger or gate
it behind a debug flag. Locate the addItem method and replace the console.log
call with console.debug (or wrap it in a conditional using a debug/verbose flag
your app uses) so normal production runs don’t emit that success message while
still allowing developers to enable it when needed.

In `@src/features/vrmStore/vrmStoreContext.tsx`:
- Around line 34-58: Replace verbose console logging inside the VRM add flow
with development-only or debug-level logs: change non-error
console.log/console.warn calls in the vrmListAddFile handler (the block calling
vrmListDispatch, viewer.loadVrm, updateConfig, viewer.getScreenshotBlob and the
inner vrmListDispatch/updateVrmThumb callbacks) to console.debug or wrap them
with a dev-check (e.g. if (process.env.NODE_ENV !== "production") { ... }) so
they are suppressed in production; keep the catch block using console.error for
actual errors (viewer.loadVrm .catch) but ensure it still logs the error object.
Ensure references to viewer.loadVrm, vrmListDispatch, updateConfig, and
viewer.getScreenshotBlob are preserved when making these replacements.

In `@src/features/vrmStore/vrmStoreReducer.ts`:
- Around line 5-6: Remove the redundant side-effect import of the module
"@/utils/blobDataUtils" and keep the explicit named imports Base64ToBlob and
BlobToBase64; specifically, delete the bare import line `import
"@/utils/blobDataUtils";` so only `import { Base64ToBlob, BlobToBase64 } from
"@/utils/blobDataUtils";` remains (ensure no other code in the file relies on
module-level side effects from that module before removing).
- Around line 33-36: The appendVrm reducer case (VrmStoreActionType.appendVrm)
currently pushes action.vrmData into state unconditionally; add a defensive
duplicate guard similar to addItem by checking action.vrmData?.hash (or the same
unique identifier used in addItem) against existing items in state and only
create newState = [...state, action.vrmData] if no item with that hash exists;
keep the rest of the switch unchanged and ensure you handle null/undefined
action.vrmData the same way it’s done now.
- Around line 26-52: The reducer vrmStoreReducer contains side-effecting async
calls (addItem and LoadFromLocalStorage) which breaks reducer purity; remove any
direct calls to addItem and LoadFromLocalStorage from vrmStoreReducer and
instead make the reducer only handle synchronous state transitions (e.g.,
appendVrm, updateVrmThumb, setVrmList); move the async logic that interacts with
IndexedDB/blob conversion into the context layer or a middleware/effect
(useEffect or a thunk) that listens for actions of type
VrmStoreActionType.addItem or VrmStoreActionType.loadFromLocalStorage, performs
the async work, and dispatches a follow-up action (e.g., appendVrm or
setVrmList) with the result so vrmStoreReducer remains pure.

In `@src/features/vrmViewer/model.ts`:
- Line 407: The expression idleAction ?? undefined is redundant because
idleAction (from this._currentAction) is already typed as THREE.AnimationAction
| undefined; update the call to fadeToAction to pass idleAction directly (i.e.,
replace this.fadeToAction(idleAction ?? undefined, 1) with
this.fadeToAction(idleAction, 1)) so the nullish-coalescing is removed; keep
references to the same variables and method names (idleAction,
this._currentAction, fadeToAction).

In `@src/features/vrmViewer/viewerContext.ts`:
- Line 7: The module currently exports a bare singleton "viewer" which couples
all importers; replace that export with a controlled accessor pattern: add an
initializeViewer(viewerInstance) function that stores the instance internally
and a getViewer() function that returns the current instance (throwing or
returning undefined if not initialized), then update non-React callers
(vrmDataProvider.ts and vrmIndexedDBConsoleHelper.ts) to accept a viewer
parameter where possible or call getViewer() as an explicit escape hatch; if you
decide to keep a process-scoped singleton instead, add a clear comment on the
export explaining it is an intentional escape hatch for non-React modules to
avoid accidental wider use.

In `@src/i18n/locales/ru/common.json`:
- Line 37: The Russian translation for the keys "Load VRM" and "Upload VRM" in
ru/common.json are identical ("Загрузить VRM"); change one so the UI
distinguishes loading a model into the viewer from uploading a file to
IndexedDB—e.g., keep "Upload VRM" as "Загрузить VRM" and change "Load VRM" to a
distinct phrase like "Загрузить в просмотрщик" or "Открыть VRM" so users can
tell the actions apart; update the "Load VRM" entry accordingly (affects the
"Load VRM" and "Upload VRM" keys).

In `@src/utils/vrmIndexedDBConsoleHelper.ts`:
- Around line 119-160: Re-indent the inner function doLoad so its entire body
(the return new Promise(...) block and nested handlers) is indented one level
deeper than applyAndLoad, making it visually and structurally clear that the
Promise belongs to doLoad; locate the function declaration function
applyAndLoad(hash?: string) and the nested const doLoad = (h: string) => { ...
}, then adjust indentation of the doLoad body (including the indexedDB.open
handlers, req.onsuccess/req.onerror, updateConfig and viewer.loadVrm calls) so
it is nested under doLoad rather than aligned with applyAndLoad.
- Around line 74-80: Replace the duplicated hash computation in
src/utils/vrmIndexedDBConsoleHelper.ts with a call to the existing hashCodeLarge
function from src/components/settings/common.tsx: remove the manual hashing loop
and import hashCodeLarge, then compute hash via hashCodeLarge(vrmData) (convert
to string if necessary). Ensure the import matches the exported name/signature
of hashCodeLarge and adjust types/exports if needed so the helper can reuse the
central implementation without redefining the algorithm.

Comment on lines +205 to +211
if (exists) {
viewer.loadVrm(dataUrl, () => {}).catch((e) => console.error("[VRM]", e));
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing React state updates when an already-stored VRM is selected.

When the VRM already exists in the list (line 204), updateConfig is called but setVrmHash, setVrmUrl, setVrmSaveType, and setSettingsUpdated are never called. Compare with CharacterModelPage's own click handler (lines 57-69 in CharacterModelPage.tsx) which correctly updates all of these. The UI will not reflect the selection.

🐛 Proposed fix
         if (exists) {
           viewer.loadVrm(dataUrl, () => {}).catch((e) => console.error("[VRM]", e));
           updateConfig("vrm_save_type", "local");
           updateConfig("vrm_hash", hash);
           updateConfig("vrm_url", dataUrl);
+          setVrmSaveType("local");
+          setVrmHash(hash);
+          setVrmUrl(dataUrl);
+          setSettingsUpdated(true);
           return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (exists) {
viewer.loadVrm(dataUrl, () => {}).catch((e) => console.error("[VRM]", e));
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
return;
}
if (exists) {
viewer.loadVrm(dataUrl, () => {}).catch((e) => console.error("[VRM]", e));
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
setVrmSaveType("local");
setVrmHash(hash);
setVrmUrl(dataUrl);
setSettingsUpdated(true);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/settings.tsx` around lines 205 - 211, When handling the case
where the VRM already exists (the exists branch using viewer.loadVrm and
updateConfig), also update the React state so the UI reflects the selection:
call setVrmHash(hash), setVrmUrl(dataUrl), setVrmSaveType("local") and
setSettingsUpdated(true) after viewer.loadVrm resolves (or immediately after
updateConfig) to mirror CharacterModelPage's click handler behavior; ensure you
reference the same state setters (setVrmHash, setVrmUrl, setVrmSaveType,
setSettingsUpdated) and keep the existing
updateConfig("vrm_save_type","vrm_hash","vrm_url") and viewer.loadVrm(...)
logic.

Comment on lines +207 to +209
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Storing full VRM data URL in localStorage will exceed quota and fail.

Same critical issue as flagged in vrmIndexedDBConsoleHelper.ts: updateConfig("vrm_url", dataUrl) persists the entire base64-encoded VRM file to localStorage, which has a ~5-10 MB limit. Real VRM files are typically 10-50+ MB, producing data URLs of 13-67+ MB. The updateConfig call will throw a QuotaExceededError and corrupt the config flow.

Store only the hash in config (e.g., vrm_hash) and resolve the VRM data from IndexedDB at load time. The vrm_url config should only hold a short URL or be left empty for local VRMs.

Also applies to: 215-217

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/settings.tsx` around lines 207 - 209, The code is saving the
entire base64 VRM into localStorage via updateConfig("vrm_url", dataUrl), which
will exceed quota; instead remove or stop writing the full dataUrl to vrm_url
and only persist the short identifiers: call updateConfig("vrm_save_type",
"local") and updateConfig("vrm_hash", hash) (keep vrm_url empty or a short
pointer), and change any other occurrences (the same pattern at the other
occurrence around the 215-217 block) so that VRM binary stays in IndexedDB and
the app resolves the full VRM from IndexedDB at load time using vrm_hash; ensure
updateConfig is only used for small strings and that loading code looks up the
VRM from IndexedDB when vrm_save_type === "local".

Comment on lines +212 to +232
vrmDataProvider
.addItem(hash, "local", dataUrl)
.then(() => {
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
addVrmFromStored(hash, dataUrl);
return viewer.loadVrm(dataUrl, () => {});
})
.then(() => {
viewer.getScreenshotBlob((thumbBlob: Blob | null) => {
if (thumbBlob) {
import("@/utils/blobDataUtils").then(({ BlobToBase64 }) => {
BlobToBase64(thumbBlob).then((thumbData) => {
vrmDataProvider.updateItemThumb(hash, thumbData);
});
});
}
});
})
.catch((e) => console.error("[VRM] Ошибка загрузки VRM:", e));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing state updates after new VRM is stored and loaded.

Similar to the "already exists" path, the "new VRM" path (lines 212-232) calls updateConfig and addVrmFromStored but never calls setVrmHash, setVrmUrl, setVrmSaveType, or setSettingsUpdated. The settings UI won't reflect the newly loaded VRM.

🐛 Proposed fix — add state updates after addVrmFromStored
         vrmDataProvider
           .addItem(hash, "local", dataUrl)
           .then(() => {
             updateConfig("vrm_save_type", "local");
             updateConfig("vrm_hash", hash);
             updateConfig("vrm_url", dataUrl);
             addVrmFromStored(hash, dataUrl);
+            setVrmSaveType("local");
+            setVrmHash(hash);
+            setVrmUrl(dataUrl);
+            setSettingsUpdated(true);
             return viewer.loadVrm(dataUrl, () => {});
           })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vrmDataProvider
.addItem(hash, "local", dataUrl)
.then(() => {
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
addVrmFromStored(hash, dataUrl);
return viewer.loadVrm(dataUrl, () => {});
})
.then(() => {
viewer.getScreenshotBlob((thumbBlob: Blob | null) => {
if (thumbBlob) {
import("@/utils/blobDataUtils").then(({ BlobToBase64 }) => {
BlobToBase64(thumbBlob).then((thumbData) => {
vrmDataProvider.updateItemThumb(hash, thumbData);
});
});
}
});
})
.catch((e) => console.error("[VRM] Ошибка загрузки VRM:", e));
vrmDataProvider
.addItem(hash, "local", dataUrl)
.then(() => {
updateConfig("vrm_save_type", "local");
updateConfig("vrm_hash", hash);
updateConfig("vrm_url", dataUrl);
addVrmFromStored(hash, dataUrl);
setVrmSaveType("local");
setVrmHash(hash);
setVrmUrl(dataUrl);
setSettingsUpdated(true);
return viewer.loadVrm(dataUrl, () => {});
})
.then(() => {
viewer.getScreenshotBlob((thumbBlob: Blob | null) => {
if (thumbBlob) {
import("@/utils/blobDataUtils").then(({ BlobToBase64 }) => {
BlobToBase64(thumbBlob).then((thumbData) => {
vrmDataProvider.updateItemThumb(hash, thumbData);
});
});
}
});
})
.catch((e) => console.error("[VRM] Ошибка загрузки VRM:", e));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/settings.tsx` around lines 212 - 232, After successfully
storing and loading a new VRM in the vrmDataProvider.addItem(...) promise chain
(the block that calls updateConfig and addVrmFromStored and then
viewer.loadVrm), update the React state the same way as the "already exists"
path: call setVrmHash(hash), setVrmUrl(dataUrl), setVrmSaveType("local"), and
setSettingsUpdated(true) after addVrmFromStored (or immediately after
updateConfig calls) so the settings UI reflects the new VRM; ensure these calls
are placed before or inside the subsequent then that triggers getScreenshotBlob
so state is settled when the thumbnail/save flow continues.

Comment on lines +110 to +120
/** Хеш для больших строк (VRM base64): берём начало, конец и длину, чтобы не блокировать UI. */
const SAMPLE_SIZE = 100000;
export function hashCodeLarge(str: string): string {
const len = str.length;
if (len <= SAMPLE_SIZE * 2) return hashCode(str);
let h = 0;
for (let i = 0; i < SAMPLE_SIZE; i++) h = ((h << 5) - h + str.charCodeAt(i)) << 0;
for (let i = len - SAMPLE_SIZE; i < len; i++) h = ((h << 5) - h + str.charCodeAt(i)) << 0;
h = ((h << 5) - h + len) << 0;
return h.toString();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Weak hash with partial sampling creates a real collision risk for VRM files.

hashCodeLarge only examines the first and last 100k characters (≈75 KB of binary each) plus the total length. Two different VRM files that share the same base64 header/trailer structure and identical length will produce the same hash. Since this hash is used as the IndexedDB primary key (via put), a collision silently overwrites an existing VRM record.

Consider using crypto.subtle.digest('SHA-256', ...) on the full ArrayBuffer (read via readAsArrayBuffer instead of readAsDataURL) for a collision-resistant hash. This is async and avoids blocking the main thread as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/settings/common.tsx` around lines 110 - 120, The current
hashCodeLarge function (and constant SAMPLE_SIZE) samples only ends of the
base64 string and is collision-prone for VRM files; replace it with an async,
collision-resistant SHA-256 computation that digests the full file ArrayBuffer
using crypto.subtle.digest. Change the call site to read the file with
FileReader.readAsArrayBuffer (instead of readAsDataURL), convert the resulting
ArrayBuffer to a hex (or base64) string, and return that from an async function
(e.g., computeSha256Hash or an async-replaced hashCodeLarge); update any
IndexedDB put usage to await the async hash before using it as the primary key
so writes no longer risk silent overwrites.

Comment on lines +63 to +68
const addVrmFromStored = (hash: string, url: string) => {
vrmListDispatch({
type: VrmStoreActionType.appendVrm,
vrmData: new VrmData(hash, url, "/vrm/thumb-placeholder.jpg", "local"),
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

addVrmFromStored does not guard against duplicate entries.

The appendVrm reducer action (see vrmStoreReducer.ts line 30) simply appends: [...state, action.vrmData]. If addVrmFromStored is called multiple times with the same hash (e.g., user re-uploads the same file), duplicate cards will appear in the character model list.

🐛 Proposed fix — check for duplicates before dispatching
     const addVrmFromStored = (hash: string, url: string) => {
+        if (loadedVrmList.some(v => v.getHash() === hash)) return;
         vrmListDispatch({
             type: VrmStoreActionType.appendVrm,
             vrmData: new VrmData(hash, url, "/vrm/thumb-placeholder.jpg", "local"),
         });
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addVrmFromStored = (hash: string, url: string) => {
vrmListDispatch({
type: VrmStoreActionType.appendVrm,
vrmData: new VrmData(hash, url, "/vrm/thumb-placeholder.jpg", "local"),
});
};
const addVrmFromStored = (hash: string, url: string) => {
if (loadedVrmList.some(v => v.getHash() === hash)) return;
vrmListDispatch({
type: VrmStoreActionType.appendVrm,
vrmData: new VrmData(hash, url, "/vrm/thumb-placeholder.jpg", "local"),
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreContext.tsx` around lines 63 - 68,
addVrmFromStored currently always dispatches VrmStoreActionType.appendVrm which
lets duplicates be appended; before calling vrmListDispatch inside
addVrmFromStored, check the current VRM list state for an existing item with the
same hash (compare against state entries' hash/id) and only dispatch if no match
is found; use the existing state accessor (the context state where
addVrmFromStored is defined) to perform the duplicate check, and keep
constructing the VrmData only when you intend to dispatch to avoid unnecessary
object creation.

Comment on lines 60 to 97
const addItem = (vrmList: VrmData[], file: File, callback: (prop: AddItemCallbackType) => any): VrmData[] => {
console.log("[VRM] addItem: начало", { fileName: file.name, size: file.size });
let loadedVrmList = vrmList;
const blob = new Blob([file], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
BlobToBase64(blob).then((data: string) => {
const hash = hashCode(data);
if (loadedVrmList.findIndex((vrm: VrmData) => vrm.hashEquals(hash)) == -1) {
loadedVrmList = [...loadedVrmList, new VrmData(hash, url, '/vrm/thumb-placeholder.jpg', 'local')];
vrmDataProvider.addItem(hash, 'local', data);
callback({ url, vrmList: loadedVrmList, hash });
}
});
console.log("[VRM] addItem: blob создан, objectURL =", url);
BlobToBase64(blob)
.then((data: string) => {
if (!data || typeof data !== "string") {
console.error("[VRM] addItem: BlobToBase64 вернул пустой или не строку");
return;
}
console.log("[VRM] addItem: BlobToBase64 готов, длина base64:", data.length);
let hash: string;
try {
hash = hashCodeLarge(data);
} catch (e) {
console.error("[VRM] addItem: ошибка hashCode", e);
return;
}
const exists = loadedVrmList.findIndex((vrm: VrmData) => vrm.hashEquals(hash)) !== -1;
console.log("[VRM] addItem: hash =", hash, "уже в списке?", exists);
if (!exists) {
loadedVrmList = [...loadedVrmList, new VrmData(hash, url, '/vrm/thumb-placeholder.jpg', 'local')];
return vrmDataProvider
.addItem(hash, 'local', data)
.then(() => {
console.log("[VRM] addItem: IndexedDB записан, вызываем callback");
callback({ url, vrmList: loadedVrmList, hash });
});
} else {
console.log("[VRM] addItem: модель с таким hash уже есть, callback не вызываем");
}
})
.catch((err) => {
console.error("[VRM] addItem: ошибка BlobToBase64 или цепочки", err);
});
return vrmList;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reducer dispatching side effects — stale closure risk with concurrent adds.

addItem is called synchronously from the reducer (Line 31), captures vrmList in a closure, then performs async work. If two files are added in quick succession, both closures capture the same vrmList snapshot. The second callback's loadedVrmList won't include the first file's addition, so the first entry can be silently overwritten when the callback sets state.

This is an existing architectural pattern in this file (e.g., LoadFromLocalStorage does the same), but the new addItem flow makes it more likely to hit in practice since users can upload multiple files in sequence. Consider moving the async logic out of the reducer (e.g., into the context/dispatch layer) and sequencing writes, or using appendVrm dispatches that merge into current state rather than replacing it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 60 - 97, The addItem
function performs async work while closing over the reducer's vrmList, causing
stale-state races when multiple files are added concurrently; move the async
flow out of the reducer (or stop relying on the captured vrmList) by making
addItem synchronous only (create blob/url and dispatch an "enqueueUpload"
action) and perform BlobToBase64/hashCodeLarge/vrmDataProvider.addItem in the
context/dispatch layer (or a side-effect handler) so the final state update uses
the current store; alternatively change the callback to dispatch an incremental
append action (e.g., appendVrm) that merges a new VrmData by current state
rather than replacing it, ensuring functions VrmData, addItem,
vrmDataProvider.addItem and the callback use the store-merge reducer instead of
writing based on the captured vrmList.

Comment on lines 60 to 98
const addItem = (vrmList: VrmData[], file: File, callback: (prop: AddItemCallbackType) => any): VrmData[] => {
console.log("[VRM] addItem: начало", { fileName: file.name, size: file.size });
let loadedVrmList = vrmList;
const blob = new Blob([file], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
BlobToBase64(blob).then((data: string) => {
const hash = hashCode(data);
if (loadedVrmList.findIndex((vrm: VrmData) => vrm.hashEquals(hash)) == -1) {
loadedVrmList = [...loadedVrmList, new VrmData(hash, url, '/vrm/thumb-placeholder.jpg', 'local')];
vrmDataProvider.addItem(hash, 'local', data);
callback({ url, vrmList: loadedVrmList, hash });
}
});
console.log("[VRM] addItem: blob создан, objectURL =", url);
BlobToBase64(blob)
.then((data: string) => {
if (!data || typeof data !== "string") {
console.error("[VRM] addItem: BlobToBase64 вернул пустой или не строку");
return;
}
console.log("[VRM] addItem: BlobToBase64 готов, длина base64:", data.length);
let hash: string;
try {
hash = hashCodeLarge(data);
} catch (e) {
console.error("[VRM] addItem: ошибка hashCode", e);
return;
}
const exists = loadedVrmList.findIndex((vrm: VrmData) => vrm.hashEquals(hash)) !== -1;
console.log("[VRM] addItem: hash =", hash, "уже в списке?", exists);
if (!exists) {
loadedVrmList = [...loadedVrmList, new VrmData(hash, url, '/vrm/thumb-placeholder.jpg', 'local')];
return vrmDataProvider
.addItem(hash, 'local', data)
.then(() => {
console.log("[VRM] addItem: IndexedDB записан, вызываем callback");
callback({ url, vrmList: loadedVrmList, hash });
});
} else {
console.log("[VRM] addItem: модель с таким hash уже есть, callback не вызываем");
}
})
.catch((err) => {
console.error("[VRM] addItem: ошибка BlobToBase64 или цепочки", err);
});
return vrmList;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Object URL is leaked when the VRM already exists.

When the hash already exists (Line 90 branch), the url created on Line 64 via URL.createObjectURL is never revoked and never passed anywhere, causing a memory leak. The same leak occurs if BlobToBase64 returns invalid data (Line 69) or hashCodeLarge throws (Line 77).

Revoke the URL in every early-return / non-success path.

Suggested fix
             if (!data || typeof data !== "string") {
                 console.error("[VRM] addItem: BlobToBase64 вернул пустой или не строку");
+                window.URL.revokeObjectURL(url);
                 return;
             }
             ...
             } catch (e) {
                 console.error("[VRM] addItem: ошибка hashCode", e);
+                window.URL.revokeObjectURL(url);
                 return;
             }
             ...
             } else {
                 console.log("[VRM] addItem: модель с таким hash уже есть, callback не вызываем");
+                window.URL.revokeObjectURL(url);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 60 - 98, The addItem
function creates an object URL (const url = window.URL.createObjectURL(blob))
but never revokes it on early exits; update addItem so URL.revokeObjectURL(url)
is called whenever you return early or encounter an error: specifically revoke
url before returning when BlobToBase64 yields invalid data, inside the catch
block for BlobToBase64/.then chain, and in the branch where exists === true (the
"model already exists" path); for the successful path (after
vrmDataProvider.addItem and after invoking callback({ url, ... })) either revoke
the URL after the consumer no longer needs it or document that the caller is
responsible—ensure revocations reference the same url variable so no objectURL
is leaked.

Comment on lines +61 to +95
console.log("[VRM] addItem: начало", { fileName: file.name, size: file.size });
let loadedVrmList = vrmList;
const blob = new Blob([file], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
BlobToBase64(blob).then((data: string) => {
const hash = hashCode(data);
if (loadedVrmList.findIndex((vrm: VrmData) => vrm.hashEquals(hash)) == -1) {
loadedVrmList = [...loadedVrmList, new VrmData(hash, url, '/vrm/thumb-placeholder.jpg', 'local')];
vrmDataProvider.addItem(hash, 'local', data);
callback({ url, vrmList: loadedVrmList, hash });
}
});
console.log("[VRM] addItem: blob создан, objectURL =", url);
BlobToBase64(blob)
.then((data: string) => {
if (!data || typeof data !== "string") {
console.error("[VRM] addItem: BlobToBase64 вернул пустой или не строку");
return;
}
console.log("[VRM] addItem: BlobToBase64 готов, длина base64:", data.length);
let hash: string;
try {
hash = hashCodeLarge(data);
} catch (e) {
console.error("[VRM] addItem: ошибка hashCode", e);
return;
}
const exists = loadedVrmList.findIndex((vrm: VrmData) => vrm.hashEquals(hash)) !== -1;
console.log("[VRM] addItem: hash =", hash, "уже в списке?", exists);
if (!exists) {
loadedVrmList = [...loadedVrmList, new VrmData(hash, url, '/vrm/thumb-placeholder.jpg', 'local')];
return vrmDataProvider
.addItem(hash, 'local', data)
.then(() => {
console.log("[VRM] addItem: IndexedDB записан, вызываем callback");
callback({ url, vrmList: loadedVrmList, hash });
});
} else {
console.log("[VRM] addItem: модель с таким hash уже есть, callback не вызываем");
}
})
.catch((err) => {
console.error("[VRM] addItem: ошибка BlobToBase64 или цепочки", err);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Console logs are in Russian — consider English for international maintainability.

All console.log / console.error messages in addItem are in Russian (e.g., "начало", "blob создан", "ошибка hashCode"). For an open-source project with an international contributor base, English log messages would be more accessible. If Russian context is needed, consider structured logging with a locale-neutral key plus a detail field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 61 - 95, Update the
Russian console messages in the addItem logic to English to improve
international maintainability: replace all console.log/console.error strings
inside the addItem flow (the messages around "начало", "blob создан",
"BlobToBase64 вернул...", "ошибка hashCode", "уже в списке?", "IndexedDB
записан", etc.) with clear English equivalents, keeping the same context and
variables (references to url, hash, exists) and preserving error objects passed
to console.error; do the same for the final .catch(...) message so
BlobToBase64/hash/IndexedDB errors are logged in English while keeping the
existing calls to BlobToBase64, hashCodeLarge, vrmDataProvider.addItem and the
callback unchanged.

Comment on lines +90 to +92
} else {
console.log("[VRM] addItem: модель с таким hash уже есть, callback не вызываем");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

No user feedback when a duplicate VRM is uploaded.

When the hash already exists, the callback is never invoked (Line 91). The caller (and ultimately the user) receives no signal that the upload was a duplicate and was rejected. This can create a confusing UX where the user uploads a file and nothing happens.

Consider invoking the callback with the existing list (and possibly flagging the duplicate), or surfacing a notification to the user.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/vrmStore/vrmStoreReducer.ts` around lines 90 - 92, In addItem,
when the hash already exists you currently only log and never call the callback;
change the else branch to invoke the provided callback (callback) with the
current items and an indicator that the upload was a duplicate (e.g.,
callback(items, { duplicate: true })) so the caller/user gets feedback, and
optionally also dispatch a user-facing notification (e.g., dispatch or call a
notifyDuplicate action) to surface the duplicate to the UI.

Comment on lines +139 to +141
await updateConfig("vrm_save_type", "local");
await updateConfig("vrm_hash", h);
await updateConfig("vrm_url", record.vrmData);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Storing full VRM data URL in localStorage via updateConfig("vrm_url", ...) will exceed localStorage limits.

VRM files are typically many megabytes. readAsDataURL produces a base64 string ~33% larger than the binary. updateConfig writes to localStorage (see src/utils/config.ts line 161), which has a ~5-10 MB quota in most browsers. This will silently fail or throw for any non-trivial VRM, corrupting config state.

Store only the hash in config and load VRM data from IndexedDB at runtime instead of persisting the entire data URL to localStorage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/vrmIndexedDBConsoleHelper.ts` around lines 139 - 141, The code
stores a full base64 VRM data URL into localStorage via updateConfig("vrm_url",
...) which will exceed browser storage quotas; instead, remove the
updateConfig("vrm_url", record.vrmData) write and only persist the vrm_hash and
vrm_save_type ("local") as you already do (updateConfig("vrm_hash", h) and
updateConfig("vrm_save_type", "local")). Keep the VRM blob/data in IndexedDB
(the code that created/stored record.vrmData) and change any consumers that read
config.vrm_url to load the VRM at runtime from IndexedDB by vrm_hash using your
IndexedDB helper functions, so config only holds the hash and not the full data
URL.

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.

1 participant