Skip to content

fix: navigation issues, AI explain validation, and user data sync#416

Merged
ViktorSvertoka merged 3 commits intodevelopfrom
feat/ai-helper
Mar 25, 2026
Merged

fix: navigation issues, AI explain validation, and user data sync#416
ViktorSvertoka merged 3 commits intodevelopfrom
feat/ai-helper

Conversation

@TiZorii
Copy link
Collaborator

@TiZorii TiZorii commented Mar 25, 2026

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

    • Save, view, hide, and reorder your personal AI-learned terms and explanations in the dashboard.
  • Improvements

    • AI helper rejects terms over 100 characters and saves successful explanations for future use.
    • Dashboard loads saved terms from the server, shows a load-error message, and rolls back UI on persistence failures.
    • Password input now enforces UTF‑8 byte-length limits.
    • Updated legal page back navigation and footer styling.
  • Localization

    • Added messages for load errors and “term too long” hints.

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
@vercel
Copy link
Contributor

vercel bot commented Mar 25, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
devlovers-net Ignored Ignored Preview Mar 25, 2026 6:53pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

Warning

Rate limit exceeded

@TiZorii has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 32 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ad8ccf8-7733-4c70-8960-9183e6ef76fa

📥 Commits

Reviewing files that changed from the base of the PR and between ad467e9 and 193aa8a.

📒 Files selected for processing (1)
  • frontend/components/dashboard/ExplainedTermsCard.tsx
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
AI Server Actions
frontend/actions/ai.ts
New server-only module with saveLearnedTerm, getLearnedTerms, setTermHidden, and updateTermsOrder — per-user term upsert, fetch, visibility toggle, and ordering; authentication checks and standardized success/error results.
Database Schema & Migration
frontend/db/schema/ai.ts, frontend/db/schema/index.ts, frontend/drizzle/meta/_journal.json
New aiLearnedTerms table (ai_learned_terms) with multilingual explanation fields, isHidden, sortOrder, createdAt, UUID id, FK userId, unique (userId, term); schema re-export and drizzle journal entry added.
Dashboard integration
frontend/components/dashboard/ExplainedTermsCard.tsx
Replaced local-cache/hide/order persistence with server actions: fetch via getLearnedTerms(), use setTermHidden() and updateTermsOrder() for hide/reorder with optimistic UI and rollback on failure; added load-error handling.
AI Helper
frontend/components/q&a/AIWordHelper.tsx
Short-circuits terms >100 chars (new TERM_TOO_LONG error + i18n), calls saveLearnedTerm() asynchronously after successful explanation fetch (errors logged).
Auth Password Field
frontend/components/auth/fields/PasswordField.tsx
Added UTF‑8 byte-length validation against PASSWORD_MAX_BYTES in invalid/input handlers; sets custom validity when exceeded.
Legal navigation
frontend/components/legal/LegalBackButton.tsx, frontend/components/legal/LegalPageShell.tsx
New LegalBackButton component using router.back()/router.push; LegalPageShell now uses LegalBackButton and updated background utility classes.
Footer styling
frontend/components/shared/Footer.tsx
Adjusted Tailwind support selector and converted bracketed CSS-variable class tokens to text-(--var) / border-(--var) style for hover/active states.
Localization
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Added aiHelper.termTooLong.{title,hint} and dashboard.explainedTerms.loadError translations; updated related strings.

Sequence Diagram

sequenceDiagram
    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 }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 A rabbit hops through code and thyme,
Terms nest in DB rows, neat in time.
From client fetch to server store,
Order kept and hidden no more—
Little hops, big persistence — sublime!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately summarizes the main changes: navigation fixes (LegalBackButton, Footer link), AI validation (term length check), and backend persistence for learned terms (user data sync).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-helper

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
Contributor

@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: 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 | 🟠 Major

Don't block cached explanations with the new length guard.

frontend/components/dashboard/ExplainedTermsCard.tsx now hydrates persisted explanations into the local cache, but this early return runs before getCachedExplanation(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

📥 Commits

Reviewing files that changed from the base of the PR and between 8730038 and 897fd79.

📒 Files selected for processing (14)
  • frontend/actions/ai.ts
  • frontend/components/auth/fields/PasswordField.tsx
  • frontend/components/dashboard/ExplainedTermsCard.tsx
  • frontend/components/legal/LegalBackButton.tsx
  • frontend/components/legal/LegalPageShell.tsx
  • frontend/components/q&a/AIWordHelper.tsx
  • frontend/components/shared/Footer.tsx
  • frontend/db/schema/ai.ts
  • frontend/db/schema/index.ts
  • frontend/drizzle/meta/0033_snapshot.json
  • frontend/drizzle/meta/_journal.json
  • frontend/messages/en.json
  • frontend/messages/pl.json
  • frontend/messages/uk.json

Comment on lines +123 to +135
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))
)
)
)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
git ls-files frontend/actions/ | head -20

Repository: 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.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 146


🏁 Script executed:

# Look for database library setup
fd "*.ts" | xargs rg -l "db\.(transaction|update)" | head -5

Repository: 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 2

Repository: 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 cat

Repository: 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 -n

Repository: DevLoversTeam/devlovers.net

Length of output: 2707


🏁 Script executed:

# Search for any transaction usage in the codebase
rg "transaction\(" . --type ts -B 2 -A 2

Repository: 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 -20

Repository: 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 -1

Repository: 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 3

Repository: 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 -20

Repository: 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 -20

Repository: 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 ts

Repository: 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 -50

Repository: 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.ts

Repository: 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:


🏁 Script executed:

# Check if there are any batch operations documented or used elsewhere
rg "\.batch\(" frontend/actions/ --type ts

Repository: 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.
.findFirst, db.select, db.update, db.insert, db.delete. BatchResponse is a tuple of results matching each statement's return type (e.g., arrays of rows, ResultSet, etc.). This API has been stable since early implementations (e.g., v0.28+), with no specific changes or new features in v0.45.0 (released Dec 2025, bug fixes only). Official docs at https://orm.drizzle.team/docs/batch-api apply to v0.45.x.

Citations:


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.

Suggested change
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.

Comment on lines +266 to +268
saveLearnedTerm(term, data).catch(() => {
// DB sync failure is non-blocking; local cache still works
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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
Copy link
Contributor

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 897fd79 and ad467e9.

📒 Files selected for processing (8)
  • frontend/actions/ai.ts
  • frontend/components/dashboard/ExplainedTermsCard.tsx
  • frontend/components/legal/LegalBackButton.tsx
  • frontend/components/q&a/AIWordHelper.tsx
  • frontend/db/schema/ai.ts
  • frontend/messages/en.json
  • frontend/messages/pl.json
  • frontend/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

Comment on lines 75 to +83
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);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

@ViktorSvertoka ViktorSvertoka merged commit a447890 into develop Mar 25, 2026
7 checks passed
@ViktorSvertoka ViktorSvertoka deleted the feat/ai-helper branch March 25, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants