Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Someone is attempting to deploy a commit to the heyamica Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 likeextensions.json(recommended extensions) orlaunch.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.jsonThis 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
vieweras a bare module-level singleton bypasses the React Context mechanism and tightly couples all importers — notablyvrmDataProvider.tsandvrmIndexedDBConsoleHelper.ts— to this one fixed instance. Ifvieweris 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 receivevieweras 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 ?? undefinedis a no-op — simplify to justidleAction.
idleActionis captured fromthis._currentAction, which is typedTHREE.AnimationAction | undefined. Because the right-hand side of??is itselfundefined,idleAction ?? undefinedalways evaluates toidleAction, 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 indoLoadmakes the control flow hard to follow.The body of
doLoad(lines 121-159) is indented at the same level as the enclosingapplyAndLoad, making it look like thereturn new Promise(...)belongs toapplyAndLoadrather thandoLoad. 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 fromhashCodeLargeincommon.tsx.The hash algorithm (sample start/end + length) is implemented identically in
hashCodeLarge(src/components/settings/common.tsxlines 112-120) and also inscripts/console-indexeddb-vrm-helper.jslines 68-72. Import and reusehashCodeLargehere 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 —addItemnow properly returns aPromise.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 toconsole.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.errorcalls with Russian text. These are useful during development but will be noisy in production. Consider usingconsole.debugfor 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.tsalready attaches the same API towindow.__vrmIndexedDBin dev mode (and includes the additionalapplyAndLoadmethod this file lacks). Consider whether this standalone script is still needed. If it's intended for production debugging (where the TS helper is gated byisDev), 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) andhandleChangeVrmFile(line 240) are unused.vrmFileInputRefis never triggered via.click(), andCharacterModelPagehandles VRM file selection independently through its own programmatic file input inhandleLoadVrmClick.🤖 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. UnlessblobDataUtilshas 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:appendVrmcase lacks duplicate guard.The reducer blindly appends
action.vrmDatato state. If a caller dispatchesappendVrmmore 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 inaddItem.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
addItemandLoadFromLocalStorageperform 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 (newappendVrmpath), consider migrating the async logic into the context layer (e.g.,useEffector 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| updateConfig("vrm_save_type", "local"); | ||
| updateConfig("vrm_hash", hash); | ||
| updateConfig("vrm_url", dataUrl); |
There was a problem hiding this comment.
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".
| 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)); |
There was a problem hiding this comment.
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.
| 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.
| /** Хеш для больших строк (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(); | ||
| } |
There was a problem hiding this comment.
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.
| const addVrmFromStored = (hash: string, url: string) => { | ||
| vrmListDispatch({ | ||
| type: VrmStoreActionType.appendVrm, | ||
| vrmData: new VrmData(hash, url, "/vrm/thumb-placeholder.jpg", "local"), | ||
| }); | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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; |
There was a problem hiding this comment.
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.
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| } else { | ||
| console.log("[VRM] addItem: модель с таким hash уже есть, callback не вызываем"); | ||
| } |
There was a problem hiding this comment.
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.
| await updateConfig("vrm_save_type", "local"); | ||
| await updateConfig("vrm_hash", h); | ||
| await updateConfig("vrm_url", record.vrmData); |
There was a problem hiding this comment.
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.
upload VRM model to local storage IndexedDB
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Chores