Skip to content

AI-assisted reward builder - #4277

Merged
steven-tey merged 27 commits into
mainfrom
ai-reward-builder
Aug 16, 2026
Merged

AI-assisted reward builder#4277
steven-tey merged 27 commits into
mainfrom
ai-reward-builder

Conversation

@pepeladeira

@pepeladeira pepeladeira commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added an AI-powered reward builder for creating non-referral rewards from natural-language input.
    • Added presets for sales, leads, and clicks, including percentage, duration, and conditional options.
    • Added preview, review, accept, discard, loading, and error states.
    • Added validation, plan eligibility checks, generation limits, and usage-credit protection with automatic refunds when generation fails.
    • Added reduced-motion support and a sparkle icon for AI-related interface elements.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Aug 16, 2026 2:26am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds validated AI reward schemas, authorized streaming generation, shared AI usage accounting, typed presets, a client-side builder with preview and review flows, and integration into non-referral reward forms.

Changes

AI reward builder

Layer / File(s) Summary
Reward schemas and presets
apps/web/lib/ai/ai-reward-schema.ts, apps/web/ui/partners/rewards/reward-presets.ts
Defines validated reward drafts, conditional modifiers, inferred types, and presets for sale, lead, and click events.
Shared AI usage accounting
apps/web/lib/api/links/usage-checks.ts, apps/web/lib/ai/generate-partner-network-invite-email.ts
Adds shared atomic credit reservation and refund helpers. Updates invite-email generation to use them.
Authorized reward generation
apps/web/lib/ai/generate-reward.ts, apps/web/lib/upstash/ratelimit-policies.ts
Validates requests, checks authentication and workspace permissions, enforces Advanced-plan access, applies rate limits, reserves usage credit, and streams structured reward drafts.
Client builder and preview
apps/web/ui/partners/rewards/ai-reward-builder.tsx, packages/ui/src/icons/nucleo/*
Adds draft normalization, preset selection, streamed generation, validation, review actions, error handling, animations, loading states, and the Sparkle3Fill icon.
Reward sheet integration
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
Adds AI input and preview behavior for non-referral rewards while preserving referral-specific rendering and existing form controls.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Partner
  participant AIRewardInput
  participant useAIRewardBuilder
  participant generateReward
  participant AIRewardPreviewFrame
  participant RewardForm
  Partner->>AIRewardInput: enter prompt or select preset
  AIRewardInput->>useAIRewardBuilder: start reward generation
  AIRewardInput->>useAIRewardBuilder: select preset
  useAIRewardBuilder->>generateReward: submit workspaceId, event, prompt
  generateReward-->>useAIRewardBuilder: stream structured reward draft
  useAIRewardBuilder->>AIRewardPreviewFrame: update preview state
  AIRewardPreviewFrame-->>Partner: show preview and review actions
  Partner->>RewardForm: accept draft
  RewardForm-->>Partner: update reward fields
Loading

Possibly related PRs

  • dubinc/dub#3923: Modifies the same reward sheet and reward-form modifier handling.
  • dubinc/dub#4032: Modifies reward-condition structures used by reward building.
  • dubinc/dub#4063: Modifies the same reward sheet for reward currency formatting.

Suggested reviewers: devkiran, steven-tey

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding an AI-assisted reward builder.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ai-reward-builder

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.

@pepeladeira
pepeladeira marked this pull request as ready for review August 11, 2026 21:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (3)

936-937: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Explain why submit is disabled during AI review.

The submit button is disabled while aiBuilder.isReviewing is true, but disabledTooltip only covers the upsell cases. A user who does not notice the Accept and Discard buttons sees a disabled button with no explanation. Add a tooltip branch that instructs the user to accept or discard the AI preview.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx` around lines 936 -
937, Update the submit button’s disabledTooltip logic near the isUpdating and
aiBuilder.isReviewing conditions to add a branch for AI review state,
instructing users to accept or discard the AI preview before submitting.
Preserve the existing upsell tooltip branches and apply this message when
aiBuilder.isReviewing disables submission.

683-776: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated description editor.

The description and tooltip editor block is repeated verbatim in the non-referral branch (Lines 683-776) and the referral branch (Lines 790-883). The two copies must stay in sync for every future change to the description UI, the max lengths, or the animation.

Extract one local component, for example RewardDescriptionEditor, that reads description and tooltipDescription and calls setValue. Render it in both branches.

Also applies to: 790-883

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx` around lines 683 -
776, The description and tooltip editor markup is duplicated across the
non-referral and referral branches. Extract the shared block into a local
RewardDescriptionEditor component that receives or accesses description,
tooltipDescription, and setValue, including the existing animation, controls,
max lengths, and clearing behavior, then render that component in both branches
and remove both duplicated implementations.

968-980: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused className prop from RewardSheetCard. Both call sites omit className, so the prop is not used in this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx` around lines 968 -
980, Remove the unused className prop from the RewardSheetCard component’s
PropsWithChildren type, parameter destructuring, and cn call while preserving
its existing default styling. Leave both call sites unchanged since neither
passes className.
apps/web/ui/partners/rewards/ai-reward-builder.tsx (1)

205-260: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cancel the streamed draft when the builder unmounts.

The for await loop keeps calling applyDraft after the sheet closes or after a new generation starts. Track a generation token or a mounted ref, and stop applying partials when the token no longer matches. This also prevents an old stream from overwriting a newer one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/ai-reward-builder.tsx` around lines 205 - 260,
Update generate and the streamed readStreamableValue loop to track a mounted
state and per-generation token, invalidating the token on unmount and before
starting a new generation. Stop consuming or applying partial drafts when the
token is no longer current, so stale streams cannot call applyDraft or overwrite
newer generation results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/ai/ai-reward-schema.ts`:
- Around line 71-75: Constrain the amount field in aiRewardModifierSchema with
nonnegative validation, and add object-level validation so percentage modifiers
cannot exceed 100 while preserving the existing unrestricted flat-dollar
behavior.

In `@apps/web/lib/ai/generate-reward.ts`:
- Around line 59-68: Update generateReward to use inputSchema.safeParse and
throw a controlled Error message from validation failures before accessing
parsed data. After obtaining the authenticated session and normalized
workspaceId, call assertRateLimit with [session.user.id, workspaceId] and the
shared policy before invoking streamText, preserving the existing authorization
and generation flow.
- Around line 99-121: The streamText call in the async generation flow must
propagate AI SDK generation failures and limit output size: add an onError
handler that passes the provided error to stream.error, and set maxOutputTokens
to 2000 alongside the existing model options.

In `@apps/web/ui/partners/rewards/ai-reward-builder.tsx`:
- Around line 783-787: Update the children wrapper in the AI reward builder
preview to apply the React 19 inert attribute whenever chromeMounted is true,
alongside the existing pointer-events-none and select-none classes. Preserve
normal keyboard and pointer interaction when chromeMounted is false.
- Around line 149-161: Update applyDraft to preserve the form’s original
defaults when applying next, so the AI-generated changes remain dirty and
unsaved-change detection continues to work; keep discard’s existing
reset(snapshotRef.current) behavior unchanged so discarding restores the
original state.
- Around line 84-108: Update the modifier mapping in the reward builder to
assign the resolved modifierType to the returned type field instead of
modifier.type, ensuring omitted AI types are populated before validateReward
processes the payload.

---

Nitpick comments:
In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx`:
- Around line 936-937: Update the submit button’s disabledTooltip logic near the
isUpdating and aiBuilder.isReviewing conditions to add a branch for AI review
state, instructing users to accept or discard the AI preview before submitting.
Preserve the existing upsell tooltip branches and apply this message when
aiBuilder.isReviewing disables submission.
- Around line 683-776: The description and tooltip editor markup is duplicated
across the non-referral and referral branches. Extract the shared block into a
local RewardDescriptionEditor component that receives or accesses description,
tooltipDescription, and setValue, including the existing animation, controls,
max lengths, and clearing behavior, then render that component in both branches
and remove both duplicated implementations.
- Around line 968-980: Remove the unused className prop from the RewardSheetCard
component’s PropsWithChildren type, parameter destructuring, and cn call while
preserving its existing default styling. Leave both call sites unchanged since
neither passes className.

In `@apps/web/ui/partners/rewards/ai-reward-builder.tsx`:
- Around line 205-260: Update generate and the streamed readStreamableValue loop
to track a mounted state and per-generation token, invalidating the token on
unmount and before starting a new generation. Stop consuming or applying partial
drafts when the token is no longer current, so stale streams cannot call
applyDraft or overwrite newer generation results.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2880a52e-828d-467b-99d4-97cf60845cdb

📥 Commits

Reviewing files that changed from the base of the PR and between 47a6b18 and 2d9ef5c.

📒 Files selected for processing (7)
  • apps/web/lib/ai/ai-reward-schema.ts
  • apps/web/lib/ai/generate-reward.ts
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/ui/partners/rewards/ai-reward-builder.tsx
  • apps/web/ui/partners/rewards/reward-presets.ts
  • packages/ui/src/icons/nucleo/index.ts
  • packages/ui/src/icons/nucleo/sparkle3-fill.tsx

Comment thread apps/web/lib/ai/ai-reward-schema.ts Outdated
Comment thread apps/web/lib/ai/generate-reward.ts Outdated
Comment thread apps/web/lib/ai/generate-reward.ts
Comment thread apps/web/ui/partners/rewards/ai-reward-builder.tsx
Comment thread apps/web/ui/partners/rewards/ai-reward-builder.tsx
Comment thread apps/web/ui/partners/rewards/ai-reward-builder.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/lib/ai/ai-reward-schema.ts (1)

24-28: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Require metadataField for metadata conditions.

The schema accepts a condition with attribute: "metadata" and no metadataField. That condition cannot identify the metadata key to evaluate. Add an object refinement that requires a non-empty metadataField when attribute is metadata.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/ai/ai-reward-schema.ts` around lines 24 - 28, Update the
condition schema object around metadataField to add a refinement requiring
metadataField to be non-empty whenever attribute is "metadata"; preserve
optional metadataField behavior for all other attribute values.
apps/web/lib/ai/generate-reward.ts (1)

135-150: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the draft against the selected event.

aiRewardSchema accepts percentage and lifetime rewards, plus all condition entities, for every event. The selected event only affects the prompt. A model response can therefore pass Output.object while producing a percentage or lifetime click/lead reward, or a condition that is not supported for that event.

Create an event-bound output schema or add an event-specific refinement before forwarding the draft. Validate base fields, inherited modifier fields, and each condition entity and attribute against REWARD_CONDITIONS[event].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/ai/generate-reward.ts` around lines 135 - 150, Update the reward
generation flow around aiRewardSchema and partialObject before stream.update to
validate drafts against the selected event, not only the base schema. Build an
event-bound schema or refinement using REWARD_CONDITIONS[event] that validates
base and inherited modifier fields, rejects unsupported percentage or lifetime
click/lead rewards, and rejects unsupported condition entities or attributes
before forwarding the draft.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/api/links/usage-checks.ts`:
- Around line 57-69: Update the atomic update in the usage-check flow to compare
aiUsage against the persisted Project.aiLimit rather than the potentially stale
workspace.aiLimit. Preserve the existing increment behavior only when the stored
limit has not been reached, using a database-supported conditional update or a
transaction that locks and rechecks the project row.

---

Outside diff comments:
In `@apps/web/lib/ai/ai-reward-schema.ts`:
- Around line 24-28: Update the condition schema object around metadataField to
add a refinement requiring metadataField to be non-empty whenever attribute is
"metadata"; preserve optional metadataField behavior for all other attribute
values.

In `@apps/web/lib/ai/generate-reward.ts`:
- Around line 135-150: Update the reward generation flow around aiRewardSchema
and partialObject before stream.update to validate drafts against the selected
event, not only the base schema. Build an event-bound schema or refinement using
REWARD_CONDITIONS[event] that validates base and inherited modifier fields,
rejects unsupported percentage or lifetime click/lead rewards, and rejects
unsupported condition entities or attributes before forwarding the draft.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 853d182b-ddf1-4b64-9675-bd388e40724c

📥 Commits

Reviewing files that changed from the base of the PR and between 997f460 and f9de79f.

📒 Files selected for processing (5)
  • apps/web/lib/ai/ai-reward-schema.ts
  • apps/web/lib/ai/generate-partner-network-invite-email.ts
  • apps/web/lib/ai/generate-reward.ts
  • apps/web/lib/api/links/usage-checks.ts
  • apps/web/ui/partners/rewards/ai-reward-builder.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/ui/partners/rewards/ai-reward-builder.tsx

Comment thread apps/web/lib/api/links/usage-checks.ts Outdated
@pepeladeira

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review. any breaking changes or security issues?

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@pepeladeira I will perform a full review of #4277. I will check breaking changes and security-sensitive paths, including authorization, rate limits, usage credits, input validation, and streamed AI output.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (3)

683-775: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the duplicated reward description block.

Lines 683-775 and lines 790-882 are the same markup: the Pen2 toggle, the animated motion.div, InlineBadgePopoverInput for description, InlineBadgePopoverRichTextArea for tooltipDescription, and the clear button. Only the title body and the card content differ between the referral and non-referral branches. Two copies of about 95 lines must now be kept in sync.

Extract the block into a local component (for example RewardDescriptionFields) that reads description and tooltipDescription from the form context, then render it in both branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx` around lines 683 -
775, The referral and non-referral branches duplicate the reward description
editor markup. Extract the shared Pen2 toggle, animated description/tooltip
fields, and clear-button logic into a local RewardDescriptionFields component
that reads and updates description and tooltipDescription through the form
context, then render that component in both branches while leaving their
distinct titles and card content unchanged.

323-330: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Stabilize the getValues and reset adapters.

Both props are new function instances on every render. useAIRewardBuilder lists them in the dependency arrays of ensureSnapshot, applyDraft, and generate, so all of those callbacks are rebuilt on every render of RewardSheetContent, including on every streamed chunk. Wrapping the adapters in useCallback restores the memoization.

♻️ Proposed refactor
   const aiEvent = selectedEvent === "referral" ? "sale" : selectedEvent;
+  const getAIFormValues = useCallback(
+    () => getValues() as Record<string, unknown>,
+    [getValues],
+  );
+  const resetAIFormValues = useCallback(
+    (
+      values: Record<string, unknown>,
+      options?: { keepDefaultValues?: boolean },
+    ) => reset(values as FormData, options),
+    [reset],
+  );
   const aiBuilder = useAIRewardBuilder({
     event: aiEvent,
-    getValues: () => getValues() as Record<string, unknown>,
-    reset: (values, options) => reset(values as FormData, options),
+    getValues: getAIFormValues,
+    reset: resetAIFormValues,
   });

Add useCallback to the React import if it is not already present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx` around lines 323 -
330, Stabilize the getValues and reset adapter props passed to
useAIRewardBuilder in RewardSheetContent by wrapping each in useCallback with
appropriate dependencies. Add the React useCallback import if needed, while
preserving their current behavior and type conversions so ensureSnapshot,
applyDraft, and generate retain memoized callbacks across renders.

970-982: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused className prop from RewardSheetCard. No caller passes className, so this prop adds unused API surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx` around lines 970 -
982, Remove the optional className prop from the RewardSheetCard component props
and eliminate its use in the cn call, preserving the existing card styles and
all other props.
apps/web/ui/partners/rewards/ai-reward-builder.tsx (1)

269-276: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Each streamed chunk resets the whole form and regenerates modifier ids.

applyDraft runs on every partial object. buildRewardFormValuesFromDraft assigns a new uuid() to each modifier on line 99, so reset produces new modifiers[].id values on every chunk. RewardsLogic keys its useFieldArray rows by that id, so every condition row unmounts and remounts many times during a single generation. This causes visible churn in the preview and wasted renders.

Consider reusing ids by index across chunks, or throttling partial application (for example, apply partials only when the modifier count or scalar fields change).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/rewards/ai-reward-builder.tsx` around lines 269 - 276,
Update the streamed partial handling in the generation loop and the modifier
conversion used by buildRewardFormValuesFromDraft so existing modifier ids are
reused by index across chunks instead of generating a new uuid for every reset.
Preserve stable ids for unchanged rows while assigning uuids only to newly added
modifiers, preventing RewardsLogic field-array rows from remounting during
streaming.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/ai/ai-reward-schema.ts`:
- Around line 5-28: Update aiRewardSchema, aiRewardModifierSchema, and
aiRewardConditionSchema to enforce event-specific rules using
REWARD_CONDITIONS[event] (or validate the completed draft before returning it).
Require click and lead rewards, including modifiers, to be flat with
amountInCents and duration 0, and reject conditions whose entity, attribute, or
operator is not permitted for the selected event. Ensure generate-reward
structured output cannot pass invalid combinations through safeParse to the
builder.

In `@apps/web/ui/partners/rewards/ai-reward-builder.tsx`:
- Around line 452-457: Update the onKeyDown handler in the AI reward builder to
avoid calling generate() when Enter is pressed during IME composition, while
preserving the existing preventDefault and submission behavior for non-composing
Enter without Shift. Use the keyboard event’s composition-state indicator before
invoking generate().
- Around line 827-834: Update the “Try again” Button’s onClick behavior to
preserve the current prompt when calling requestExit, allowing the user to
resend it without retyping; alternatively, rename the button text to “Dismiss”
if discarding the prompt is the intended behavior.
- Around line 442-451: Update the focus management around the textarea and
action row in the AI reward builder to track focus on their shared container
rather than the textarea alone. Use the wrapper’s onFocus/onBlur and
relatedTarget containment check to keep focused true while focus remains inside,
preventing the presets and Generate button from unmounting during keyboard
navigation; retain the delayed close only when focus leaves the container.

---

Nitpick comments:
In `@apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx`:
- Around line 683-775: The referral and non-referral branches duplicate the
reward description editor markup. Extract the shared Pen2 toggle, animated
description/tooltip fields, and clear-button logic into a local
RewardDescriptionFields component that reads and updates description and
tooltipDescription through the form context, then render that component in both
branches while leaving their distinct titles and card content unchanged.
- Around line 323-330: Stabilize the getValues and reset adapter props passed to
useAIRewardBuilder in RewardSheetContent by wrapping each in useCallback with
appropriate dependencies. Add the React useCallback import if needed, while
preserving their current behavior and type conversions so ensureSnapshot,
applyDraft, and generate retain memoized callbacks across renders.
- Around line 970-982: Remove the optional className prop from the
RewardSheetCard component props and eliminate its use in the cn call, preserving
the existing card styles and all other props.

In `@apps/web/ui/partners/rewards/ai-reward-builder.tsx`:
- Around line 269-276: Update the streamed partial handling in the generation
loop and the modifier conversion used by buildRewardFormValuesFromDraft so
existing modifier ids are reused by index across chunks instead of generating a
new uuid for every reset. Preserve stable ids for unchanged rows while assigning
uuids only to newly added modifiers, preventing RewardsLogic field-array rows
from remounting during streaming.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6aaf745-638f-40b5-963a-0782e574d101

📥 Commits

Reviewing files that changed from the base of the PR and between 1f30d59 and 69e3e38.

📒 Files selected for processing (10)
  • apps/web/lib/ai/ai-reward-schema.ts
  • apps/web/lib/ai/generate-partner-network-invite-email.ts
  • apps/web/lib/ai/generate-reward.ts
  • apps/web/lib/api/links/usage-checks.ts
  • apps/web/lib/upstash/ratelimit-policies.ts
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/ui/partners/rewards/ai-reward-builder.tsx
  • apps/web/ui/partners/rewards/reward-presets.ts
  • packages/ui/src/icons/nucleo/index.ts
  • packages/ui/src/icons/nucleo/sparkle3-fill.tsx

Comment thread apps/web/lib/ai/ai-reward-schema.ts Outdated
Comment thread apps/web/ui/partners/rewards/ai-reward-builder.tsx Outdated
Comment thread apps/web/ui/partners/rewards/ai-reward-builder.tsx
Comment thread apps/web/ui/partners/rewards/ai-reward-builder.tsx
@pepeladeira

Copy link
Copy Markdown
Collaborator Author

@coderabbitai final review. any breaking changes or security issues?

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Changed files ---'
git diff --name-status origin/main...HEAD

echo '--- Diff stat ---'
git diff --stat origin/main...HEAD

echo '--- Outlines ---'
for file in \
  apps/web/lib/ai/ai-reward-schema.ts \
  apps/web/lib/ai/generate-reward.ts \
  apps/web/lib/api/links/usage-checks.ts \
  apps/web/lib/upstash/ratelimit-policies.ts \
  apps/web/ui/partners/rewards/ai-reward-builder.tsx \
  apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx \
  apps/web/ui/partners/rewards/reward-presets.ts
do
  if [ -f "$file" ]; then
    echo "### $file"
    ast-grep outline "$file" --items all || true
  fi
done

echo '--- Full changed-file diffs ---'
git diff --no-ext-diff --unified=80 origin/main...HEAD -- \
  apps/web/lib/ai/ai-reward-schema.ts \
  apps/web/lib/ai/generate-reward.ts \
  apps/web/lib/api/links/usage-checks.ts \
  apps/web/lib/upstash/ratelimit-policies.ts \
  apps/web/ui/partners/rewards/ai-reward-builder.tsx \
  apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx \
  apps/web/ui/partners/rewards/reward-presets.ts

Length of output: 113986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Reward action and schema locations ---'
fd -t f . apps/web | rg '(create-reward|update-reward|rewards\.ts|reward.*schema|partner.*reward)' | head -80

echo '--- Reward action call and validation contexts ---'
rg -n -C 8 --type ts --type tsx \
  '(createRewardAction|updateRewardAction|createOrUpdateRewardSchema|rewardConditionsArraySchema|amountInCents)' \
  apps/web/lib/actions apps/web/lib/zod apps/web/ui/partners/rewards 2>/dev/null | head -500

echo '--- AI credit helper callers and AI usage model fields ---'
rg -n -C 6 --type ts \
  '\b(reserveAIUsageCredit|refundAIUsageCredit|throwIfAIUsageExceeded|aiUsage|aiLimit)\b' \
  apps/web | head -500

echo '--- Changed invite-email implementation and base version ---'
sed -n '1,280p' apps/web/lib/ai/generate-partner-network-invite-email.ts
echo '--- BASE ---'
git show origin/main:apps/web/lib/ai/generate-partner-network-invite-email.ts | sed -n '1,280p'

echo '--- Exact changed file line numbers ---'
nl -ba apps/web/lib/ai/generate-reward.ts | sed -n '60,175p'
nl -ba apps/web/lib/ai/ai-reward-schema.ts | sed -n '1,180p'
nl -ba apps/web/ui/partners/rewards/ai-reward-builder.tsx | sed -n '30,140p'

Length of output: 3825


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in \
  apps/web/lib/actions/partners/create-reward.ts \
  apps/web/lib/actions/partners/update-reward.ts \
  apps/web/lib/zod/schemas/rewards.ts \
  apps/web/lib/ai/generate-partner-network-invite-email.ts \
  apps/web/lib/api/links/usage-checks.ts
do
  echo "### OUTLINE $file"
  ast-grep outline "$file" --items all || true
done

echo '--- reward actions ---'
cat -n apps/web/lib/actions/partners/create-reward.ts
cat -n apps/web/lib/actions/partners/update-reward.ts

echo '--- reward schema relevant declarations ---'
rg -n -C 10 \
  '(createOrUpdateRewardSchema|rewardConditionBaseSchema|rewardConditionsSchema|rewardConditionsArraySchema|amountInCents)' \
  apps/web/lib/zod/schemas/rewards.ts

echo '--- shared helper call sites ---'
rg -n -C 10 \
  '(reserveAIUsageCredit|refundAIUsageCredit|throwIfAIUsageExceeded)' \
  apps/web

echo '--- invite generator: current then base ---'
cat -n apps/web/lib/ai/generate-partner-network-invite-email.ts
echo '--- BASE ---'
git show origin/main:apps/web/lib/ai/generate-partner-network-invite-email.ts | cat -n

echo '--- generated-draft form conversion ---'
sed -n '30,135p' apps/web/ui/partners/rewards/ai-reward-builder.tsx | cat -n

Length of output: 68346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Reward payload conversion ---'
nl -ba apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx | sed -n '120,210p'

echo '--- Persisted reward validation ---'
fd -t f 'validate-reward.ts' apps/web -x sh -c 'echo "### $1"; cat -n "$1"' sh {}

echo '--- Amount schema units ---'
nl -ba apps/web/lib/zod/schemas/rewards.ts | sed -n '330,430p'

echo '--- Complete reward condition validation ---'
nl -ba apps/web/lib/zod/schemas/rewards.ts | sed -n '316,372p'

Length of output: 874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Reward payload conversion ---'
sed -n '120,210p' apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx | cat -n

echo '--- Persisted reward validation ---'
fd -t f -a 'validate-reward.ts' apps/web -x sh -c 'echo "### $1"; cat -n "$1"' sh {}

echo '--- Amount schema units ---'
sed -n '330,430p' apps/web/lib/zod/schemas/rewards.ts | cat -n

echo '--- Complete reward condition validation ---'
sed -n '316,372p' apps/web/lib/zod/schemas/rewards.ts | cat -n

Length of output: 17368


@pepeladeira Final review complete.

I found no blocking breaking changes or security issues in PR #4277.

The reviewed paths enforce workspace membership, role permissions, plan access, rate limits, atomic AI-credit reservation, and schema validation. Generated rewards still pass through the existing reward validation before persistence. Flat reward amounts use dollar values in the form and convert to cents in getRewardPayload, so the AI draft conversion is correct.

I did not run runtime tests in this review environment.

You are interacting with an AI system.

@pepeladeira

Copy link
Copy Markdown
Collaborator Author

@coderabbitai final review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base_ref="origin/main"

echo '--- Current commits and changed files ---'
git log --oneline "${base_ref}..HEAD"
git diff --name-status "${base_ref}...HEAD"

echo '--- Changed-file diff: server and validation paths ---'
git diff --no-ext-diff --unified=60 "${base_ref}...HEAD" -- \
  apps/web/lib/ai/generate-reward.ts \
  apps/web/lib/ai/ai-reward-schema.ts \
  apps/web/lib/api/links/usage-checks.ts \
  apps/web/lib/upstash/ratelimit-policies.ts \
  apps/web/lib/ai/generate-partner-network-invite-email.ts

echo '--- Structure map: client reward flow ---'
for file in \
  apps/web/ui/partners/rewards/ai-reward-builder.tsx \
  apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx \
  apps/web/ui/partners/rewards/reward-presets.ts \
  apps/web/lib/actions/partners/create-reward.ts \
  apps/web/lib/actions/partners/update-reward.ts \
  apps/web/lib/api/rewards/validate-reward.ts
do
  if [ -f "$file" ]; then
    echo "### $file"
    ast-grep outline "$file" --items all || true
  fi
done

echo '--- Exact security and persistence contexts ---'
for file in \
  apps/web/lib/ai/generate-reward.ts \
  apps/web/lib/api/links/usage-checks.ts \
  apps/web/lib/actions/partners/create-reward.ts \
  apps/web/lib/actions/partners/update-reward.ts \
  apps/web/lib/api/rewards/validate-reward.ts \
  apps/web/ui/partners/rewards/ai-reward-builder.tsx \
  apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
do
  if [ -f "$file" ]; then
    echo "### $file"
    cat -n "$file"
  fi
done

echo '--- AI generator call sites and form submission flow ---'
rg -n -C 7 --type ts --type tsx \
  '\b(generateReward|reserveAIUsageCredit|refundAIUsageCredit|getRewardPayload|createRewardAction|updateRewardAction|validateReward)\b' \
  apps/web

Length of output: 162702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Prisma Project fields and table mapping ---'
fd -t f -a 'schema.prisma' . -x sh -c '
  echo "### $1"
  rg -n -A130 -B8 "^model Project\\b|\\b(aiUsage|aiLimit)\\b|@@map" "$1" | head -260
' sh {}

echo '--- Reward-condition schema and event catalog ---'
sed -n '1,430p' apps/web/lib/zod/schemas/rewards.ts | cat -n

echo '--- AI reward schema test ---'
cat -n apps/web/tests/misc/ai-reward-schema.test.ts

echo '--- Current UI and schema diff ---'
git diff --no-ext-diff --unified=40 origin/main...HEAD -- \
  apps/web/lib/zod/schemas/rewards.ts \
  apps/web/ui/partners/rewards/ai-reward-builder.tsx \
  apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx \
  apps/web/ui/partners/rewards/reward-presets.ts \
  apps/web/tests/misc/ai-reward-schema.test.ts

echo '--- Portable call-site and error-flow search ---'
rg -n -C 5 -g '*.ts' -g '*.tsx' \
  '\b(generateReward|reserveAIUsageCredit|refundAIUsageCredit|getAIRewardSchema|rewardConditionsArraySchema)\b' \
  apps/web

echo '--- Raw Project SQL conventions ---'
rg -n -C 4 -g '*.ts' -g '*.tsx' \
  '(\$executeRaw|UPDATE[[:space:]]+Project|aiUsage|aiLimit)' \
  apps packages

Length of output: 180095


@pepeladeira Final review complete for #4277.

I found no blocking breaking changes or security issues.

The AI reward action verifies authentication, workspace membership, user role, plan capability, rate limits, and AI usage limits. The AI usage reservation uses an atomic conditional update. Concurrent requests cannot exceed the persisted aiLimit.

The streamed output receives event-specific schema validation. The existing reward create and update actions apply their server-side validation before persistence. The client conversion correctly converts flat reward amounts and currency condition values from dollars to cents.

The new schema tests cover the reward-condition catalog, metadata requirements, percentage limits, and click/lead constraints.

I did not run runtime tests in this read-only review environment.


🧠 Learnings used
Learnt from: pepeladeira
Repo: dubinc/dub PR: 3923
File: apps/web/lib/actions/partners/update-reward.ts:160-168
Timestamp: 2026-05-18T22:18:20.575Z
Learning: In dubinc/dub, `updateRewardAction` (apps/web/lib/actions/partners/update-reward.ts) only updates the `Reward` row and does NOT touch `programEnrollment` rows or queue any enrollment sync cron job. The enrollment sync cron at `/api/cron/rewards/sync-enrollments` only handles `create` and `delete` actions — there is no update-action cron path. Therefore, calling `notifyPartnersRewardChanged` directly inside `waitUntil(Promise.allSettled([...]))` within `updateRewardAction` is the correct pattern. Do NOT flag this as "premature notification before enrollment sync finalization" or as a duplication risk.

Learnt from: pepeladeira
Repo: dubinc/dub PR: 3923
File: apps/web/lib/actions/partners/update-reward.ts:160-168
Timestamp: 2026-05-18T22:18:20.575Z
Learning: In dubinc/dub, `updateRewardAction` (apps/web/lib/actions/partners/update-reward.ts) only updates the `Reward` row and does NOT write to `programEnrollment` rows. The enrollment sync cron (`/api/cron/rewards/sync-enrollments`) only handles `create` and `delete` actions — there is no "update" enrollment sync. Therefore, calling `notifyPartnersRewardChanged` directly inside `waitUntil(Promise.allSettled([...]))` within `updateRewardAction` is the correct and complete pattern for reward-update partner notifications. Do NOT flag this as premature notification or potential duplication.

Learnt from: pepeladeira
Repo: dubinc/dub PR: 3923
File: apps/web/lib/actions/partners/create-reward.ts:111-118
Timestamp: 2026-05-18T22:15:06.052Z
Learning: In dubinc/dub, the established pattern for durable background jobs is to commit the DB change first, then `await` the QStash publish immediately after (e.g., `markDomainAsDeleted`, `update-partner-profile`, `confirm-payouts`, `merge-partner-accounts`, `create-reward`). Do NOT flag this as a missing transactional-outbox or partial-apply risk — it is an intentional, accepted pattern across the codebase.

You are interacting with an AI system.

@steven-tey
steven-tey enabled auto-merge (squash) August 16, 2026 02:17
@steven-tey
steven-tey merged commit 6417fe7 into main Aug 16, 2026
11 of 12 checks passed
@steven-tey
steven-tey deleted the ai-reward-builder branch August 16, 2026 02:32
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