fix: navigation issues, AI explain validation, and user data sync#416
fix: navigation issues, AI explain validation, and user data sync#416ViktorSvertoka merged 3 commits intodevelopfrom
Conversation
Fixed Back button to use history navigation instead of redirecting to homepage Corrected footer DevLovers link to properly navigate to homepage Added frontend validation for AI Explain to prevent multi-line input errors and avoid 400 requests Improved user feedback for invalid text selection in AI Explain Implemented backend persistence for learned AI terms to sync across devices via user account
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds server-side persistence for user-learned terms (DB schema + server actions), updates UI to use those actions (fetch/save/hide/reorder), adds term-length validation and i18n messages, introduces a LegalBackButton component, and tightens password UTF‑8 byte-length validation. Changes
Sequence DiagramsequenceDiagram
actor User
participant Client as Client Component
participant Action as Server Action
participant DB as Database
User->>Client: Open dashboard / request explanation
Client->>Action: getLearnedTerms()
Action->>DB: SELECT ai_learned_terms WHERE userId
DB-->>Action: term rows
Action-->>Client: { success: true, terms }
Client->>Client: Render terms
User->>Client: Request AI explanation for term
Client->>Action: (AI request handled client-side) — then -> saveLearnedTerm(term, explanation)
Client->>Action: saveLearnedTerm(term, explanation)
Action->>DB: INSERT ... ON CONFLICT(userId, term) DO UPDATE (explanations, sortOrder)
DB-->>Action: ACK
Action-->>Client: { success: true }
User->>Client: Hide or reorder term(s)
Client->>Client: Optimistic UI update
Client->>Action: setTermHidden() / updateTermsOrder()
Action->>DB: UPDATE isHidden / sortOrder
DB-->>Action: ACK
Action-->>Client: { success: true }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
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: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/components/q&a/AIWordHelper.tsx (1)
206-223:⚠️ Potential issue | 🟠 MajorDon't block cached explanations with the new length guard.
frontend/components/dashboard/ExplainedTermsCard.tsxnow hydrates persisted explanations into the local cache, but this early return runs beforegetCachedExplanation(term). Any previously cached term over 100 chars becomes unreadable after this change even though its explanation is already available locally.♻️ Suggested fix
const fetchExplanation = useCallback(async () => { - if (term.length > 100) { - setError('TERM_TOO_LONG'); - return; - } - const cached = getCachedExplanation(term); if (cached) { setExplanation(cached); setError(null); setRateLimitState({ isRateLimited: false, resetIn: 0, retryAttempts: 0 }); setServiceErrorState({ isServiceError: false, errorCode: '', retryAttempts: 0, }); return; } + + if (term.length > 100) { + setError('TERM_TOO_LONG'); + return; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/q`&a/AIWordHelper.tsx around lines 206 - 223, The length guard in fetchExplanation is preventing retrieval of local cached explanations for long terms; change the control flow so getCachedExplanation(term) is called before enforcing the term.length > 100 check (or only apply the length guard when no cached explanation exists) so cached explanations (hydrated by ExplainedTermsCard) are returned even for terms >100 chars; update fetchExplanation to fetch cached first, setExplanation and clear errors if found, and only validate length and set TERM_TOO_LONG when no cached value is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/actions/ai.ts`:
- Around line 96-109: Before performing the visibility update, reject empty or
whitespace-only normalized terms the same way saveLearnedTerm does: call
normalizeTerm(term), check for a falsy/empty result and return an
error/invalid-term response instead of proceeding; only then run
db.update(aiLearnedTerms).set({ isHidden }).where(and(eq(aiLearnedTerms.userId,
user.id), eq(aiLearnedTerms.term, normalized))). Ensure the guard is placed
immediately after computing normalized to prevent false-positive success
responses.
- Around line 123-135: The batch of per-term updates using Promise.all should be
replaced with a single db.batch call to ensure atomic ordering updates; locate
the orderedTerms.map block that builds db.update(aiLearnedTerms).set({
sortOrder: index }).where(and(eq(aiLearnedTerms.userId, user.id),
eq(aiLearnedTerms.term, normalizeTerm(term)))) and instead collect those update
queries into an array and pass them to db.batch(...) so the Neon/local drivers
run them atomically; keep normalizeTerm, aiLearnedTerms, and user.id usage
unchanged and return/await the db.batch promise.
In `@frontend/components/dashboard/ExplainedTermsCard.tsx`:
- Around line 67-80: The optimistic updates in handleRemoveTerm and
handleRestoreTerm (and the other handlers at 97-104, 199-205) must either
rollback on failure or refetch authoritative state: capture the previous state
of terms and hiddenTerms before calling setTerms/setHiddenTerms, perform the
async calls to setTermHidden and updateTermsOrder, and in each catch revert
state by restoring the captured previous arrays (or call a single refetch
function to re-sync from the server) so the UI does not diverge when
setTermHidden or updateTermsOrder fails; apply the same pattern to the other
handlers to ensure consistent rollback/refetch behavior.
- Around line 33-53: The current getLearnedTerms() call swallows both rejected
promises and result.success === false, leaving the UI showing an empty list
instead of an error; wrap the async load to handle rejections and non-success
responses and surface an error state instead of silently returning.
Specifically, update the call to getLearnedTerms() (the promise chain that
currently populates setCachedExplanation, setTerms and setHiddenTerms inside
startTransition) to add a .catch handler (or use try/catch if converted to
async/await) that sets a load/error state (eg. setLoadError or setIsSyncFailed)
and avoids mutating terms when loading fails, and also handle the
!result.success branch by setting that same error state rather than returning
silently; keep existing setCachedExplanation/setTerms/setHiddenTerms logic only
when the fetch succeeded.
In `@frontend/components/legal/LegalBackButton.tsx`:
- Around line 5-16: LegalBackButton currently calls router.back() which can
navigate away or do nothing for direct visitors; update the onClick handler in
the LegalBackButton component to check whether a reliable history entry exists
(e.g. guard with window.history.length > 1) and if not, navigate to your
fallback route via router.push('/') (or router.replace('/') if you want to avoid
adding another history entry). Locate the LegalBackButton function and replace
the inline onClick={() => router.back()} with a small handler that uses
window.history.length check (or your app-specific internal-navigation flag if
available) and calls router.back() when safe otherwise calls router.push('/') as
fallback.
In `@frontend/components/q`&a/AIWordHelper.tsx:
- Around line 266-268: The current silent catch on the saveLearnedTerm(term,
data) call in AIWordHelper.tsx must be changed to surface failures: update the
catch block to log the error (include the error object) and set a UI-visible
failure state or invoke the existing user-notification mechanism (toast/modal)
so the user knows persistence failed; optionally expose a retry action that
re-calls saveLearnedTerm with the same term/data and clear or update the state
on success. Locate the saveLearnedTerm(term, data).catch(...) invocation and
replace the empty catch with error handling that logs via your app logger and
updates component state or calls the notification helper so the dashboard sync
problem is visible and recoverable.
In `@frontend/db/schema/ai.ts`:
- Around line 1-18: The ai_learned_terms table currently defines userId as a
plain text column; add a proper foreign key constraint from
aiLearnedTerms.userId to users.id with onDelete: 'cascade'. Locate the
aiLearnedTerms pgTable definition and replace the userId column declaration with
the pattern used in quiz.ts/points.ts/notifications.ts: use
text('user_id').notNull() wrapped with the foreign key helper (e.g.,
references(users.id) or pg-core's foreign key config) and include onDelete:
'cascade' so deletions of the users row cascade to ai_learned_terms rows.
---
Outside diff comments:
In `@frontend/components/q`&a/AIWordHelper.tsx:
- Around line 206-223: The length guard in fetchExplanation is preventing
retrieval of local cached explanations for long terms; change the control flow
so getCachedExplanation(term) is called before enforcing the term.length > 100
check (or only apply the length guard when no cached explanation exists) so
cached explanations (hydrated by ExplainedTermsCard) are returned even for terms
>100 chars; update fetchExplanation to fetch cached first, setExplanation and
clear errors if found, and only validate length and set TERM_TOO_LONG when no
cached value is available.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 16a285af-fb4b-4f4d-9c3d-b7969413277a
📒 Files selected for processing (14)
frontend/actions/ai.tsfrontend/components/auth/fields/PasswordField.tsxfrontend/components/dashboard/ExplainedTermsCard.tsxfrontend/components/legal/LegalBackButton.tsxfrontend/components/legal/LegalPageShell.tsxfrontend/components/q&a/AIWordHelper.tsxfrontend/components/shared/Footer.tsxfrontend/db/schema/ai.tsfrontend/db/schema/index.tsfrontend/drizzle/meta/0033_snapshot.jsonfrontend/drizzle/meta/_journal.jsonfrontend/messages/en.jsonfrontend/messages/pl.jsonfrontend/messages/uk.json
| await Promise.all( | ||
| orderedTerms.map((term, index) => | ||
| db | ||
| .update(aiLearnedTerms) | ||
| .set({ sortOrder: index }) | ||
| .where( | ||
| and( | ||
| eq(aiLearnedTerms.userId, user.id), | ||
| eq(aiLearnedTerms.term, normalizeTerm(term)) | ||
| ) | ||
| ) | ||
| ) | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the file
git ls-files frontend/actions/ | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 180
🏁 Script executed:
# Check the actual content around lines 123-135
cat -n frontend/actions/ai.ts | sed -n '110,145p'Repository: DevLoversTeam/devlovers.net
Length of output: 1200
🏁 Script executed:
# Find database imports and setup in the file
rg "import.*db" frontend/actions/ai.tsRepository: DevLoversTeam/devlovers.net
Length of output: 146
🏁 Script executed:
# Look for database library setup
fd "*.ts" | xargs rg -l "db\.(transaction|update)" | head -5Repository: DevLoversTeam/devlovers.net
Length of output: 467
🏁 Script executed:
# Check package.json for database dependencies
cat package.json | grep -A 20 "dependencies"Repository: DevLoversTeam/devlovers.net
Length of output: 115
🏁 Script executed:
# Look for transaction usage patterns in the codebase
rg "db\.transaction\|\.transaction\(" frontend/ --type ts -A 2Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Check the db setup/exports
cat frontend/db/index.ts 2>/dev/null || cat -n frontend/db.ts 2>/dev/null || find . -path "*/db/index.ts" -o -path "*/db.ts" | head -1 | xargs catRepository: DevLoversTeam/devlovers.net
Length of output: 2161
🏁 Script executed:
# Look at the db schema and setup
find . -name "index.ts" -path "*/db/*" | head -1 | xargs cat -nRepository: DevLoversTeam/devlovers.net
Length of output: 2707
🏁 Script executed:
# Search for any transaction usage in the codebase
rg "transaction\(" . --type ts -B 2 -A 2Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Check Drizzle ORM documentation and verify neon-http transaction support
# First, let's see if there are any existing transaction patterns
rg "db\." frontend/actions/ --type ts | grep -i "transaction\|batch" | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Let's check if there's a package-lock.json or similar to see exact versions
find . -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" | head -1Repository: DevLoversTeam/devlovers.net
Length of output: 99
🏁 Script executed:
# Check how other files handle multiple updates
rg "Promise\.all.*update\|batch.*update" frontend/actions/ --type ts -B 3 -A 3Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Check the drizzle-orm version in package-lock.json
cat ./frontend/package-lock.json | grep -A 2 '"drizzle-orm"' | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 169
🏁 Script executed:
# Also check for `@neondatabase/serverless` version
cat ./frontend/package-lock.json | grep -A 2 '@neondatabase/serverless' | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 902
🏁 Script executed:
# Let's also check if there's any documentation or comments about transaction support
rg "transaction" frontend/db/ --type tsRepository: DevLoversTeam/devlovers.net
Length of output: 697
🏁 Script executed:
# Check if there are other update operations in the codebase to see patterns used
rg "\.update\(" frontend/actions/ --type ts -B 1 -A 5 | head -50Repository: DevLoversTeam/devlovers.net
Length of output: 1671
🏁 Script executed:
# Verify if drizzle-orm/neon-http supports transactions by checking documentation patterns
# Let's see if there are any workarounds or batch operations documented
rg "batch\|transaction" frontend/db/index.tsRepository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Check if there's any conditional logic for transaction handling
cat frontend/db/index.ts | grep -A 50 "export"Repository: DevLoversTeam/devlovers.net
Length of output: 85
🌐 Web query:
drizzle-orm neon-http transaction support v0.45
💡 Result:
No, Drizzle ORM's neon-http driver does not support interactive transactions (like db.transaction) in v0.45.x. It explicitly throws "No transactions support in neon-http driver" as seen in recent issues from September 2025 with Drizzle v0.44.5. Use db.batch for non-interactive multi-statement batches instead, added in v0.29.4. For full transaction support (interactive with rollbacks/savepoints), switch to neon-serverless (WebSocket) driver. Official docs note neon-http is for "single, non-interactive transactions" and recommend neon-serverless for "session or interactive transaction support". No changelog entries in v0.45.0/0.45.1 mention adding transaction support to neon-http.
Citations:
- 1: Error: No transactions support in neon-http driver - unable_to_create_user better-auth/better-auth#4747
- 2: https://www.answeroverflow.com/m/1149370348593217619
- 3: https://orm.drizzle.team/docs/connect-neon
- 4: [BUG]: Typescript error with returning({...}) function when combining both HTTP and WS Neon connection types drizzle-team/drizzle-orm#3334
- 5: [BUG]: Property 'batch' does not exist on type 'NeonDatabase' drizzle-team/drizzle-orm#2980
- 6: https://orm.drizzle.team/docs/latest-releases/drizzle-orm-v0294
🏁 Script executed:
# Check if there are any batch operations documented or used elsewhere
rg "\.batch\(" frontend/actions/ --type tsRepository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Let's check the actual error handling and transaction attempts in ai.ts more carefully
cat -n frontend/actions/ai.ts | sed -n '115,145p'Repository: DevLoversTeam/devlovers.net
Length of output: 1008
🌐 Web query:
drizzle-orm batch method v0.45 documentation
💡 Result:
Drizzle ORM's db.batch method executes multiple SQL statements in a single batch call, supported for specific drivers: LibSQL (Turso), Neon, and D1 (Cloudflare). It reduces network round trips for better performance. For LibSQL, it's an implicit transaction: all succeed or all rollback. For D1, statements execute sequentially in auto-commit; failure aborts the sequence. Not supported for standard PostgreSQL/node-postgres due to protocol limitations (extended-query doesn't allow multiple statements). Usage example: const batchResponse = await db.batch([ db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }), db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)), db.query.usersTable.findMany({}), db.select.from(usersTable).where(eq(usersTable.id, 1)), db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(usersTable), ]); Supported builders: db.all, db.get, db.values, db.run, db.execute, db.query.
.findMany, db.query.Citations:
- 1: https://orm.drizzle.team/docs/batch-api
- 2: https://drizzle-orm.burt.pe.kr/docs/batch-api
- 3: [FEATURE]: BATCH API in node-postgres drizzle-team/drizzle-orm#3828
- 4: https://orm.drizzle.team/docs/latest-releases
- 5: https://newreleases.io/project/github/drizzle-team/drizzle-orm/release/0.45.0
Use db.batch() instead of Promise.all for atomic ordering updates.
The current Promise.all approach doesn't guarantee atomicity—if one update fails, earlier ones may already be committed, leaving the order inconsistent. However, db.transaction() is not supported by the neon-http driver used in production. Use db.batch() instead, which is supported by both the local PostgreSQL and Neon drivers and provides atomicity for Neon (implicit transaction).
💡 Suggested fix
try {
- await Promise.all(
- orderedTerms.map((term, index) =>
- db
- .update(aiLearnedTerms)
- .set({ sortOrder: index })
- .where(
- and(
- eq(aiLearnedTerms.userId, user.id),
- eq(aiLearnedTerms.term, normalizeTerm(term))
- )
- )
- )
- );
+ await db.batch(
+ orderedTerms.map((term, index) =>
+ db
+ .update(aiLearnedTerms)
+ .set({ sortOrder: index })
+ .where(
+ and(
+ eq(aiLearnedTerms.userId, user.id),
+ eq(aiLearnedTerms.term, normalizeTerm(term))
+ )
+ )
+ )
+ );
return { success: true };📝 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.
| await Promise.all( | |
| orderedTerms.map((term, index) => | |
| db | |
| .update(aiLearnedTerms) | |
| .set({ sortOrder: index }) | |
| .where( | |
| and( | |
| eq(aiLearnedTerms.userId, user.id), | |
| eq(aiLearnedTerms.term, normalizeTerm(term)) | |
| ) | |
| ) | |
| ) | |
| ); | |
| await db.batch( | |
| orderedTerms.map((term, index) => | |
| db | |
| .update(aiLearnedTerms) | |
| .set({ sortOrder: index }) | |
| .where( | |
| and( | |
| eq(aiLearnedTerms.userId, user.id), | |
| eq(aiLearnedTerms.term, normalizeTerm(term)) | |
| ) | |
| ) | |
| ) | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/actions/ai.ts` around lines 123 - 135, The batch of per-term updates
using Promise.all should be replaced with a single db.batch call to ensure
atomic ordering updates; locate the orderedTerms.map block that builds
db.update(aiLearnedTerms).set({ sortOrder: index
}).where(and(eq(aiLearnedTerms.userId, user.id), eq(aiLearnedTerms.term,
normalizeTerm(term)))) and instead collect those update queries into an array
and pass them to db.batch(...) so the Neon/local drivers run them atomically;
keep normalizeTerm, aiLearnedTerms, and user.id usage unchanged and return/await
the db.batch promise.
| saveLearnedTerm(term, data).catch(() => { | ||
| // DB sync failure is non-blocking; local cache still works | ||
| }); |
There was a problem hiding this comment.
Don't silently discard failed learned-term saves.
This is the write that makes the new learned-term sync durable for the explain flow. If it fails, the modal still looks successful locally but the term never shows up on the dashboard or another device, and the user gets no indication that persistence failed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/q`&a/AIWordHelper.tsx around lines 266 - 268, The current
silent catch on the saveLearnedTerm(term, data) call in AIWordHelper.tsx must be
changed to surface failures: update the catch block to log the error (include
the error object) and set a UI-visible failure state or invoke the existing
user-notification mechanism (toast/modal) so the user knows persistence failed;
optionally expose a retry action that re-calls saveLearnedTerm with the same
term/data and clear or update the state on success. Locate the
saveLearnedTerm(term, data).catch(...) invocation and replace the empty catch
with error handling that logs via your app logger and updates component state or
calls the notification helper so the dashboard sync problem is visible and
recoverable.
actions/ai.ts
- setTermHidden: added if (!normalized) guard (same as saveLearnedTerm)
db/schema/ai.ts
- userId column: added .references(() => users.id, { onDelete: 'cascade' })
- Note: run npx drizzle-kit push to apply the FK constraint to the DB
components/legal/LegalBackButton.tsx
- onClick: now checks window.history.length > 1 before router.back(), falls back to router.push('/')
components/q&a/AIWordHelper.tsx
- fetchExplanation: cache check moved before the 100-char guard (cached long terms now display)
- saveLearnedTerm catch: logs err via console.error instead of silently ignoring
components/dashboard/ExplainedTermsCard.tsx
- getLearnedTerms load: .catch + !result.success both set loadError state; error shown in UI
- handleRemoveTerm / handleRestoreTerm: capture prev state, rollback on failure
- handleDrop: state update moved outside functional updater, rollback on failure
- onTouchEnd (touch drag): same pattern using termsRef.current for pre-state capture
messages/en|uk|pl.json
- Added dashboard.explainedTerms.loadError translation key
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/components/dashboard/ExplainedTermsCard.tsx`:
- Around line 75-83: The current .catch() paths in handlers like
handleRemoveTerm (and the similar blocks around setTermHidden/updateTermsOrder
and restore handlers) roll back local arrays captured earlier, which can clobber
later successful changes; instead, remove the snapshot-restore logic and on any
mutation failure call the authoritative refetch function getLearnedTerms() (or
invoke a single server endpoint that performs restore+reorder) so the UI is
reconciled with server state; update the catch blocks for setTermHidden and
updateTermsOrder calls to await getLearnedTerms() (or the combined
restore+reorder action) rather than resetting the previous terms/hiddenTerms
arrays.
- Around line 29-31: The component initializes terms as [] and loadError as
false which causes the empty-state UI to flash before getLearnedTerms()
resolves; add an explicit initial loading flag (e.g., isInitialLoad or
isLoadingInitial) in ExplainedTermsCard defaulting to true, set it false when
the first fetch in getLearnedTerms() completes (both on success and error) and
change rendering logic to only show the “no explanations yet” empty state when
isInitialLoad is false and terms is empty (keep using
setTerms/setLoadError/setHiddenTerms as before). Apply the same pattern to the
other occurrences noted (the similar state blocks around lines 33-61, 282-288,
351-359) so each feature tracks initial loading separately from empty results.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 03b62e8d-051d-4024-a190-acc966767aec
📒 Files selected for processing (8)
frontend/actions/ai.tsfrontend/components/dashboard/ExplainedTermsCard.tsxfrontend/components/legal/LegalBackButton.tsxfrontend/components/q&a/AIWordHelper.tsxfrontend/db/schema/ai.tsfrontend/messages/en.jsonfrontend/messages/pl.jsonfrontend/messages/uk.json
✅ Files skipped from review due to trivial changes (1)
- frontend/db/schema/ai.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- frontend/components/q&a/AIWordHelper.tsx
- frontend/messages/uk.json
- frontend/components/legal/LegalBackButton.tsx
- frontend/actions/ai.ts
| const handleRemoveTerm = (term: string) => { | ||
| hideTermFromDashboard(term); | ||
| setTerms(prevTerms => prevTerms.filter(t => t !== term)); | ||
| setHiddenTerms(prevHidden => [...prevHidden, term]); | ||
| const prevTerms = terms; | ||
| const prevHidden = hiddenTerms; | ||
| setTerms(prevTerms.filter(t => t !== term)); | ||
| setHiddenTerms([...prevHidden, term]); | ||
| setTermHidden(term, true).catch(() => { | ||
| setTerms(prevTerms); | ||
| setHiddenTerms(prevHidden); | ||
| }); |
There was a problem hiding this comment.
Refetch authoritative state on mutation failure instead of restoring captured arrays.
Each .catch() restores a snapshot taken before the request. If another hide/restore/reorder succeeds while an earlier request is still in flight, a late failure will wipe out that newer change. The restore path is riskier still because setTermHidden() and updateTermsOrder() can partially succeed, leaving the server ahead of the UI after the rollback. Prefer a single server action for restore+reorder, or re-run getLearnedTerms() in the failure path.
Also applies to: 86-98, 115-123, 216-223
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/dashboard/ExplainedTermsCard.tsx` around lines 75 - 83,
The current .catch() paths in handlers like handleRemoveTerm (and the similar
blocks around setTermHidden/updateTermsOrder and restore handlers) roll back
local arrays captured earlier, which can clobber later successful changes;
instead, remove the snapshot-restore logic and on any mutation failure call the
authoritative refetch function getLearnedTerms() (or invoke a single server
endpoint that performs restore+reorder) so the UI is reconciled with server
state; update the catch blocks for setTermHidden and updateTermsOrder calls to
await getLearnedTerms() (or the combined restore+reorder action) rather than
resetting the previous terms/hiddenTerms arrays.
Fixed Back button to use history navigation instead of redirecting to homepage
Corrected footer DevLovers link to properly navigate to homepage
Added frontend validation for AI Explain to prevent multi-line input errors and avoid 400 requests
Improved user feedback for invalid text selection in AI Explain
Implemented backend persistence for learned AI terms to sync across devices via user account
Summary by CodeRabbit
New Features
Improvements
Localization