Conversation
📝 WalkthroughWalkthroughImplements monthly message usage tracking for non-pro users by adding a Changes
Sequence DiagramsequenceDiagram
actor User
participant Frontend as Client/UI
participant API as API Route
participant Convex as Convex System
participant DB as Database
rect rgba(100, 150, 200, 0.5)
Note over User,DB: Message Submission Flow
User->>Frontend: Submit message
Frontend->>API: POST /api/messages
rect rgba(150, 100, 200, 0.5)
Note over API,DB: Usage Check (Non-Pro)
API->>Convex: checkAndIncrementUsage(userId, limit)
Convex->>DB: Query usage by userId
DB-->>Convex: Return usage record
alt Current month
Convex->>Convex: Validate count < limit
alt Allowed
Convex->>DB: Increment messageCount
Convex-->>API: { allowed: true, count }
else Limit reached
Convex-->>API: { allowed: false, count, limit }
API-->>Frontend: 429 Too Many Requests
end
else New month
Convex->>DB: Reset usage (new period)
Convex->>DB: Increment messageCount
Convex-->>API: { allowed: true, count }
end
end
rect rgba(200, 150, 100, 0.5)
Note over Frontend,DB: Render Usage State
Frontend->>Convex: getUserUsage()
Convex->>DB: Query usage by userId
DB-->>Convex: Return messageCount, periodStart
Convex-->>Frontend: Usage data
Frontend->>Frontend: Display banner/disable input
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@convex/system.ts`:
- Around line 643-646: The early-return for the missing `record` currently
returns periodStart = Date.now(), which is inconsistent with the later computed
`periodStart` (start of current month); change the `!record` branch to compute
the same month-start timestamp as the main path (use new Date() and
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) or reuse the same
`periodStart` calculation) so the returned `{ messageCount: 0, periodStart }`
uses the same month-start semantics as the rest of the function.
In `@src/app/api/messages/route.ts`:
- Around line 38-49: The current code calls
convex.mutation(api.system.checkAndIncrementUsage, ...) before validating the
conversation and before writing the message, which can consume quota on
failures; move the quota check/increment (api.system.checkAndIncrementUsage) to
after the conversation validation (the conversation lookup/validation logic in
this route) and before the message write so you only consume quota for a
validated send. Better: implement a single Convex mutation that performs
conversation validation, usage check-and-increment, and the message creation in
one atomic server-side operation (fold api.system.checkAndIncrementUsage + the
conversation validation + the message creation mutation into one mutation) so
all three succeed or fail together.
In `@src/features/conversations/components/conversation-sidebar.tsx`:
- Around line 237-238: The submit button currently disables when atLimit is true
even if an operation is in progress, preventing the stop/cancel path in
handleSubmit; update the PromptInputSubmit disabled prop so the button is
enabled whenever isProcessing is true, e.g. replace the expression
disabled={atLimit || (isProcessing ? false : !input)} with a condition that only
disables when not processing: disabled={!isProcessing && (atLimit || !input)},
ensuring stop/cancel remains clickable during processing.
- Around line 129-139: The catch block in conversation-sidebar.tsx is clearing
the input after failures; modify the send handler (the function that catches
errors) so it does not clear the input state on error — move the input-clearing
logic to the success path only, and add an early return inside the catch branch
(for both the 429 HTTPError case and the generic error case) so the draft is
preserved; use the existing identifiers (the catch block, toast.error calls,
openUserProfile, and the input state variable) to locate and update the code.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3d733b8f-ae08-493a-a23e-53e3a07bbc72
📒 Files selected for processing (4)
convex/schema.tsconvex/system.tssrc/app/api/messages/route.tssrc/features/conversations/components/conversation-sidebar.tsx
| if (!record) return { messageCount: 0, periodStart: Date.now() }; | ||
|
|
||
| const now = new Date(); | ||
| const periodStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1); |
There was a problem hiding this comment.
Return a consistent billing window start.
The !record branch returns Date.now(), but the rest of the function treats periodStart as the start of the current month. That makes the same field mean two different things.
Proposed fix
export const getUserUsage = query({
args: {},
handler: async (ctx) => {
+ const now = new Date();
+ const periodStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1);
+
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
const userId = identity.subject;
const record = await ctx.db
.query("usage")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.first();
- if (!record) return { messageCount: 0, periodStart: Date.now() };
-
- const now = new Date();
- const periodStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1);
+ if (!record) return { messageCount: 0, periodStart };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@convex/system.ts` around lines 643 - 646, The early-return for the missing
`record` currently returns periodStart = Date.now(), which is inconsistent with
the later computed `periodStart` (start of current month); change the `!record`
branch to compute the same month-start timestamp as the main path (use new
Date() and Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) or reuse the
same `periodStart` calculation) so the returned `{ messageCount: 0, periodStart
}` uses the same month-start semantics as the rest of the function.
| if (!hasPro) { | ||
| const usage = await convex.mutation(api.system.checkAndIncrementUsage, { | ||
| internalKey, | ||
| userId, | ||
| limit: FREE_MESSAGE_LIMIT, | ||
| }); | ||
| if (!usage.allowed) { | ||
| return NextResponse.json( | ||
| { error: "Free message limit reached", count: usage.count, limit: FREE_MESSAGE_LIMIT }, | ||
| { status: 429 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Don't burn quota before the send can actually proceed.
This increments usage before the conversation lookup on Lines 53-63 and before any message write. A bad conversationId or later failure will still consume one of the user's monthly messages. At minimum, move the increment after validation; ideally, fold validation, quota accounting, and message creation into a single Convex mutation so they commit together.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/messages/route.ts` around lines 38 - 49, The current code calls
convex.mutation(api.system.checkAndIncrementUsage, ...) before validating the
conversation and before writing the message, which can consume quota on
failures; move the quota check/increment (api.system.checkAndIncrementUsage) to
after the conversation validation (the conversation lookup/validation logic in
this route) and before the message write so you only consume quota for a
validated send. Better: implement a single Convex mutation that performs
conversation validation, usage check-and-increment, and the message creation in
one atomic server-side operation (fold api.system.checkAndIncrementUsage + the
conversation validation + the message creation mutation into one mutation) so
all three succeed or fail together.
| } catch (error) { | ||
| if (error instanceof HTTPError && error.response.status === 429) { | ||
| toast.error("You've used all 10 free messages this month.", { | ||
| action: { | ||
| label: "Upgrade to Pro", | ||
| onClick: () => openUserProfile(), | ||
| }, | ||
| }); | ||
| } else { | ||
| toast.error("Message failed to send"); | ||
| } |
There was a problem hiding this comment.
Keep the draft when the send fails.
Because Line 142 still clears input after this catch, a 429 or transient failure discards the user's prompt even though nothing was sent. Clear the input only on success, and return from the catch path.
Proposed fix
try {
await ky.post("/api/messages", {
json: {
conversationId,
message: message.text,
},
});
+ setInput("");
} catch (error) {
if (error instanceof HTTPError && error.response.status === 429) {
toast.error("You've used all 10 free messages this month.", {
action: {
label: "Upgrade to Pro",
onClick: () => openUserProfile(),
},
});
} else {
toast.error("Message failed to send");
}
+ return;
}
-
- setInput("");
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/conversations/components/conversation-sidebar.tsx` around lines
129 - 139, The catch block in conversation-sidebar.tsx is clearing the input
after failures; modify the send handler (the function that catches errors) so it
does not clear the input state on error — move the input-clearing logic to the
success path only, and add an early return inside the catch branch (for both the
429 HTTPError case and the generic error case) so the draft is preserved; use
the existing identifiers (the catch block, toast.error calls, openUserProfile,
and the input state variable) to locate and update the code.
| <PromptInputSubmit | ||
| disabled={isProcessing ? false : !input} | ||
| disabled={atLimit || (isProcessing ? false : !input)} |
There was a problem hiding this comment.
Don't disable the stop action at the quota boundary.
On the 10th free message, isProcessing can be true while atLimit is also true. This expression disables submit in that state, so the user can't trigger the stop/cancel path in handleSubmit.
Proposed fix
- disabled={atLimit || (isProcessing ? false : !input)}
+ disabled={isProcessing ? false : atLimit || !input}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/conversations/components/conversation-sidebar.tsx` around lines
237 - 238, The submit button currently disables when atLimit is true even if an
operation is in progress, preventing the stop/cancel path in handleSubmit;
update the PromptInputSubmit disabled prop so the button is enabled whenever
isProcessing is true, e.g. replace the expression disabled={atLimit ||
(isProcessing ? false : !input)} with a condition that only disables when not
processing: disabled={!isProcessing && (atLimit || !input)}, ensuring
stop/cancel remains clickable during processing.
Summary by CodeRabbit