Skip to content

Conversation

@rossmanko
Copy link
Contributor

@rossmanko rossmanko commented Sep 1, 2025

Summary by CodeRabbit

  • New Features

    • Interactive setup CLI that generates .env.local.
    • Full per-message feedback flow (UI components, hook, server mutation, DB schema) with thumbs up/down and optional details.
    • Token-length validation on paste and submit with toast errors.
    • Contextual Upgrade button for rate-limit errors and mobile-aware toast placement.
    • Help Center entry in user menu.
  • Documentation

    • README reorganized into Getting started with Demo link; .env example reordered and base URL example added.
  • Chores

    • Added setup script, new dependencies (including OpenRouter SDK and Radix UI packages) and multiple dependency upgrades.
  • Bug Fixes

    • Clarified rate-limit messaging and adjusted rate-limit check timing; persisted messages now include userId.

@vercel
Copy link

vercel bot commented Sep 1, 2025

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

Project Deployment Preview Comments Updated (UTC)
hackerai Ready Ready Preview Comment Sep 2, 2025 6:45pm

@rossmanko rossmanko changed the title Daily branch 202025 09 01 Daily branch 2025 09 01 Sep 1, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds token-count enforcement for paste and submit, exports MAX_TOKENS and countInputTokens, implements per-message feedback UI/hook and Convex persistence/schema, centralizes ownership checks in messages, adds an interactive setup script and env example reorder, updates README and dependencies, and refines rate-limit/Toaster/upgrade UI.

Changes

Cohort / File(s) Summary
Token utilities & enforcement
app/components/ChatInput.tsx, app/hooks/useChatHandlers.ts, lib/token-utils.ts
Exported MAX_TOKENS and added countInputTokens. Paste is blocked with a toast if tokenized content > MAX_TOKENS. Submissions are validated (input + file tokens) and aborted with a toast when exceeding MAX_TOKENS.
Per-message feedback (UI, hook, server, schema)
app/components/FeedbackInput.tsx, app/components/MessageActions.tsx, app/components/Messages.tsx, app/hooks/useFeedback.ts, convex/feedback.ts, convex/schema.ts, types/chat.ts, lib/utils.ts
Adds FeedbackInput component and useFeedback hook; MessageActions/Messages accept feedback props and setMessages; Convex schema adds feedback table and feedback_id; server mutation createFeedback persists/updates feedback; types updated to include feedback metadata.
Message persistence & ownership (Convex + DB actions + API)
convex/messages.ts, convex/chats.ts, lib/db/actions.ts, app/api/chat/route.ts
Centralized ownership verification in convex/messages.ts (new internalQuery verify); removed exported verify from chats; saveMessage now requires userId and persists user_id; chat API delays rate-limit check and includes userId when saving messages.
Message processing & types
lib/utils/message-processor.ts, lib/utils.ts, lib/utils/message-utils.ts, types/chat.ts
Switched processing types to ChatMessage (includes MessageMetadata); introduced exported MessageRecord and updated convertToUIMessages to map feedback into metadata; strengthened message role typing and helper signatures.
Interactive setup & env template
scripts/setup.ts, .env.local.example
Adds scripts/setup.ts interactive CLI (prompts, Convex setup, generates WorkOS cookie, writes .env.local) and reorders .env.local.example (moves Convex service role key, adds commented NEXT_PUBLIC_BASE_URL example).
Docs & package metadata
README.md, package.json
Reworked README onboarding/formatting; added setup script; bumped many dependencies and added new ones (Radix UI packages, @openrouter/ai-sdk-provider, chalk, etc.).
Rate-limit, upgrade UI & Toaster
lib/rate-limit.ts, app/components/MessageErrorState.tsx, components/ui/sonner.tsx
Rate-limit message conditional on Pro status; MessageErrorState now shows in-app Upgrade button via useUpgrade; Toaster placement becomes mobile-aware via useIsMobile.
File transform & logging cleanup
lib/utils/file-transform-utils.ts
Removed noisy console.log statements and replaced failure log with console.error; behavior unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CI as ChatInput
  participant T as token-utils
  participant Toast as Toaster

  rect rgb(249,250,255)
  note right of CI #DDEBF7: Paste validation
  U->>CI: Paste text
  CI->>T: countInputTokens(pasteText)
  T-->>CI: tokenCount
  alt tokenCount > MAX_TOKENS
    CI->>Toast: show error toast ("Paste too long")
    CI-->>U: prevent paste
  else
    CI-->>U: allow paste (handlePasteEvent)
  end
  end
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant H as useChatHandlers
  participant T as token-utils
  participant Toast as Toaster
  participant API as Chat API

  rect rgb(245,255,245)
  note right of H #E6F4EA: Submit validation
  U->>H: Submit message (+files)
  H->>T: countInputTokens(input, uploadedFiles)
  T-->>H: totalTokens
  alt totalTokens > MAX_TOKENS
    H->>Toast: show error toast ("Message is too long")
    H-->>U: abort submit
  else
    H->>API: create/send chat/message (includes userId)
    API-->>H: ack/stream
    H-->>U: success
  end
  end
Loading
sequenceDiagram
  autonumber
  participant Dev as Developer
  participant S as scripts/setup.ts
  participant C as Convex CLI
  participant FS as Filesystem

  Dev->>S: pnpm run setup
  S->>Dev: prompt for API keys & validate
  S->>C: run convex dev --configure=new --project=NAME
  C-->>S: generates .env.local (with NEXT_PUBLIC_CONVEX_URL)
  S->>FS: write consolidated .env.local (keys + WorkOS cookie + comments)
  S-->>Dev: print next steps
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I counted tokens, quick and bright,
Blocked long pastes that grew at night.
I seeded envs and ran the script,
Stored tiny thumbs and notes to crypt.
Hops of feedback—soft and light. ✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 79d0e65 and f5d411e.

📒 Files selected for processing (9)
  • app/components/FeedbackInput.tsx (1 hunks)
  • app/components/Messages.tsx (4 hunks)
  • app/components/SidebarUserNav.tsx (4 hunks)
  • app/hooks/useFeedback.ts (1 hunks)
  • convex/messages.ts (11 hunks)
  • convex/schema.ts (2 hunks)
  • lib/db/actions.ts (4 hunks)
  • lib/utils.ts (2 hunks)
  • lib/utils/message-utils.ts (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch daily-branch-202025-09-01

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

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

🧹 Nitpick comments (8)
.env.local.example (1)

55-55: Standardize base URL (no trailing slash) + fix EOF newline.

Avoid double slashes when composing paths and satisfy dotenv-linter EndingBlankLine.

-# NEXT_PUBLIC_BASE_URL=http://localhost:3000/
+# NEXT_PUBLIC_BASE_URL=http://localhost:3000
+
README.md (1)

42-52: Avoid duplicate installs in the flow.

Right now users run pnpm install and then pnpm run setup, which runs install again. Either remove the “Install dependencies” step from the README or drop the install from the setup script in package.json.

I’ve proposed a package.json change below to remove the second install and pin tsx; alternatively, keep README and keep install inside setup—choose one.

package.json (1)

11-13: Make setup faster and deterministic: use local tsx, not npx, and drop the extra install.

This avoids a second install and pins the runner.

-    "format:check": "prettier --check .",
-    "setup": "pnpm install && npx tsx scripts/setup.ts"
+    "format:check": "prettier --check .",
+    "setup": "pnpm tsx scripts/setup.ts"

Also add tsx to devDependencies:

   "devDependencies": {
@@
-    "typescript": "^5"
+    "typescript": "^5",
+    "tsx": "^4"
   }
scripts/setup.ts (1)

77-83: Validate WorkOS Client ID.

Prompt currently accepts empty/invalid IDs; add a simple format check.

 async function getWorkOSClientId(): Promise<string> {
   console.log(`\n${chalk.bold("Getting WorkOS Client ID")}`);
   console.log(
     'You can find your WorkOS Client ID in the dashboard under the "Quick start" section: https://dashboard.workos.com/get-started',
   );
-  return await question("Enter your WorkOS Client ID: ");
+  const id = await question("Enter your WorkOS Client ID: ");
+  if (!id.trim().startsWith("client_")) {
+    console.log(chalk.red("Invalid WorkOS Client ID format"));
+    console.log('Client IDs should start with "client_"');
+    return await getWorkOSClientId();
+  }
+  return id.trim();
 }
app/components/ChatInput.tsx (1)

90-112: Prevent “paste passes, submit blocks” by counting existing input + files; add text/plain fallback

Right now you only validate the pasted text in isolation. Consider validating the combined size of current textarea content + pasted text + uploaded files to avoid surprising users at submit time. Also add a fallback for "text/plain" to improve cross-browser support. Update deps so the listener sees fresh files.

   useEffect(() => {
     const handlePaste = async (e: ClipboardEvent) => {
       // Only handle paste if the textarea is focused
       if (textareaRef.current === document.activeElement) {
-        // Check if pasting text content
-        const clipboardData = e.clipboardData;
+        // Check if pasting text content
+        const clipboardData = e.clipboardData;
         if (clipboardData) {
-          const pastedText = clipboardData.getData("text");
+          const pastedText =
+            clipboardData.getData("text") || clipboardData.getData("text/plain");
 
           if (pastedText) {
-            // Check token limit for the pasted text only
-            const tokenCount = countInputTokens(pastedText, []);
-            if (tokenCount > MAX_TOKENS) {
+            // Validate combined size: current input + pasted text + files
+            const currentInput = textareaRef.current?.value ?? "";
+            const combinedTokenCount = countInputTokens(
+              currentInput + pastedText,
+              uploadedFiles,
+            );
+            if (combinedTokenCount > MAX_TOKENS) {
               e.preventDefault();
-              toast.error("Content is too long to paste", {
-                description: `The content you're trying to paste is too large (${tokenCount.toLocaleString()} tokens). Please copy a smaller amount.`,
+              toast.error("Content is too long to paste", {
+                description: `Pasting this would exceed the limit (${combinedTokenCount.toLocaleString()} tokens including existing input${
+                  uploadedFiles.length ? " and files" : ""
+                }). Please paste less.`,
               });
               return;
             }
           }
         }
 
         const filesProcessed = await handlePasteEvent(e);
         // If files were processed, the event.preventDefault() is already called
         // in handlePasteEvent, so no additional action needed here
       }
     };
 
     document.addEventListener("paste", handlePaste);
     return () => {
       document.removeEventListener("paste", handlePaste);
     };
-  }, [handlePasteEvent]);
+  }, [handlePasteEvent, uploadedFiles]);

Also applies to: 119-123

lib/token-utils.ts (2)

4-4: Exporting MAX_TOKENS is good; consider single source of truth config

Looks good. Consider reading this from a shared config/env to keep client and server aligned (e.g., NEXT_PUBLIC_MAX_INPUT_TOKENS) if model/context changes.


126-139: Guard against malformed file token metadata (negative/NaN)

Clamp file token contributions to non-negative to avoid accidental underflow from bad metadata.

 export const countInputTokens = (
   input: string,
   uploadedFiles: Array<{ tokens?: number }> = [],
 ): number => {
   const textTokens = countTokens(input);
-  const fileTokens = uploadedFiles.reduce(
-    (total, file) => total + (file.tokens || 0),
-    0,
-  );
+  const fileTokens = uploadedFiles.reduce((total, file) => {
+    const t = Number.isFinite(file.tokens) ? (file.tokens as number) : 0;
+    return total + Math.max(0, t);
+  }, 0);
   return textTokens + fileTokens;
 };
app/hooks/useChatHandlers.ts (1)

66-74: Avoid false positives: count tokens on trimmed input and only valid files

Use the same valid-file criteria you later apply when sending to prevent over-counting (e.g., files not fully uploaded). Also match the actual text sent by counting on trimmed input.

-      const tokenCount = countInputTokens(input, uploadedFiles);
-      if (tokenCount > MAX_TOKENS) {
-        const hasFiles = uploadedFiles.length > 0;
+      const trimmed = input.trim();
+      const filesForCounting = uploadedFiles.filter(
+        (f) => f.uploaded && f.url && f.fileId,
+      );
+      const tokenCount = countInputTokens(trimmed, filesForCounting);
+      if (tokenCount > MAX_TOKENS) {
+        const hasFiles = filesForCounting.length > 0;
         toast.error("Message is too long", {
           description: `Your message is too large (${tokenCount.toLocaleString()} tokens). Please make it shorter${
-            hasFiles ? " or remove some files" : ""
+            hasFiles ? " or remove some files" : ""
           }.`,
         });
         return;
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 555fd75 and 37b40ba.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • .env.local.example (2 hunks)
  • README.md (1 hunks)
  • app/components/ChatInput.tsx (2 hunks)
  • app/hooks/useChatHandlers.ts (2 hunks)
  • lib/token-utils.ts (2 hunks)
  • package.json (3 hunks)
  • scripts/setup.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • scripts/setup.ts
  • app/hooks/useChatHandlers.ts
  • app/components/ChatInput.tsx
  • lib/token-utils.ts
package.json

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

Add @types/node to devDependencies when using Node.js built-in modules

Files:

  • package.json
🧠 Learnings (1)
📚 Learning: 2025-08-27T12:38:06.615Z
Learnt from: RostyslavManko
PR: hackerai-tech/hackerai#14
File: convex/chats.ts:4-24
Timestamp: 2025-08-27T12:38:06.615Z
Learning: Convex functions with serviceKey parameters are designed for backend service-to-service communication and don't require ctx.auth.getUserIdentity() checks. The serviceKey validation against process.env.CONVEX_SERVICE_ROLE_KEY provides sufficient authentication for these backend operations.

Applied to files:

  • .env.local.example
🧬 Code graph analysis (2)
app/hooks/useChatHandlers.ts (1)
lib/token-utils.ts (2)
  • countInputTokens (129-139)
  • MAX_TOKENS (4-4)
app/components/ChatInput.tsx (1)
lib/token-utils.ts (2)
  • countInputTokens (129-139)
  • MAX_TOKENS (4-4)
🪛 dotenv-linter (3.3.0)
.env.local.example

[warning] 55-55: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🔇 Additional comments (5)
.env.local.example (1)

8-10: Clarify Service Role Key usage (server-to-server only).

"for securing public functions" is misleading for Convex. Service Role Keys are for server-to-server calls (e.g., Next.js API routes) and must never be exposed client-side.

Apply:

-# Convex Service Role Key (Required for securing public functions)
-# Generate a secure random string and add it to your Convex environment variables
+# Convex Service Role Key (server-to-server only; DO NOT expose to the client)
+# Used when calling Convex internal functions from Next.js API routes or server components. Optional for local dev.
 # CONVEX_SERVICE_ROLE_KEY=
README.md (1)

26-29: Clarify if both OpenRouter and OpenAI are required or if either is sufficient.

If either provider is acceptable, say “OpenRouter or OpenAI” and mark the other optional to reduce friction.

Suggested wording: “You’ll need an OpenRouter or OpenAI account (at least one), plus Convex and WorkOS accounts.”

package.json (1)

65-68: Good: @types/node present as required by guidelines.

Meets the “add @types/node when using Node built-ins” rule.

app/components/ChatInput.tsx (1)

27-29: LGTM: shared token utils + toast import

Centralizing token constants and using a common counter is the right move for consistency.

app/hooks/useChatHandlers.ts (1)

7-9: LGTM: shared token utils + toast import

Consistent usage across paste and submit paths.

Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
package.json (1)

15-64: Fix peer dependency mismatches and lock toolchain

  • Unmet peers: openai@5.16.0, @browserbasehq/stagehand@1.14.0, and zod-to-json-schema@3.24.6 require zod@^3.x but zod@4.1.5 is installed; add a pnpm.overrides entry or align versions.
  • chalk@5.x is ESM-only—replace any require('chalk') with import chalk from 'chalk'.
  • Pin Node engine and sync packageManager version for CI consistency.
  "private": true,
  "engines": {
    "node": ">=18.18.0"
  },
- "packageManager": "pnpm@9",
+ "packageManager": "pnpm@10.15.0",
🧹 Nitpick comments (1)
package.json (1)

11-12: Prefer pnpm dlx for setup script and confirm ESM chalk import

  • In package.json, replace
    "setup": "pnpm install && npx tsx scripts/setup.ts"
    with
    "setup": "pnpm install && pnpm dlx tsx scripts/setup.ts".
  • scripts/setup.ts already uses import chalk from "chalk" and contains no require('chalk').
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 37b40ba and 5d977eb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • package.json (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
package.json

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

Add @types/node to devDependencies when using Node.js built-in modules

Files:

  • package.json
🔇 Additional comments (1)
package.json (1)

72-72: eslint-config-next matches Next version — LGTM.

Version parity with next 15.5.2 is correct.

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
lib/rate-limit.ts (1)

59-71: Bug: error.type mismatches UI check ("rate_limit" vs "rate_limit:chat")

MessageErrorState gates the Upgrade button on error.type === "rate_limit", but here you throw "rate_limit:chat". The button and rate-limit copy won’t render. Fix either here or in the UI.

Option A (normalize type here):

-      throw new ChatSDKError("rate_limit:chat", cause);
+      throw new ChatSDKError("rate_limit", cause);

and in the catch:

-  "rate_limit:chat",
+  "rate_limit",

Option B (preferred for flexibility): keep "rate_limit:chat" and update the UI to startsWith("rate_limit") (see suggested UI diff in MessageErrorState).

🧹 Nitpick comments (4)
lib/rate-limit.ts (2)

52-57: Polish copy: avoid comma splice and keep CTA only for free tier

Small UX nit: split the sentence to avoid a comma splice; keep the CTA for free users only (as you did).

-        cause = `You've reached the current usage cap for HackerAI, please try again after ${timeString}.`;
+        cause = `You've reached the current usage cap for HackerAI. Please try again after ${timeString}.`;
...
-        cause = `You've reached the current usage cap for HackerAI, please try again after ${timeString}.\n\nUpgrade to Pro for higher usage limits and more features.`;
+        cause = `You've reached the current usage cap for HackerAI. Please try again after ${timeString}.\n\nUpgrade to Pro for higher usage limits and more features.`;

6-9: Type userId with Id<'users'> per guidelines

Per repo TS guidelines, prefer Id<'users'> over string for document IDs.

-export const checkRateLimit = async (
-  userId: string,
+import type { Id } from "./_generated/dataModel";
+export const checkRateLimit = async (
+  userId: Id<'users'>,
   isPro: boolean,
 ): Promise<void> => {
app/components/MessageErrorState.tsx (2)

38-46: A11y: indicate progress state on Upgrade button

Expose loading state to assistive tech.

-          <Button
+          <Button
             variant="default"
             size="sm"
             onClick={handleUpgrade}
-            disabled={upgradeLoading}
+            disabled={upgradeLoading}
+            aria-busy={upgradeLoading}
+            aria-live="polite"
           >
             {upgradeLoading ? "Loading..." : "Upgrade"}
           </Button>

16-17: Surface upgrade errors to users

useUpgrade exposes upgradeError; consider showing it under the buttons.

-  const { handleUpgrade, upgradeLoading } = useUpgrade();
+  const { handleUpgrade, upgradeLoading, upgradeError } = useUpgrade();

Add below the buttons block:

{upgradeError && (
  <p className="text-destructive text-xs mt-2" aria-live="polite">
    {upgradeError}
  </p>
)}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5d977eb and 65f443f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • app/components/MessageErrorState.tsx (3 hunks)
  • lib/rate-limit.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • lib/rate-limit.ts
  • app/components/MessageErrorState.tsx
🧬 Code graph analysis (1)
app/components/MessageErrorState.tsx (2)
app/hooks/useUpgrade.ts (1)
  • useUpgrade (5-72)
app/contexts/GlobalState.tsx (1)
  • useGlobalState (297-303)

Copy link

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

Caution

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

⚠️ Outside diff range comments (2)
convex/chats.ts (2)

93-93: Return type should be Doc Id, not string.

ctx.db.insert("chats", …) returns Id<"chats">. The return validator must be v.id("chats").

Apply:

-  returns: v.string(),
+  returns: v.id("chats"),

194-198: Missing return validator on query (violates Convex function contract).

All Convex functions must declare returns. Add a validator for the paginate result.

Apply:

 export const getUserChats = query({
   args: {
     paginationOpts: paginationOptsValidator,
   },
+  returns: v.object({
+    page: v.array(
+      v.object({
+        _id: v.id("chats"),
+        _creationTime: v.number(),
+        id: v.string(),
+        title: v.string(),
+        user_id: v.string(),
+        finish_reason: v.optional(v.string()),
+        todos: v.optional(
+          v.array(
+            v.object({
+              id: v.string(),
+              content: v.string(),
+              status: v.union(
+                v.literal("pending"),
+                v.literal("in_progress"),
+                v.literal("completed"),
+                v.literal("cancelled"),
+              ),
+            }),
+          ),
+        ),
+        update_time: v.number(),
+      }),
+    ),
+    isDone: v.boolean(),
+    continueCursor: v.string(),
+  }),
🧹 Nitpick comments (3)
convex/chats.ts (2)

70-75: Prefer .unique() over .first() when querying by unique index.

This better documents intent and throws if the index isn’t actually unique.

Apply (illustrative for each location):

-      .first();
+      .unique();

Also applies to: 145-152, 253-260


247-251: Same-file internal call: add explicit return type to avoid TS circularity.

Annotate the ctx.runQuery call’s return to satisfy the guideline.

Apply:

-      await ctx.runQuery(internal.chats.verifyChatOwnership, {
+      await (ctx.runQuery(internal.chats.verifyChatOwnership, {
         chatId: args.chatId,
         userId: user.subject,
-      });
+      }) as Promise<null>);
app/api/chat/route.ts (1)

70-72: Rate-limit moved post-initial-save — confirm desired side effects.

This will create/update chats and persist the user message before rejecting on rate limit. If that’s unintended, pre-check rate limit right after resolving { userId, isPro } and again post-truncation if needed.

Potential alternative:

-    // Handle initial chat ...
-    const { isNewChat } = await handleInitialChatAndUserMessage({...});
-
-    // Check rate limit for the user
-    await checkRateLimit(userId, isPro);
+    // Early cheap guard before any DB writes
+    await checkRateLimit(userId, isPro);
+
+    // Handle initial chat ...
+    const { isNewChat } = await handleInitialChatAndUserMessage({...});
+
+    // Optional: re-check here if truncation/flow affects accounting semantics
+    // await checkRateLimit(userId, isPro);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 65f443f and 2ecaf2a.

📒 Files selected for processing (2)
  • app/api/chat/route.ts (1 hunks)
  • convex/chats.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • app/api/chat/route.ts
  • convex/chats.ts
convex/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

convex/**/*.ts: Always use the new Convex function syntax (query/mutation/action objects with args/returns/handler) when defining Convex functions
When a function returns null, include returns: v.null() and return null explicitly
Use internalQuery/internalMutation/internalAction for private functions callable only by other Convex functions; do not expose sensitive logic via public query/mutation/action
Use query/mutation/action only for public API functions
Do not try to register functions via the api or internal objects
Always include argument and return validators for all Convex functions (query/internalQuery/mutation/internalMutation/action/internalAction)
In JS implementations, functions without an explicit return value implicitly return null
Use ctx.runQuery from queries/mutations/actions to call a query
Use ctx.runMutation from mutations/actions to call a mutation
Use ctx.runAction from actions to call an action
Only call an action from another action when crossing runtimes (e.g., V8 to Node); otherwise extract shared helper code
Minimize calls from actions to queries/mutations to avoid race conditions from splitting transactions
Pass FunctionReference values (from api/internal) to ctx.runQuery/ctx.runMutation/ctx.runAction; do not pass function implementations
When calling a function in the same file via ctx.run*, add an explicit return type annotation at the call site to avoid TS circularity
Use the generated api object for public functions and internal object for internal functions from convex/_generated/api.ts
Respect file-based routing for function references: e.g., convex/example.ts export f -> api.example.f; nested paths map to dot-separated namespaces
For paginated queries use paginationOptsValidator in args and .paginate(args.paginationOpts) on a query
v.bigint() is deprecated; use v.int64() for signed 64-bit integers
Use v.record(keys, values) for record-like data; v.map() and v.set() are not supported
For full-text search, use withSearchIndex("ind...

Files:

  • convex/chats.ts
🧠 Learnings (1)
📚 Learning: 2025-08-29T13:33:09.901Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.901Z
Learning: Applies to convex/**/*.ts : In JS implementations, functions without an explicit return value implicitly return null

Applied to files:

  • convex/chats.ts
🧬 Code graph analysis (1)
app/api/chat/route.ts (1)
lib/rate-limit.ts (1)
  • checkRateLimit (6-73)

Copy link

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

Caution

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

⚠️ Outside diff range comments (5)
convex/chats.ts (1)

171-205: Add a returns validator and standardize empty pagination shape (continueCursor should be null).

This query lacks a returns validator (required by our Convex guidelines) and returns an empty string for continueCursor in empty results. Align with paginate semantics and our other APIs by validating the response and using null for no cursor.

Apply:

 export const getUserChats = query({
   args: {
     paginationOpts: paginationOptsValidator,
   },
+  returns: v.object({
+    page: v.array(v.any()),
+    isDone: v.boolean(),
+    continueCursor: v.union(v.string(), v.null()),
+    pageStatus: v.optional(v.union(v.string(), v.null())),
+    splitCursor: v.optional(v.union(v.string(), v.null())),
+  }),
   handler: async (ctx, args) => {
     const identity = await ctx.auth.getUserIdentity();
     if (!identity) {
       return {
         page: [],
         isDone: true,
-        continueCursor: "",
+        continueCursor: null,
       };
     }
@@
       console.error("Failed to get user chats:", error);
       return {
         page: [],
         isDone: true,
-        continueCursor: "",
+        continueCursor: null,
       };
     }
   },
 });
convex/feedback.ts (1)

4-55: Verify chat ownership via internal query; don’t rely solely on message.user_id (backfill-safe).

Older messages may not have user_id set; the current check would incorrectly block feedback. Use centralized ownership verification on the parent chat and only gate on message.user_id when it exists.

-import { mutation } from "./_generated/server";
+import { mutation } from "./_generated/server";
+import { internal } from "./_generated/api";
@@
-    } else if (message.user_id !== user.subject) {
-      throw new Error(
-        "Unauthorized: User not allowed to give feedback for this message",
-      );
-    }
+    } else if (message.user_id && message.user_id !== user.subject) {
+      throw new Error(
+        "Unauthorized: User not allowed to give feedback for this message",
+      );
+    } else {
+      // Ensure the requester owns the chat for this message (covers legacy rows without user_id)
+      await ctx.runQuery(internal.messages.verifyChatOwnership, {
+        chatId: message.chat_id,
+        userId: user.subject,
+      });
+    }

Optional: If feedback is only intended for assistant messages, enforce it:

if (message.role !== "assistant") {
  throw new Error("Unsupported: Feedback is only allowed on assistant messages");
}
convex/messages.ts (3)

126-137: Don’t swallow authorization errors; only treat “Chat not found” as empty.

Right now any error (including unauthorized) returns empty results, masking auth issues. Distinguish not-found vs unauthorized.

-      try {
-        await ctx.runQuery(internal.messages.verifyChatOwnership, {
-          chatId: args.chatId,
-          userId: user.subject,
-        });
-      } catch (error) {
-        // Chat doesn't exist yet - return empty results (will be created on first message)
-        return {
-          page: [],
-          isDone: true,
-          continueCursor: "",
-        };
-      }
+      try {
+        await ctx.runQuery(internal.messages.verifyChatOwnership, {
+          chatId: args.chatId,
+          userId: user.subject,
+        });
+      } catch (error) {
+        if (error instanceof Error && error.message.includes("Chat not found")) {
+          return {
+            page: [],
+            isDone: true,
+            continueCursor: null,
+          };
+        }
+        throw error; // Bubble Unauthorized and others
+      }

254-261: Avoid .filter(); add an index for (chat_id, role, _creationTime) and query it.

Filters aren’t supported per our rules; this should be index-backed. Add a composite index and use it here.

Proposed code (requires schema update):

-      const lastAssistantMessage = await ctx.db
-        .query("messages")
-        .withIndex("by_chat_id", (q) => q.eq("chat_id", args.chatId))
-        .filter((q) => q.eq(q.field("role"), "assistant"))
-        .order("desc")
-        .first();
+      const lastAssistantMessage = await ctx.db
+        .query("messages")
+        .withIndex("by_chat_role_created", (q) =>
+          q.eq("chat_id", args.chatId).eq("role", "assistant")
+        )
+        .order("desc")
+        .first();

Schema addition (convex/schema.ts):

messages: defineTable({
  // ...
}).index("by_chat_role_created", ["chat_id", "role", "_creationTime"]);

Want me to push the schema change in this PR?


352-359: Range query on _creationTime must be index-backed.

Using gt("_creationTime", …) on an index that doesn’t include _creationTime will fail. Add a composite index (chat_id, _creationTime) and query it.

-      const messages = await ctx.db
-        .query("messages")
-        .withIndex("by_chat_id", (q) =>
-          q
-            .eq("chat_id", message.chat_id)
-            .gt("_creationTime", message._creationTime),
-        )
-        .collect();
+      const messages = await ctx.db
+        .query("messages")
+        .withIndex("by_chat_created", (q) =>
+          q.eq("chat_id", message.chat_id).gt("_creationTime", message._creationTime)
+        )
+        .collect();

Schema addition:

messages: defineTable({
  // ...
}).index("by_chat_created", ["chat_id", "_creationTime"]);
🧹 Nitpick comments (16)
components/ui/sonner.tsx (3)

11-24: Memoize position/offset to avoid prop churn and unnecessary re-renders

getPositionProps() recreates new objects each render, which can trigger downstream re-layout/reposition in Sonner. Memoize and add an explicit type for clarity.

Apply:

-  const getPositionProps = () => {
-    if (isMobile) {
-      return {
-        position: "top-center" as const,
-        offset: { top: 20 },
-      };
-    }
-    return {
-      position: "bottom-right" as const,
-      offset: { bottom: 140, right: 50 },
-    };
-  };
-
-  const positionProps = getPositionProps();
+  const positionProps = useMemo<Pick<ToasterProps, "position" | "offset">>(
+    () =>
+      isMobile
+        ? { position: "top-center" as const, offset: { top: 20 } }
+        : { position: "bottom-right" as const, offset: { bottom: 140, right: 50 } },
+    [isMobile],
+  );

Add import:

import { useMemo } from "react";

Also applies to: 26-26


32-33: Confirm override intent for position/offset

With the current spread order, any position/offset passed via {...props} will override your computed values. If you want your responsive placement to win, move {...props} before them.

       className="toaster group"
-      position={positionProps.position}
-      offset={positionProps.offset}
+      {...props}
+      position={positionProps.position}
+      offset={positionProps.offset}
@@
-      {...props}

Also applies to: 45-45


16-18: Use safe-area insets on mobile (components/ui/sonner.tsx lines 16–18 and 21–23)
Replace

offset: { top: 20 },

with

offset: { top: "calc(env(safe-area-inset-top) + 20px)" },

to avoid collisions with iOS notches; Toaster supports CSS length strings for offset cite12

lib/utils/file-transform-utils.ts (1)

145-147: Good escalation to error; add timeout and size guard to prevent stalls/OOM on large PDFs

Recommend adding an AbortController-based timeout and a max-bytes check in convertFileToBase64. Also include status/url in the error for quicker triage.

// Drop-in replacement for lines 16-30 (outside the selected range)
// Adds: 15s timeout + 20MB size cap + richer errors
async function convertFileToBase64(fileUrl: string): Promise<string | null> {
  if (!fileUrl) return null;

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 15_000);
  try {
    const response = await fetch(fileUrl, { signal: controller.signal });
    if (!response.ok) {
      console.error("convertFileToBase64: non-OK response", {
        url: fileUrl,
        status: response.status,
        statusText: response.statusText,
      });
      return null;
    }

    const contentLength = response.headers.get("content-length");
    const maxBytes = 20 * 1024 * 1024; // 20MB
    if (contentLength && Number(contentLength) > maxBytes) {
      console.error("convertFileToBase64: file too large", {
        url: fileUrl,
        bytes: Number(contentLength),
      });
      return null;
    }

    const arrayBuffer = await response.arrayBuffer();
    if (arrayBuffer.byteLength > maxBytes) {
      console.error("convertFileToBase64: file too large after fetch", {
        url: fileUrl,
        bytes: arrayBuffer.byteLength,
      });
      return null;
    }

    return Buffer.from(arrayBuffer).toString("base64");
  } catch (error) {
    console.error("Failed to convert file to base64", { url: fileUrl, error });
    return null;
  } finally {
    clearTimeout(timeout);
  }
}
convex/schema.ts (3)

32-32: Prefer strong foreign keys for user_id if a users table exists

If you have a users table, model user_id as v.id("users") to enforce referential integrity; otherwise keep string if it’s an external auth ID.

-    user_id: v.optional(v.string()),
+    user_id: v.optional(v.id("users")),

36-36: Index feedback_id for fast lookups and joins

If you read messages by feedback_id (or join from feedback -> message), add an index.

   })
     .index("by_message_id", ["id"])
-    .index("by_chat_id", ["chat_id"]),
+    .index("by_chat_id", ["chat_id"])
+    .index("by_feedback_id", ["feedback_id"]),

Also applies to: 39-41


52-55: Feedback table: consider ownership/audit fields and query indexes

Add user_id (or message_id) if you need ownership checks or reverse lookups, plus an index by type if filtering.

   feedback: defineTable({
     feedback_type: v.union(v.literal("positive"), v.literal("negative")),
     feedback_details: v.optional(v.string()),
-  }),
+    // Optional but recommended if you enforce ownership/analytics:
+    // user_id: v.id("users"),
+    // message_id: v.id("messages"),
+  })
+    // Optional indexes based on usage:
+    // .index("by_feedback_type", ["feedback_type"]),
types/chat.ts (1)

59-66: Align metadata enum with shared constants; keep it optional at usage sites

Using a shared constant reduces drift; ensure downstream treats metadata as optional.

const FEEDBACK_TYPES = ["positive", "negative"] as const;
export const messageMetadataSchema = z.object({
  feedbackType: z.enum(FEEDBACK_TYPES),
});
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
export type ChatMessage = UIMessage<MessageMetadata>; // metadata remains optional on UIMessage
app/components/FeedbackInput.tsx (2)

14-26: Guard against double submit.

Skip if already submitting to prevent rapid Enter presses from enqueuing multiple requests.

   const handleSend = async () => {
-    if (!details.trim()) return;
+    if (isSubmitting || !details.trim()) return;

33-42: Optional: Don’t trigger send/cancel while submitting.

Prevent key handlers during isSubmitting to avoid racey UX.

   const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (isSubmitting) return;
     if (e.key === "Enter" && !e.shiftKey) {
       e.preventDefault();
       handleSend();
convex/messages.ts (1)

187-193: Use null for empty continueCursor to match validator.

Minor consistency with the declared returns type and our paginate usage.

       return {
         page: [],
         isDone: true,
-        continueCursor: "",
+        continueCursor: null,
       };
app/components/chat.tsx (1)

118-123: Avoid unsafe cast; prefer stronger typing at the source.

Casting messages as ChatMessage[] hides potential mismatches. If useChat supports generics, parameterize it to return ChatMessage[]; otherwise, change normalizeMessages to accept the exact UIMessage<MessageMetadata> type you get from useChat to remove the assertion.

lib/utils/message-processor.ts (1)

51-53: LGTM on narrowing to ChatMessage.

Signature aligns with the rest of the PR. Minor nit: update the JSDoc “UI messages” wording to “ChatMessage[]” for consistency.

- * @param messages - Array of UI messages to normalize
+ * @param messages - ChatMessage[] to normalize
app/components/MessageActions.tsx (2)

54-59: Memoize the feedback handler to reduce re-renders.

Wrap handleFeedback in useCallback and import it. Keeps child props stable when lists are large.

-import { useState } from "react";
+import { useState, useCallback } from "react";
@@
-const handleFeedback = (type: "positive" | "negative") => {
-  if (onFeedback) {
-    onFeedback(type);
-  }
-};
+const handleFeedback = useCallback(
+  (type: "positive" | "negative") => {
+    onFeedback?.(type);
+  },
+  [onFeedback],
+);

108-169: Improve a11y for feedback buttons.

Add aria-pressed to reflect current selection; assistive tech will announce the toggle state.

-<button
+<button
   type="button"
   onClick={() => handleFeedback("positive")}
+  aria-pressed={existingFeedback === "positive"}
@@
-<button
+<button
   type="button"
   onClick={() => handleFeedback("negative")}
+  aria-pressed={existingFeedback === "negative" || isAwaitingFeedbackDetails}
app/hooks/useFeedback.ts (1)

74-96: Optional: reflect “details saved” in metadata (if modeled).

If MessageMetadata includes feedback details, update local state after submit so UI can reflect it immediately.

 await createFeedback({
   feedback_type: "negative",
   feedback_details: details,
   message_id: feedbackInputMessageId,
 });
+// Optionally persist details locally:
+setMessages((prev) =>
+  prev.map((m) =>
+    m.id === feedbackInputMessageId
+      ? {
+          ...m,
+          metadata: { ...(m.metadata ?? {}), feedbackType: "negative", feedbackDetails: details },
+        }
+      : m,
+  ),
+);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6f149ad and 909962c.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (16)
  • app/api/chat/route.ts (2 hunks)
  • app/components/FeedbackInput.tsx (1 hunks)
  • app/components/MessageActions.tsx (5 hunks)
  • app/components/Messages.tsx (4 hunks)
  • app/components/chat.tsx (2 hunks)
  • app/hooks/useFeedback.ts (1 hunks)
  • components/ui/sonner.tsx (1 hunks)
  • convex/chats.ts (2 hunks)
  • convex/feedback.ts (1 hunks)
  • convex/messages.ts (10 hunks)
  • convex/schema.ts (2 hunks)
  • lib/db/actions.ts (4 hunks)
  • lib/utils.ts (1 hunks)
  • lib/utils/file-transform-utils.ts (1 hunks)
  • lib/utils/message-processor.ts (3 hunks)
  • types/chat.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/api/chat/route.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • components/ui/sonner.tsx
  • lib/utils/file-transform-utils.ts
  • app/components/Messages.tsx
  • app/components/chat.tsx
  • convex/schema.ts
  • convex/feedback.ts
  • app/hooks/useFeedback.ts
  • lib/utils.ts
  • lib/utils/message-processor.ts
  • lib/db/actions.ts
  • app/components/FeedbackInput.tsx
  • types/chat.ts
  • convex/chats.ts
  • app/components/MessageActions.tsx
  • convex/messages.ts
convex/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

convex/**/*.ts: Always use the new Convex function syntax (query/mutation/action objects with args/returns/handler) when defining Convex functions
When a function returns null, include returns: v.null() and return null explicitly
Use internalQuery/internalMutation/internalAction for private functions callable only by other Convex functions; do not expose sensitive logic via public query/mutation/action
Use query/mutation/action only for public API functions
Do not try to register functions via the api or internal objects
Always include argument and return validators for all Convex functions (query/internalQuery/mutation/internalMutation/action/internalAction)
In JS implementations, functions without an explicit return value implicitly return null
Use ctx.runQuery from queries/mutations/actions to call a query
Use ctx.runMutation from mutations/actions to call a mutation
Use ctx.runAction from actions to call an action
Only call an action from another action when crossing runtimes (e.g., V8 to Node); otherwise extract shared helper code
Minimize calls from actions to queries/mutations to avoid race conditions from splitting transactions
Pass FunctionReference values (from api/internal) to ctx.runQuery/ctx.runMutation/ctx.runAction; do not pass function implementations
When calling a function in the same file via ctx.run*, add an explicit return type annotation at the call site to avoid TS circularity
Use the generated api object for public functions and internal object for internal functions from convex/_generated/api.ts
Respect file-based routing for function references: e.g., convex/example.ts export f -> api.example.f; nested paths map to dot-separated namespaces
For paginated queries use paginationOptsValidator in args and .paginate(args.paginationOpts) on a query
v.bigint() is deprecated; use v.int64() for signed 64-bit integers
Use v.record(keys, values) for record-like data; v.map() and v.set() are not supported
For full-text search, use withSearchIndex("ind...

Files:

  • convex/schema.ts
  • convex/feedback.ts
  • convex/chats.ts
  • convex/messages.ts
convex/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

convex/schema.ts: Define the Convex schema in convex/schema.ts
Import schema definition functions (defineSchema, defineTable) from convex/server
Understand system fields: _id uses v.id(tableName) and _creationTime uses v.number(); they’re auto-added to all documents
Include all indexed fields in the index name (e.g., by_field1_and_field2 for ["field1","field2"])
Query indexes in the same column order as defined; create separate indexes for alternate orders

Files:

  • convex/schema.ts
🧠 Learnings (3)
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/schema.ts : Define the Convex schema in convex/schema.ts

Applied to files:

  • convex/schema.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Use internalQuery/internalMutation/internalAction for private functions callable only by other Convex functions; do not expose sensitive logic via public query/mutation/action

Applied to files:

  • convex/chats.ts
  • convex/messages.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Use query/mutation/action only for public API functions

Applied to files:

  • convex/chats.ts
🧬 Code graph analysis (8)
components/ui/sonner.tsx (1)
hooks/use-mobile.ts (1)
  • useIsMobile (5-21)
app/components/Messages.tsx (3)
types/chat.ts (1)
  • ChatMessage (65-65)
app/hooks/useFeedback.ts (1)
  • useFeedback (12-109)
app/components/FeedbackInput.tsx (1)
  • FeedbackInput (10-82)
app/components/chat.tsx (2)
lib/utils/message-processor.ts (1)
  • normalizeMessages (50-130)
types/chat.ts (1)
  • ChatMessage (65-65)
app/hooks/useFeedback.ts (2)
types/chat.ts (1)
  • ChatMessage (65-65)
convex/feedback.ts (1)
  • createFeedback (4-54)
lib/utils.ts (1)
types/chat.ts (1)
  • ChatMessage (65-65)
lib/utils/message-processor.ts (1)
types/chat.ts (1)
  • ChatMessage (65-65)
app/components/MessageActions.tsx (1)
components/ui/with-tooltip.tsx (1)
  • WithTooltip (16-46)
convex/messages.ts (2)
convex/_generated/server.js (2)
  • internalQuery (39-39)
  • internalQuery (39-39)
convex/_generated/api.js (2)
  • internal (22-22)
  • internal (22-22)
🔇 Additional comments (10)
components/ui/sonner.tsx (1)

5-5: Good addition: device-aware import

Importing the mobile hook here keeps the component cohesive. LGTM.

lib/utils.ts (1)

35-36: Mapping id looks correct

Switch to message.id aligns with schema and API usage.

lib/db/actions.ts (1)

45-56: No action needed: userId correctly passed and enforced
convex.messages.saveMessage and chats.saveChat both include userId in their args; saveMessage calls verifyChatOwnership before inserting, and saveChat assigns user_id on creation.

convex/chats.ts (1)

229-233: Ownership check on delete is correct.

Throwing on mismatched user_id is the right behavior and keeps this mutation public without a service key.

convex/messages.ts (3)

7-27: Internal ownership check looks good.

Using internalQuery with explicit args/returns and throwing on failure matches our Convex rules.


33-73: saveMessage: good idempotency check and centralized ownership verification.

Early-return on duplicate id and storing user_id are correct.


90-116: Return validator is solid and forward-compatible.

Typed page shape plus optional pageStatus/splitCursor aligns with paginate.

app/components/chat.tsx (1)

294-301: Propagate setMessages as a React Dispatch.

This assumes Messages’s setMessages prop is typed as React.Dispatch<React.SetStateAction<ChatMessage[]>>. If not, update it; this will enable functional updates and prevent stale-closure bugs in feedback handlers.

app/components/Messages.tsx (2)

65-72: LGTM on feedback wiring.

Hook integration and prop plumbing are correct; state is scoped per-message and only shown for assistant messages.


298-303: Nice UX touches.

Passing existingFeedback and showing inline FeedbackInput only when active keeps the thread compact.

Also applies to: 305-313

Copy link

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

♻️ Duplicate comments (3)
app/hooks/useFeedback.ts (3)

1-1: Type setMessages as Dispatch and tighten ID typing (prevents stale closures and enforces consistency).

Update imports and props to allow functional updates; also type IDs from ChatMessage to align with our guidelines and reduce accidental string misuse.

-import { useState, useCallback } from "react";
+import { useState, useCallback, type Dispatch, type SetStateAction } from "react";
@@
 interface UseFeedbackProps {
   messages: ChatMessage[];
-  setMessages: (messages: ChatMessage[]) => void;
+  setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
 }
@@
-  const [feedbackInputMessageId, setFeedbackInputMessageId] = useState<
-    string | null
-  >(null);
+  const [feedbackInputMessageId, setFeedbackInputMessageId] = useState<
+    ChatMessage["id"] | null
+  >(null);
@@
-    async (messageId: string, type: "positive" | "negative") => {
+    async (messageId: ChatMessage["id"], type: "positive" | "negative") => {

Also applies to: 7-10, 14-16, 23-23


41-49: Fix stale-closure update and preserve metadata on positive; also close details input if switching to positive.

Use a functional state update and merge metadata to avoid clobbering other fields; clear the inline input when a message is flipped to positive.

-          // Update local message state immediately
-          setMessages(
-            messages.map((msg) =>
-              msg.id === messageId
-                ? { ...msg, metadata: { feedbackType: "positive" } }
-                : msg,
-            ),
-          );
+          // Update local message state immediately (merge metadata)
+          setMessages((prev) =>
+            prev.map((msg) =>
+              msg.id === messageId
+                ? {
+                    ...msg,
+                    metadata: {
+                      ...(msg.metadata ?? {}),
+                      feedbackType: "positive",
+                    },
+                  }
+                : msg,
+            ),
+          );
+          // If details input was open for this message, close it
+          setFeedbackInputMessageId((curr) => (curr === messageId ? null : curr));

Also applies to: 50-54


70-81: Do the same for the negative path: functional update + metadata merge.

Prevents stale-state bugs and avoids dropping other metadata keys.

-          // Update local message state immediately
-          setMessages(
-            messages.map((msg) =>
-              msg.id === messageId
-                ? { ...msg, metadata: { feedbackType: "negative" } }
-                : msg,
-            ),
-          );
+          // Update local message state immediately (merge metadata)
+          setMessages((prev) =>
+            prev.map((msg) =>
+              msg.id === messageId
+                ? {
+                    ...msg,
+                    metadata: {
+                      ...(msg.metadata ?? {}),
+                      feedbackType: "negative",
+                    },
+                  }
+                : msg,
+            ),
+          );
🧹 Nitpick comments (4)
app/components/SidebarUserNav.tsx (1)

119-122: Prefer link semantics with asChild for a11y and built‑in rel control.

This avoids JS handlers and ensures proper attributes.

-<DropdownMenuItem onClick={handleHelpCenter}>
-  <LifeBuoy className="mr-2 h-4 w-4" />
-  <span>Help Center</span>
-</DropdownMenuItem>
+<DropdownMenuItem asChild>
+  <a href="https://help.hackerai.co/en/" target="_blank" rel="noopener noreferrer">
+    <LifeBuoy className="mr-2 h-4 w-4" aria-hidden="true" />
+    <span>Help Center</span>
+  </a>
+</DropdownMenuItem>
app/hooks/useFeedback.ts (3)

25-33: Existing-feedback precheck can be stale; consider removing or guarding with a ref.

Because messages is read from the outer scope, the “already positive/negative” checks can race with concurrent updates. Options:

  • Remove the precheck and rely on backend idempotency (server patches existing feedback safely), or
  • Track latest messages in a ref to avoid stale reads.

87-88: Trim dependencies after switching to functional updates.

Drop messages from the dependency array to avoid needless re-creations; setMessages can remain for clarity.

-    [createFeedback, messages, setMessages],
+    [createFeedback, setMessages],

90-112: Optional: surface details locally if UI needs it.

If any component renders feedback details from message metadata, consider mirroring feedback_details locally to avoid an extra fetch. Otherwise, current behavior is fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 909962c and 79d0e65.

📒 Files selected for processing (2)
  • app/components/SidebarUserNav.tsx (3 hunks)
  • app/hooks/useFeedback.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • app/components/SidebarUserNav.tsx
  • app/hooks/useFeedback.ts
🧬 Code graph analysis (2)
app/components/SidebarUserNav.tsx (1)
components/ui/dropdown-menu.tsx (1)
  • DropdownMenuItem (248-248)
app/hooks/useFeedback.ts (2)
types/chat.ts (1)
  • ChatMessage (65-65)
convex/feedback.ts (1)
  • createFeedback (4-54)
🔇 Additional comments (1)
app/components/SidebarUserNav.tsx (1)

5-5: LGTM: correct icon import.

LifeBuoy is correctly imported from lucide-react and used below.

@rossmanko rossmanko merged commit c7f9b1e into main Sep 2, 2025
2 of 3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 22, 2025
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