Skip to content

Conversation

@krishna7531
Copy link

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a complete initialization of a new Tutor Finder web application built with React, TypeScript, and Vite. The project establishes a full-stack architecture including a frontend application for discovering and comparing tutors, Supabase integration for backend coordination, and serverless Deno functions for orchestrating searches across tutoring websites. The application workflow allows users to select an exam type and location, which triggers discovery of relevant tutoring websites via an AI service, followed by parallel searches across those websites using the Mino API to extract and aggregate tutor listings. The UI includes components for rendering live search progress, displaying tutor cards with selectable options, and comparing tutors side-by-side. Configuration files, styling infrastructure, utility hooks, and type definitions establish the foundational setup for the application.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as Frontend App
    participant Supabase
    participant AIGateway as AI Gateway
    participant MinoAPI as Mino API
    participant TutoringWebsites as Tutoring Websites

    User->>Frontend: Select exam & location
    Frontend->>Supabase: POST discover-tutor-websites(exam, location)
    Supabase->>AIGateway: Request website recommendations
    AIGateway-->>Supabase: List of tutoring websites
    Supabase-->>Frontend: Return websites
    
    Frontend->>Frontend: Initialize agents for each website
    Frontend->>User: Show "Finding tutoring websites" state
    
    par Website Search
        Frontend->>MinoAPI: POST search-tutors-mino(websiteUrl, exam)
        MinoAPI->>TutoringWebsites: Fetch & extract tutors (SSE)
        TutoringWebsites-->>MinoAPI: Tutor data stream
        MinoAPI-->>Frontend: Stream tutor results (SSE)
    end
    
    Frontend->>Frontend: Parse streaming results
    Frontend->>Frontend: Update agent status & tutor list
    Frontend->>User: Display results & enable comparison
    User->>Frontend: Select tutors & compare
    Frontend->>User: Show comparison dashboard
Loading
🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether the description relates to the changeset. Add a pull request description that explains the purpose, key features, and context of the tutor finder application being added to the cookbook.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add tutor finder to TinyFish-cookbook' clearly describes the main objective of adding a comprehensive tutor finder application to the cookbook, which aligns with the extensive changeset creating a complete Next.js/React project.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@tutor-finder/.env`:
- Around line 1-3: Remove the real Supabase credentials from the committed .env
by replacing VITE_SUPABASE_PROJECT_ID, VITE_SUPABASE_PUBLISHABLE_KEY, and
VITE_SUPABASE_URL with non-sensitive placeholders (e.g.
VITE_SUPABASE_PROJECT_ID=your_project_id) and remove the surrounding quotes to
satisfy dotenv-linter; add a .env.example containing those placeholders and
update deployment to use secure secrets (CI/hosted env vars) after rotating the
exposed keys so the published VITE_ key is no longer valid.

In `@tutor-finder/.gitignore`:
- Around line 1-24: The repo is currently tracking a sensitive .env with
Supabase credentials; update the .gitignore to include .env and .env.local,
remove the tracked .env from Git index (e.g., git rm --cached .env and commit
the change), and purge the secret from history using a history-rewriting tool
(git filter-repo or git filter-branch) targeting the .env file; after removing
the file from history, rotate/invalidate the exposed Supabase keys in your
Supabase project settings to prevent further misuse.

In `@tutor-finder/src/components/AgentPreviewCard.tsx`:
- Around line 60-66: AgentPreviewCard renders an iframe using agent.streamingUrl
with sandbox="allow-scripts allow-same-origin", which weakens isolation; update
the iframe in AgentPreviewCard to remove allow-same-origin for untrusted URLs
(leave only necessary flags such as allow-scripts), or require that streamingUrl
be hosted on a dedicated origin and use postMessage with strict origin checks
for any communication; ensure any code that relies on same-origin behavior (in
AgentPreviewCard or related handlers) is refactored to use postMessage and
validate event.origin before processing.

In `@tutor-finder/src/components/ui/carousel.tsx`:
- Around line 91-103: The effect in React.useEffect registers both "reInit" and
"select" listeners on api but the cleanup only removes "select", leaving a
dangling "reInit" listener; update the cleanup to off both events (call
api.off("reInit", onSelect) as well as api.off("select", onSelect)) and ensure
the early-return when !api remains intact so both handlers are removed when the
component unmounts or api changes.

In `@tutor-finder/src/components/ui/chart.tsx`:
- Around line 61-86: The ChartStyle component builds a CSS selector from
user-provided ids and injects raw color values; fix it by escaping
selector/components with CSS.escape when building `[data-chart=...]` and any
interpolated keys (use CSS.escape(id) and CSS.escape(key) and escape theme keys
from THEMES), validate each color before including it using
CSS.supports('color', color) (skip values that fail), and stop using
dangerouslySetInnerHTML by composing the CSS string in a local variable and
rendering it as normal <style>{cssString}</style> inside ChartStyle; update the
mapping logic that derives color (in ChartStyle’s Object.entries(THEMES) /
colorConfig mapping) to only output `--color-...` lines for escaped keys with
validated colors.

In `@tutor-finder/src/components/ui/collapsible.tsx`:
- Around line 5-7: The code incorrectly references
CollapsiblePrimitive.CollapsibleTrigger and
CollapsiblePrimitive.CollapsibleContent which do not exist; replace those with
CollapsiblePrimitive.Trigger and CollapsiblePrimitive.Content (update the
references to the symbols CollapsibleTrigger and CollapsibleContent in this file
to use the correct members Trigger and Content on CollapsiblePrimitive so the
import compiles).

In `@tutor-finder/src/components/ui/input-otp.tsx`:
- Line 44: The animate-caret-blink class used on the caret element is not
defined in the Tailwind config; open tailwind.config.ts and under theme.extend
add a "caret-blink" keyframes definition and a corresponding animation entry
(theme.extend.keyframes and theme.extend.animation) so that the
animate-caret-blink utility maps to a "caret-blink 1.25s ease-out infinite"
animation using the provided on/off opacity keyframe pattern; this will enable
the caret element (animate-caret-blink) to actually animate.

In `@tutor-finder/src/components/ui/progress.tsx`:
- Around line 6-20: The Progress component destructures value but fails to pass
it into ProgressPrimitive.Root, which prevents Radix from setting aria-valuenow;
update the Progress render to forward the value prop into ProgressPrimitive.Root
(i.e., include value={value}) and ensure the passed value is the numeric 0–100
range expected by Radix (sanitize/coerce if necessary) so accessibility
attributes are correctly emitted by ProgressPrimitive.Root and the Indicator can
continue using value for its transform.

In `@tutor-finder/src/components/ui/toggle-group.tsx`:
- Around line 24-39: The ToggleGroupItem currently always prefers context values
because it uses `context.variant || variant` and `context.size || size`; change
the logic in `ToggleGroupItem` to prefer the per-item props when provided (e.g.
use `variant ?? context.variant` and `size ?? context.size` or equivalent)
before calling `toggleVariants`, so item-level `variant`/`size` overrides work
while still falling back to `ToggleGroupContext` values when the item prop is
undefined.

In `@tutor-finder/src/hooks/use-toast.ts`:
- Around line 166-177: The effect in useToast is incorrectly depending on state
causing listeners to be re-added on every state change; change the
React.useEffect dependency array from [state] to [] so that
listeners.push(setState) runs only on mount and the cleanup removes the same
setState on unmount; locate the useToast function, the React.useEffect block,
the state/setState variables and the shared listeners array to apply this fix.

In `@tutor-finder/vitest.config.ts`:
- Around line 1-16: The config currently uses the nonexistent ESM __dirname in
path.resolve inside the resolve.alias (causing ReferenceError); replace usage by
deriving a directory from import.meta.url (e.g., create a local __dirname via
fileURLToPath(import.meta.url) and path.dirname) and then use that variable in
path.resolve for the alias; update vitest config top-level imports to include
the URL helper (fileURLToPath) so resolve.alias { "@": path.resolve(__dirname,
"./src") } uses the computed directory instead of __dirname.
🟡 Minor comments (15)
tutor-finder/src/components/ui/chart.tsx-210-214 (1)

210-214: ⚠️ Potential issue | 🟡 Minor

Tooltip hides zero values.

item.value && will suppress valid 0 values. Use a nullish check so zeroes render.

✅ Fix
-                      {item.value && (
+                      {item.value !== undefined && item.value !== null && (
                         <span className="font-mono font-medium tabular-nums text-foreground">
                           {item.value.toLocaleString()}
                         </span>
                       )}
tutor-finder/README.md-81-113 (1)

81-113: ⚠️ Potential issue | 🟡 Minor

Align README terminology and stack with the actual app.

The doc references “Next.js” and “Tender” flows, which conflicts with the Tutor Finder/Vite-based description. Please align the stack and diagram text to avoid confusion.

📝 Suggested doc corrections
-**Next.js (TypeScript)**
+**React + Vite (TypeScript)**

-ORCH["Tender Search Orchestration Layer<br/>(Next.js API / Server Actions)"]
+ORCH["Tutor Search Orchestration Layer<br/>(API / Server Actions)"]

-DB["SUPABASE<br/>(Cached Tenders & Metadata)"]
+DB["SUPABASE<br/>(Cached Tutors & Metadata)"]

-DBD["• Cached tender listings<br/>• Deduplicated tenders<br/>• Historical records"]
+DBD["• Cached tutor listings<br/>• Deduplicated tutors<br/>• Historical records"]

-MINOD["• Parallel web agents<br/>• Browse govt tender portals<br/>• Open tender pages<br/>• Extract structured fields<br/>• SSE streaming updates"]
+MINOD["• Parallel web agents<br/>• Browse tutor marketplaces<br/>• Open tutor pages<br/>• Extract structured fields<br/>• SSE streaming updates"]
tutor-finder/src/components/ui/alert.tsx-29-33 (1)

29-33: ⚠️ Potential issue | 🟡 Minor

Fix AlertTitle ref element type mismatch.

AlertTitle renders an h5 element but the ref is typed as HTMLParagraphElement, which should be HTMLHeadingElement to match the actual DOM element type.

Suggested fix
-const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
+const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
   ({ className, ...props }, ref) => (
     <h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
   ),
 );
tutor-finder/src/components/ui/calendar.tsx-44-46 (1)

44-46: ⚠️ Potential issue | 🟡 Minor

Forward DayPicker icon props to allow dynamic styling and accessibility attributes.

IconLeft/IconRight explicitly drop incoming props, preventing DayPicker from applying className, style, or other attributes to control icon appearance and accessibility.

Suggested fix
-      components={{
-        IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
-        IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
-      }}
+      components={{
+        IconLeft: (props) => <ChevronLeft {...props} className={cn("h-4 w-4", props.className)} />,
+        IconRight: (props) => <ChevronRight {...props} className={cn("h-4 w-4", props.className)} />,
+      }}
tutor-finder/src/components/ui/breadcrumb.tsx-80-80 (1)

80-80: ⚠️ Potential issue | 🟡 Minor

Typo in displayName: "BreadcrumbElipssis" should be "BreadcrumbEllipsis".

🔤 Proposed fix
-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
tutor-finder/src/components/ui/card.tsx-17-22 (1)

17-22: ⚠️ Potential issue | 🟡 Minor

Type mismatch: CardTitle ref type is HTMLParagraphElement but renders an <h3> element.

The forwardRef generic specifies HTMLParagraphElement, but the component renders an <h3> heading element. This should be HTMLHeadingElement for type correctness.

🔧 Proposed fix
-const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
+const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
   ({ className, ...props }, ref) => (
     <h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
   ),
 );
tutor-finder/src/components/ui/form.tsx-111-124 (1)

111-124: ⚠️ Potential issue | 🟡 Minor

Avoid rendering "undefined" when an error lacks a message.

If an error object exists but has no message property, String(error?.message) renders the string "undefined" to the user. The existing if (!body) check on line 116 does not catch this since the string "undefined" is truthy.

🛠️ Proposed fix
-    const body = error ? String(error?.message) : children;
+    const body = error?.message ? String(error.message) : children;
tutor-finder/src/components/ui/form.tsx-33-54 (1)

33-54: ⚠️ Potential issue | 🟡 Minor

Guard context values before using them to prevent undefined field IDs.

FormFieldContext and FormItemContext both default to empty objects, so accessing .name or .id on them returns undefined. The current guard on line 40 checks if (!fieldContext) which always passes since an empty object is truthy. This allows getFieldState(fieldContext.name, formState) to execute with undefined, and the template strings on lines 49-51 produce undefined-form-item, undefined-form-item-description, and undefined-form-item-message IDs.

Guard fieldContext?.name and itemContext?.id before using them, and move the getFieldState call and id destructuring after these guards.

🛠️ Proposed fix
 const useFormField = () => {
   const fieldContext = React.useContext(FormFieldContext);
   const itemContext = React.useContext(FormItemContext);
   const { getFieldState, formState } = useFormContext();
-
-  const fieldState = getFieldState(fieldContext.name, formState);
-
-  if (!fieldContext) {
-    throw new Error("useFormField should be used within <FormField>");
-  }
-
-  const { id } = itemContext;
+  if (!fieldContext?.name) {
+    throw new Error("useFormField should be used within <FormField>");
+  }
+  if (!itemContext?.id) {
+    throw new Error("useFormField should be used within <FormItem>");
+  }
+
+  const fieldState = getFieldState(fieldContext.name, formState);
+  const { id } = itemContext;
tutor-finder/src/components/ui/menubar.tsx-188-188 (1)

188-188: ⚠️ Potential issue | 🟡 Minor

Fix typo: displayname should be displayName.

The property name is incorrectly cased. React uses displayName (camelCase with capital N) for component debugging in DevTools. This typo means the component won't be properly identified during debugging.

🐛 Proposed fix
-MenubarShortcut.displayname = "MenubarShortcut";
+MenubarShortcut.displayName = "MenubarShortcut";
tutor-finder/index.html-1-26 (1)

1-26: ⚠️ Potential issue | 🟡 Minor

Replace placeholder metadata before release.

The title/description and OG/Twitter tags still use “Lovable App” and TODO comments. Please update these to the actual product name/description so previews and SEO are correct.

If you share the final app name and description, I can draft the updated metadata.

tutor-finder/src/components/CompareDashboard.tsx-59-61 (1)

59-61: ⚠️ Potential issue | 🟡 Minor

Add aria-label to the close button for accessibility.

Screen readers won't announce the button's purpose since it only contains an icon.

♿ Proposed fix
-          <Button variant="ghost" size="icon" onClick={onClose}>
+          <Button variant="ghost" size="icon" onClick={onClose} aria-label="Close comparison">
             <X className="w-6 h-6" />
           </Button>
tutor-finder/src/hooks/useTutorSearch.ts-44-53 (1)

44-53: ⚠️ Potential issue | 🟡 Minor

No cleanup mechanism for in-flight requests.

If the component unmounts or resetSearch is called while startSearch is in progress, the fetch requests and stream readers will continue running, potentially updating stale state.

Consider using an AbortController that's cancelled on unmount or reset.

🔧 Sketch of abort pattern
const startSearch = useCallback(async (exam: ExamType, location: string) => {
  const abortController = new AbortController();
  
  // Store abort function for cleanup
  abortControllerRef.current = abortController;
  
  // ... in fetch calls:
  const response = await fetch(url, {
    // ... other options
    signal: abortController.signal,
  });
  
  // Check if aborted before updating state
  if (abortController.signal.aborted) return;
  
  // ... state updates
}, []);
tutor-finder/supabase/functions/search-tutors-mino/index.ts-30-58 (1)

30-58: ⚠️ Potential issue | 🟡 Minor

Same prompt injection concern as discover-tutor-websites.

User-supplied exam and websiteName are directly interpolated into the goal prompt. Apply the same validation recommended for the discover function.

tutor-finder/src/components/LocationInput.tsx-7-11 (1)

7-11: ⚠️ Potential issue | 🟡 Minor

isLoading prop appears unused at the call site.

In Index.tsx (line 71), isLoading is hardcoded to false. Consider passing state.isSearching or state.isDiscovering to properly disable the input during search operations.

🔧 Suggested fix in Index.tsx
 <LocationInput
   exam={state.exam}
   onSearch={handleSearch}
-  isLoading={false}
+  isLoading={state.isSearching || state.isDiscovering}
 />
tutor-finder/supabase/functions/discover-tutor-websites/index.ts-30-52 (1)

30-52: ⚠️ Potential issue | 🟡 Minor

Potential prompt injection vulnerability.

User-supplied exam and location values are directly interpolated into the AI prompt without sanitization. A malicious user could craft inputs that manipulate the AI's behavior.

Consider sanitizing or validating these inputs against allowed values (e.g., validate exam against a known list of exam types).

🛡️ Proposed validation
+const ALLOWED_EXAMS = ['SAT', 'ACT', 'AP', 'GRE', 'GMAT', 'TOEFL/IELTS', 'JEE/NEET', 'Olympiads'];
+
 const { exam, location } = await req.json();
 
 if (!exam || !location) {
   return new Response(
     JSON.stringify({ error: 'Exam and location are required' }),
     { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
   );
 }
+
+if (!ALLOWED_EXAMS.includes(exam)) {
+  return new Response(
+    JSON.stringify({ error: 'Invalid exam type' }),
+    { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+  );
+}
🧹 Nitpick comments (19)
tutor-finder/src/components/ui/carousel.tsx (2)

42-48: Avoid overriding opts.axis when orientation isn’t provided.

Line 42 defaults orientation to "horizontal" and Line 46 overwrites opts.axis, so opts.axis: "y" has no effect and the fallback in Line 111 is effectively dead. Consider deriving a resolved orientation from props/opts and using it consistently.

♻️ Suggested refactor
-  ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
+  ({ orientation, opts, setApi, plugins, className, children, ...props }, ref) => {
+    const resolvedOrientation = orientation ?? (opts?.axis === "y" ? "vertical" : "horizontal");
     const [carouselRef, api] = useEmblaCarousel(
       {
         ...opts,
-        axis: orientation === "horizontal" ? "x" : "y",
+        axis: resolvedOrientation === "horizontal" ? "x" : "y",
       },
       plugins,
     );
@@
-          orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
+          orientation: resolvedOrientation,

Also applies to: 109-112


70-81: Map arrow keys to orientation (Up/Down for vertical).

Currently only Left/Right are handled, which is unintuitive in vertical mode.

⌨️ Suggested tweak
     const handleKeyDown = React.useCallback(
       (event: React.KeyboardEvent<HTMLDivElement>) => {
-        if (event.key === "ArrowLeft") {
+        const isHorizontal = orientation === "horizontal";
+        if ((isHorizontal && event.key === "ArrowLeft") || (!isHorizontal && event.key === "ArrowUp")) {
           event.preventDefault();
           scrollPrev();
-        } else if (event.key === "ArrowRight") {
+        } else if ((isHorizontal && event.key === "ArrowRight") || (!isHorizontal && event.key === "ArrowDown")) {
           event.preventDefault();
           scrollNext();
         }
       },
-      [scrollPrev, scrollNext],
+      [orientation, scrollPrev, scrollNext],
     );
tutor-finder/public/robots.txt (1)

1-14: Consider environment-specific crawl rules (and optionally a Sitemap).

If this app isn’t ready for indexing (e.g., staging or private flows), consider a Disallow for non‑prod or adding a Sitemap directive for production discoverability.

tutor-finder/src/test/example.test.ts (1)

1-7: Consider replacing the placeholder with a real smoke test once flows exist.

tutor-finder/package.json (1)

1-89: Consider updating package name/version to match the Tutor Finder app.

vite_react_shadcn_ts and 0.0.0 look like template defaults; aligning metadata improves clarity and downstream tooling.

tutor-finder/tsconfig.app.json (1)

1-31: Consider tightening type safety when ready.

If you want stronger compile-time guarantees, consider enabling strict, noImplicitAny, and noFallthroughCasesInSwitch.

Optional stricter config
-    "strict": false,
-    "noImplicitAny": false,
-    "noFallthroughCasesInSwitch": false,
+    "strict": true,
+    "noImplicitAny": true,
+    "noFallthroughCasesInSwitch": true,
tutor-finder/src/components/ui/resizable.tsx (1)

6-21: Prefer explicit React type imports for ComponentProps.
This avoids reliance on a global React namespace and keeps typings resilient to TS/ESLint config differences.

♻️ Suggested refactor
+import type { ComponentProps } from "react";
 import { GripVertical } from "lucide-react";
 import * as ResizablePrimitive from "react-resizable-panels";
 
 import { cn } from "@/lib/utils";
 
-const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
+const ResizablePanelGroup = ({ className, ...props }: ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
   <ResizablePrimitive.PanelGroup
     className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
     {...props}
   />
 );
 
 ...
-}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
+}: ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
   withHandle?: boolean;
 }) => (
tutor-finder/src/components/ui/sonner.tsx (1)

4-25: Prefer explicit React type imports for ComponentProps.
Keeps the file independent of global React namespace availability in TS/ESLint settings.

♻️ Suggested refactor
+import type { ComponentProps } from "react";
 import { useTheme } from "next-themes";
 import { Toaster as Sonner, toast } from "sonner";
 
-type ToasterProps = React.ComponentProps<typeof Sonner>;
+type ToasterProps = ComponentProps<typeof Sonner>;
tutor-finder/src/components/ui/breadcrumb.tsx (1)

7-13: Consider using the separator prop.

The Breadcrumb component accepts a separator prop but it's never used in the component. Either remove it from the type or pass it to context/children for customization.

tutor-finder/src/pages/NotFound.tsx (1)

1-24: Prefer router navigation for SPA behavior.

Using an anchor forces a full reload. Consider Link from react-router-dom so navigation stays client-side.

♻️ Suggested change
-import { useLocation } from "react-router-dom";
+import { Link, useLocation } from "react-router-dom";
...
-        <a href="/" className="text-primary underline hover:text-primary/90">
-          Return to Home
-        </a>
+        <Link to="/" className="text-primary underline hover:text-primary/90">
+          Return to Home
+        </Link>
tutor-finder/src/components/TutorCard.tsx (2)

64-73: Potential duplicate key issue with exam names.

If examsTaught contains duplicate values, using exam as the key will cause React warnings. Consider combining with index for uniqueness.

♻️ Proposed fix
-        {tutor.examsTaught.slice(0, 3).map((exam) => (
-          <Badge key={exam} variant="default" className="text-xs">
+        {tutor.examsTaught.slice(0, 3).map((exam, idx) => (
+          <Badge key={`${exam}-${idx}`} variant="default" className="text-xs">
             {exam}
           </Badge>
         ))}

123-128: String "null" check suggests upstream data quality issue.

The check tutor.pastResults.toLowerCase() !== 'null' handles cases where the backend returns the literal string "null" instead of an actual null value. While this defensive coding is fine, consider normalizing data at the API/hook layer to avoid spreading these checks throughout UI components.

tutor-finder/supabase/functions/discover-tutor-websites/index.ts (2)

54-67: Consider adding a timeout for the external API call.

The fetch to the AI gateway has no timeout configured. If the gateway is slow or unresponsive, this function could hang until the Supabase function timeout is reached.

⏱️ Proposed fix with AbortController
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
     const response = await fetch('https://ai.gateway.lovable.dev/v1/chat/completions', {
       method: 'POST',
       headers: {
         'Authorization': `Bearer ${LOVABLE_API_KEY}`,
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({
         model: 'google/gemini-3-flash-preview',
         messages: [
           { role: 'user', content: prompt }
         ],
         temperature: 0.7,
       }),
+      signal: controller.signal,
     });
+
+    clearTimeout(timeoutId);

94-114: JSON extraction regex may miss edge cases.

The regex /\[[\s\S]*\]/ greedily matches the first [ to the last ], which could capture unintended content if the AI response contains multiple arrays or nested structures. The fallback handling is good, but consider a more robust approach.

Additionally, there's no validation that the parsed array contains objects with the expected name and url properties.

🔍 Proposed validation
     try {
       const jsonMatch = content.match(/\[[\s\S]*\]/);
       if (jsonMatch) {
-        websites = JSON.parse(jsonMatch[0]);
+        const parsed = JSON.parse(jsonMatch[0]);
+        // Validate structure
+        if (Array.isArray(parsed) && parsed.every(w => typeof w.name === 'string' && typeof w.url === 'string')) {
+          websites = parsed;
+        }
       }
     } catch (parseError) {
tutor-finder/src/pages/Index.tsx (1)

17-17: Consider memoizing selectedTutors to avoid unnecessary recomputation.

This filter runs on every render, even when the tutors or selection haven't changed. With many tutors, this could impact performance.

♻️ Proposed memoization
+import { useState, useMemo } from 'react';
-import { useState } from 'react';
...

-  const selectedTutors = state.tutors.filter((t) => state.selectedTutorIds.has(t.id));
+  const selectedTutors = useMemo(
+    () => state.tutors.filter((t) => state.selectedTutorIds.has(t.id)),
+    [state.tutors, state.selectedTutorIds]
+  );
tutor-finder/supabase/functions/search-tutors-mino/index.ts (2)

85-129: Async IIFE for stream processing lacks proper error propagation.

The async IIFE processes the stream in the background. While errors are caught and written to the stream, there's no mechanism to signal the main function if the stream processing fails before any data is written. Additionally, if writer.close() fails in the error handler (line 124), the original error context is lost.

🔧 Improved error handling
     (async () => {
       try {
         // ... stream processing
       } catch (error) {
         console.error('Stream processing error:', error);
         try {
-          await writer.write(encoder.encode(`data: {"type":"ERROR","message":"${error instanceof Error ? error.message : 'Unknown error'}"}\n\n`));
+          const errMsg = error instanceof Error ? error.message.replace(/"/g, '\\"') : 'Unknown error';
+          await writer.write(encoder.encode(`data: {"type":"ERROR","message":"${errMsg}"}\n\n`));
           await writer.close();
         } catch (e) {
           console.error('Failed to write error to stream:', e);
+          writer.abort(e);
         }
       }
     })();

Note: The error message should also escape special characters to ensure valid JSON.


61-71: Consider adding a timeout for the Mino API call.

Similar to the discover function, this external API call has no timeout. Long-running or unresponsive requests could exhaust function resources.

tutor-finder/src/hooks/useTutorSearch.ts (2)

5-14: Set<string> in React state may cause reference equality issues.

React's state comparison uses reference equality. When creating a new Set from an existing one, React will always see it as changed. This is handled correctly in toggleTutorSelection (line 22), but be aware that components receiving selectedTutorIds may re-render even if the contents haven't changed.

Consider using an array or a plain object for more predictable React behavior, or memoize downstream computations.


141-189: Silently swallowing JSON parse errors.

While ignoring incomplete JSON during streaming is reasonable, swallowing all parse errors (line 187-189) could hide issues with malformed server responses. Consider logging at debug level.

🔍 Proposed logging
               } catch (e) {
-                // Ignore parse errors for incomplete JSON
+                // Log parse errors for debugging (may be incomplete JSON during streaming)
+                if (import.meta.env.DEV) {
+                  console.debug(`Parse error for agent ${agentId}:`, jsonStr, e);
+                }
               }

Comment on lines +1 to +3
VITE_SUPABASE_PROJECT_ID="hspjxcouamcdqrvqhkgg"
VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhzcGp4Y291YW1jZHFydnFoa2dnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk2NzI4NzEsImV4cCI6MjA4NTI0ODg3MX0.1A4KloHAiPP6ObODTZnXicntDqMfwDbQP-_FoW4eyUY"
VITE_SUPABASE_URL="https://hspjxcouamcdqrvqhkgg.supabase.co"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not commit live Supabase credentials.

These values appear to be real project identifiers and a JWT-style key; committing them (especially with VITE_ which ships to clients) is a security risk. Please rotate the keys, move real values to deployment secrets, and keep only placeholders (or an .env.example) in the repo. Removing quotes will also satisfy the dotenv-linter warnings.

🔒 Example placeholder approach
-VITE_SUPABASE_PROJECT_ID="hspjxcouamcdqrvqhkgg"
-VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
-VITE_SUPABASE_URL="https://hspjxcouamcdqrvqhkgg.supabase.co"
+VITE_SUPABASE_PROJECT_ID=
+VITE_SUPABASE_PUBLISHABLE_KEY=
+VITE_SUPABASE_URL=
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
VITE_SUPABASE_PROJECT_ID="hspjxcouamcdqrvqhkgg"
VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhzcGp4Y291YW1jZHFydnFoa2dnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk2NzI4NzEsImV4cCI6MjA4NTI0ODg3MX0.1A4KloHAiPP6ObODTZnXicntDqMfwDbQP-_FoW4eyUY"
VITE_SUPABASE_URL="https://hspjxcouamcdqrvqhkgg.supabase.co"
VITE_SUPABASE_PROJECT_ID=
VITE_SUPABASE_PUBLISHABLE_KEY=
VITE_SUPABASE_URL=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 1-1: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 3-3: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🪛 Gitleaks (8.30.0)

[high] 2-2: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In `@tutor-finder/.env` around lines 1 - 3, Remove the real Supabase credentials
from the committed .env by replacing VITE_SUPABASE_PROJECT_ID,
VITE_SUPABASE_PUBLISHABLE_KEY, and VITE_SUPABASE_URL with non-sensitive
placeholders (e.g. VITE_SUPABASE_PROJECT_ID=your_project_id) and remove the
surrounding quotes to satisfy dotenv-linter; add a .env.example containing those
placeholders and update deployment to use secure secrets (CI/hosted env vars)
after rotating the exposed keys so the published VITE_ key is no longer valid.

Comment on lines +1 to +24
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for .env files and Supabase integration

echo "=== Checking for .env files in tutor-finder ==="
find tutor-finder -maxdepth 2 -type f -name '.env*' 2>/dev/null | head -20

echo "=== Checking git tracked files that look like env files ==="
git ls-files tutor-finder | grep -E '\.(env|local)$' || echo "No tracked .env files found"

echo "=== Checking for Supabase references in codebase ==="
rg -i 'supabase' tutor-finder --type ts --type tsx --type js --type jsx -l

echo "=== Checking if .env is mentioned anywhere ==="
rg -i '\.env' tutor-finder --type md --type json -l

echo "=== Listing tutor-finder directory structure ==="
ls -la tutor-finder/ | head -30

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 1768


🏁 Script executed:

#!/bin/bash
# Verify what's in the tracked .env file

echo "=== Contents of tracked .env file ==="
cat tutor-finder/.env

echo ""
echo "=== Git history check: when was .env added ==="
git log --oneline --follow tutor-finder/.env | head -5

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 533


Critical: Add .env to .gitignore and remove it from git history.

The .env file is currently tracked in git (commit 1075e30) and contains active Supabase credentials: project ID, publishable key (JWT token), and API URL. These must be removed from the repository immediately.

Required actions:

  1. Add .env and .env.local to .gitignore
  2. Remove the file from git history: git rm --cached tutor-finder/.env and commit
  3. Invalidate the exposed Supabase credentials in your Supabase project settings (the JWT token is now public in git history)
Gitignore update
 node_modules
 dist
 dist-ssr
 *.local
+
+# Environment variables
+.env
+.env.local
+.env.*.local
 
 # Editor directories and files
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Environment variables
.env
.env.local
.env.*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
🤖 Prompt for AI Agents
In `@tutor-finder/.gitignore` around lines 1 - 24, The repo is currently tracking
a sensitive .env with Supabase credentials; update the .gitignore to include
.env and .env.local, remove the tracked .env from Git index (e.g., git rm
--cached .env and commit the change), and purge the secret from history using a
history-rewriting tool (git filter-repo or git filter-branch) targeting the .env
file; after removing the file from history, rotate/invalidate the exposed
Supabase keys in your Supabase project settings to prevent further misuse.

Comment on lines +60 to +66
{agent.streamingUrl ? (
<iframe
src={agent.streamingUrl}
className="w-full h-full border-0"
title={`Live preview for ${agent.websiteName}`}
sandbox="allow-scripts allow-same-origin"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

iframe sandbox allow-scripts allow-same-origin security implications

💡 Result:

Using both sandbox="allow-scripts allow-same-origin" significantly weakens (and in some cases effectively defeats) the point of sandbox.

What each flag does

  • allow-scripts: lets JavaScript run inside the iframe. Without it, scripts don’t run. [1]
  • allow-same-origin: keeps the iframe’s real origin. Without it, the iframe is put into a unique/opaque origin (often Origin: null), which blocks access to cookies/storage and makes same-origin checks fail. [1][2][3]

Why the combination is risky

  • If the iframe content is same-origin with the parent (or can be made same-origin via a redirect), then scripts in the iframe can access the parent DOM like any normal same-origin frame, and can even remove the sandbox attribute, making it “no more secure than not using sandbox at all.” [1][2]
  • With allow-same-origin, the iframe regains access to origin data (e.g., cookies, localStorage, IndexedDB), so any XSS or malicious code running in the iframe can act with that origin’s privileges. [2][3]

Practical guidance

  • For untrusted content/code: prefer sandbox="allow-scripts" without allow-same-origin, so code can run but stays in an opaque origin (no cookie/storage access). [2][3]
  • If you must use allow-same-origin, strongly consider hosting the iframe on a separate dedicated origin (so “same-origin” doesn’t mean “same as your app”). [1]
  • If you communicate with the iframe, use postMessage with strict origin/source checks (opaque-origin sandboxed frames may have null origin). [2]

Sources: MDN <iframe sandbox> reference [1], web.dev “Play safely in sandboxed iframes” [2], MDN CSP sandbox directive (opaque origin / null origin and storage implications) [3].


🏁 Script executed:

cd tutor-finder/src/components && cat -n AgentPreviewCard.tsx | head -80

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 3649


🏁 Script executed:

rg "type AgentStatus|interface AgentStatus" --type ts --type tsx -A 10

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 100


🏁 Script executed:

rg "type AgentStatus|interface AgentStatus" -A 10

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 1297


Security concern: iframe with dynamic URL and permissive sandbox.

The streamingUrl is sourced from an external API response and rendered directly in an iframe. While sandbox="allow-scripts allow-same-origin" provides some protection, the combination of allow-scripts and allow-same-origin significantly weakens sandbox isolation: scripts in the iframe can potentially remove the sandbox attribute entirely and access parent DOM and origin data (cookies, localStorage, IndexedDB) if the content is same-origin with the parent.

For untrusted external content, remove allow-same-origin if not strictly required for functionality, or consider hosting the iframe on a separate dedicated origin. If iframe communication is needed, use postMessage with strict origin checks.

🤖 Prompt for AI Agents
In `@tutor-finder/src/components/AgentPreviewCard.tsx` around lines 60 - 66,
AgentPreviewCard renders an iframe using agent.streamingUrl with
sandbox="allow-scripts allow-same-origin", which weakens isolation; update the
iframe in AgentPreviewCard to remove allow-same-origin for untrusted URLs (leave
only necessary flags such as allow-scripts), or require that streamingUrl be
hosted on a dedicated origin and use postMessage with strict origin checks for
any communication; ensure any code that relies on same-origin behavior (in
AgentPreviewCard or related handlers) is refactored to use postMessage and
validate event.origin before processing.

Comment on lines +91 to +103
React.useEffect(() => {
if (!api) {
return;
}

onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);

return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd carousel.tsx --type f

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 211


🏁 Script executed:

sed -n '85,110p' tutor-finder/src/components/ui/carousel.tsx

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 516


🏁 Script executed:

rg 'api\.on\(' tutor-finder/src/components/ui/carousel.tsx -A 1 -B 1

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 162


🏁 Script executed:

rg 'api\.off\(' tutor-finder/src/components/ui/carousel.tsx -A 1 -B 1

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 55


Unsubscribe the reInit listener on cleanup.

Line 97 registers reInit, but cleanup only removes select (line 101), leaving a dangling listener after unmount that can cause duplicate state updates.

🛠️ Proposed fix
       return () => {
         api?.off("select", onSelect);
+        api?.off("reInit", onSelect);
       };
🤖 Prompt for AI Agents
In `@tutor-finder/src/components/ui/carousel.tsx` around lines 91 - 103, The
effect in React.useEffect registers both "reInit" and "select" listeners on api
but the cleanup only removes "select", leaving a dangling "reInit" listener;
update the cleanup to off both events (call api.off("reInit", onSelect) as well
as api.off("select", onSelect)) and ensure the early-return when !api remains
intact so both handlers are removed when the component unmounts or api changes.

Comment on lines +61 to +86
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);

if (!colorConfig.length) {
return null;
}

return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat tutor-finder/src/components/ui/chart.tsx

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 10059


Fix CSS injection vulnerability in ChartStyle by escaping selector components.

The selector [data-chart=${id}] is not quoted, allowing special characters in id to break the CSS selector. The id prop comes from ChartContainer, which receives user-provided input. If an id contains characters like ], =, or spaces, the selector becomes malformed. Additionally, color values are not validated and could carry malicious CSS content if sourced from untrusted input. Use CSS.escape() to sanitize the id and config keys, validate colors with CSS.supports(), and remove dangerouslySetInnerHTML by using a regular <style> tag instead.

🔒️ Suggested hardening
 const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
   const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);

   if (!colorConfig.length) {
     return null;
   }

+  const safeId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id;
+  const isValidColor = (value?: string) =>
+    value && (typeof CSS === "undefined" || !CSS.supports ? true : CSS.supports("color", value));
+
-  return (
-    <style
-      dangerouslySetInnerHTML={{
-        __html: Object.entries(THEMES)
-          .map(
-            ([theme, prefix]) => `
-${prefix} [data-chart=${id}] {
-${colorConfig
-  .map(([key, itemConfig]) => {
-    const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
-    return color ? `  --color-${key}: ${color};` : null;
-  })
-  .join("\n")}
-}
-`,
-          )
-          .join("\n"),
-      }}
-    />
-  );
+  const cssText = Object.entries(THEMES)
+    .map(
+      ([theme, prefix]) => `
+${prefix} [data-chart="${safeId}"] {
+${colorConfig
+  .map(([key, itemConfig]) => {
+    const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
+    const safeColor = isValidColor(color) ? color : undefined;
+    const safeKey = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(key) : key;
+    return safeColor ? `  --color-${safeKey}: ${safeColor};` : null;
+  })
+  .join("\n")}
+}
+`,
+    )
+    .join("\n");
+
+  return <style>{cssText}</style>;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
const safeId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id;
const isValidColor = (value?: string) =>
value && (typeof CSS === "undefined" || !CSS.supports ? true : CSS.supports("color", value));
const cssText = Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart="${safeId}"] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
const safeColor = isValidColor(color) ? color : undefined;
const safeKey = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(key) : key;
return safeColor ? ` --color-${safeKey}: ${safeColor};` : null;
})
.join("\n")}
}
`,
)
.join("\n");
return <style>{cssText}</style>;
};
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 69-69: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
In `@tutor-finder/src/components/ui/chart.tsx` around lines 61 - 86, The
ChartStyle component builds a CSS selector from user-provided ids and injects
raw color values; fix it by escaping selector/components with CSS.escape when
building `[data-chart=...]` and any interpolated keys (use CSS.escape(id) and
CSS.escape(key) and escape theme keys from THEMES), validate each color before
including it using CSS.supports('color', color) (skip values that fail), and
stop using dangerouslySetInnerHTML by composing the CSS string in a local
variable and rendering it as normal <style>{cssString}</style> inside
ChartStyle; update the mapping logic that derives color (in ChartStyle’s
Object.entries(THEMES) / colorConfig mapping) to only output `--color-...` lines
for escaped keys with validated colors.

{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and check tailwind config files
echo "=== Looking for Tailwind config files ==="
find . -maxdepth 3 -name "tailwind.config.*" -o -name "tailwind.config.*" | head -20

echo ""
echo "=== Searching for caret-blink references ==="
rg "caret-blink" --type-list | head -5
rg "caret-blink" 2>/dev/null | head -20

echo ""
echo "=== Checking for animation definitions in config files ==="
find . -maxdepth 3 \( -name "*.config.ts" -o -name "*.config.js" -o -name "tailwind.config.*" \) -type f 2>/dev/null

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 1558


🏁 Script executed:

#!/bin/bash
# Check the tailwind config file for animate-caret-blink definition
echo "=== Checking tutor-finder/tailwind.config.ts ==="
cat tutor-finder/tailwind.config.ts

echo ""
echo "=== Also checking summer-school-finder for comparison ==="
cat summer-school-finder/tailwind.config.ts

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 6806


🏁 Script executed:

#!/bin/bash
# Check if tailwindcss-animate plugin provides caret-blink animation
# Let's look at package.json to see the version and check npm/web for documentation

echo "=== Checking package.json for tailwindcss-animate version ==="
grep -A 2 -B 2 "tailwindcss-animate" tutor-finder/package.json

echo ""
echo "=== Searching for any CSS file that might define caret-blink ==="
find tutor-finder -type f \( -name "*.css" -o -name "*.scss" \) -exec grep -l "caret-blink\|@keyframes" {} \;

echo ""
echo "=== Check globals.css or similar for animation definitions ==="
find tutor-finder -type f -name "globals.css" -o -name "global.css" -o -name "index.css" | head -5
</parameter>

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 578


🏁 Script executed:

#!/bin/bash
# Read the CSS files to check for caret-blink animation
echo "=== Reading tutor-finder/src/index.css ==="
cat tutor-finder/src/index.css

echo ""
echo "=== Reading tutor-finder/src/App.css ==="
cat tutor-finder/src/App.css

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 3247


🌐 Web query:

tailwindcss-animate 1.0.7 available animations list caret-blink

💡 Result:

In tailwindcss-animate@1.0.7, the built-in animation utilities are enter/exit building blocks (not a preset library like caret-blink):

Core animation “types” (utilities)

  • animate-in, animate-out (sets animation-name to enter / exit) [1]

Enter/exit parameter utilities

  • Opacity: fade-in-*, fade-out-* [1]
  • Scale: zoom-in-*, zoom-out-* [1]
  • Rotation: spin-in-*, spin-out-* [1]
  • Translate:
    • slide-in-from-top-*, slide-in-from-bottom-*, slide-in-from-left-*, slide-in-from-right-* [1]
    • slide-out-to-top-*, slide-out-to-bottom-*, slide-out-to-left-*, slide-out-to-right-* [1]

The * values come from Tailwind theme scales (opacity/scale/rotate/translate); defaults are fade-*=0, zoom-*=0, spin-*=30deg, slide-*=100%. [1]

Animation controls (utilities)

  • duration-*, delay-*, ease-* [1]
  • fill-mode-none|forwards|backwards|both [1]
  • direction-normal|reverse|alternate|alternate-reverse [1]
  • repeat-0|repeat-1|repeat-infinite [1]
  • running, paused [1]

About caret-blink

caret-blink is not included in tailwindcss-animate@1.0.7. It is included in the Tailwind v4-compatible replacement tw-animate-css as a ready-to-use animation. [2]

Sources:
[1] tailwindcss-animate@1.0.7 index.js (UNPKG) https://app.unpkg.com/tailwindcss-animate@1.0.7/files/index.js
[2] tw-animate-css README (GitHub) https://github.com/Wombosvideo/tw-animate-css


Add caret-blink animation definition to Tailwind config.

The animate-caret-blink class on line 44 is not defined in tailwind.config.ts and is not provided by the installed tailwindcss-animate@1.0.7 plugin. The caret element will render but won't animate. Add this keyframe and animation to the extend section of theme in tutor-finder/tailwind.config.ts:

keyframes: {
  "caret-blink": {
    "0%, 10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%": { opacity: "1" },
    "5%, 15%, 25%, 35%, 45%, 55%, 65%, 75%, 85%, 95%": { opacity: "0" },
  },
},
animation: {
  "caret-blink": "caret-blink 1.25s ease-out infinite",
},
🤖 Prompt for AI Agents
In `@tutor-finder/src/components/ui/input-otp.tsx` at line 44, The
animate-caret-blink class used on the caret element is not defined in the
Tailwind config; open tailwind.config.ts and under theme.extend add a
"caret-blink" keyframes definition and a corresponding animation entry
(theme.extend.keyframes and theme.extend.animation) so that the
animate-caret-blink utility maps to a "caret-blink 1.25s ease-out infinite"
animation using the provided on/off opacity keyframe pattern; this will enable
the caret element (animate-caret-blink) to actually animate.

Comment on lines +6 to +20
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing value prop on Root breaks accessibility.

The value prop is destructured on line 9 but never passed to ProgressPrimitive.Root. Radix Progress relies on this prop to set aria-valuenow, making the progress bar inaccessible to screen readers.

🔧 Proposed fix
 const Progress = React.forwardRef<
   React.ElementRef<typeof ProgressPrimitive.Root>,
   React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
 >(({ className, value, ...props }, ref) => (
   <ProgressPrimitive.Root
     ref={ref}
     className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
+    value={value}
     {...props}
   >
     <ProgressPrimitive.Indicator
       className="h-full w-full flex-1 bg-primary transition-all"
       style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
     />
   </ProgressPrimitive.Root>
 ));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
value={value}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
🤖 Prompt for AI Agents
In `@tutor-finder/src/components/ui/progress.tsx` around lines 6 - 20, The
Progress component destructures value but fails to pass it into
ProgressPrimitive.Root, which prevents Radix from setting aria-valuenow; update
the Progress render to forward the value prop into ProgressPrimitive.Root (i.e.,
include value={value}) and ensure the passed value is the numeric 0–100 range
expected by Radix (sanitize/coerce if necessary) so accessibility attributes are
correctly emitted by ProgressPrimitive.Root and the Indicator can continue using
value for its transform.

Comment on lines +24 to +39
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);

return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Item-level variant/size overrides are blocked by context precedence.

context.variant || variant always favors context (default is truthy), so per-item overrides don’t work.

✅ Suggested fix
-  const context = React.useContext(ToggleGroupContext);
+  const context = React.useContext(ToggleGroupContext);

   return (
     <ToggleGroupPrimitive.Item
       ref={ref}
       className={cn(
         toggleVariants({
-          variant: context.variant || variant,
-          size: context.size || size,
+          variant: variant ?? context.variant,
+          size: size ?? context.size,
         }),
         className,
       )}
       {...props}
     >
-  <ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
+  <ToggleGroupContext.Provider
+    value={{ variant: variant ?? "default", size: size ?? "default" }}
+  >
+    {children}
+  </ToggleGroupContext.Provider>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: variant ?? context.variant,
size: size ?? context.size,
}),
className,
)}
🤖 Prompt for AI Agents
In `@tutor-finder/src/components/ui/toggle-group.tsx` around lines 24 - 39, The
ToggleGroupItem currently always prefers context values because it uses
`context.variant || variant` and `context.size || size`; change the logic in
`ToggleGroupItem` to prefer the per-item props when provided (e.g. use `variant
?? context.variant` and `size ?? context.size` or equivalent) before calling
`toggleVariants`, so item-level `variant`/`size` overrides work while still
falling back to `ToggleGroupContext` values when the item prop is undefined.

Comment on lines +166 to +177
function useToast() {
const [state, setState] = React.useState<State>(memoryState);

React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: useEffect dependency array includes state, causing re-subscriptions on every state change.

The listener is added and removed on every state update because state is in the dependency array. This should be an empty array [] since the effect only needs to run once on mount/unmount.

🐛 Proposed fix
   React.useEffect(() => {
     listeners.push(setState);
     return () => {
       const index = listeners.indexOf(setState);
       if (index > -1) {
         listeners.splice(index, 1);
       }
     };
-  }, [state]);
+  }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, []);
🤖 Prompt for AI Agents
In `@tutor-finder/src/hooks/use-toast.ts` around lines 166 - 177, The effect in
useToast is incorrectly depending on state causing listeners to be re-added on
every state change; change the React.useEffect dependency array from [state] to
[] so that listeners.push(setState) runs only on mount and the cleanup removes
the same setState on unmount; locate the useToast function, the React.useEffect
block, the state/setState variables and the shared listeners array to apply this
fix.

Comment on lines +1 to +16
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import path from "path";

export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
},
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat package.json | grep -A 5 -B 5 "type"

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 117


🏁 Script executed:

cat tutor-finder/vitest.config.ts

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 467


🏁 Script executed:

cat tutor-finder/package.json

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 3014


Avoid __dirname in ESM config (will be undefined at runtime).

The package uses "type": "module", making this an ESM context where __dirname is not globally available. When Vitest loads the config, it will throw ReferenceError: __dirname is not defined. Derive __dirname using import.meta.url instead.

🛠️ Proposed fix
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import path from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./src/test/setup.ts"],
    include: ["src/**/*.{test,spec}.{ts,tsx}"],
  },
  resolve: {
    alias: { "@": path.resolve(__dirname, "./src") },
  },
});
🤖 Prompt for AI Agents
In `@tutor-finder/vitest.config.ts` around lines 1 - 16, The config currently uses
the nonexistent ESM __dirname in path.resolve inside the resolve.alias (causing
ReferenceError); replace usage by deriving a directory from import.meta.url
(e.g., create a local __dirname via fileURLToPath(import.meta.url) and
path.dirname) and then use that variable in path.resolve for the alias; update
vitest config top-level imports to include the URL helper (fileURLToPath) so
resolve.alias { "@": path.resolve(__dirname, "./src") } uses the computed
directory instead of __dirname.

Copy link

@npkhang99 npkhang99 left a comment

Choose a reason for hiding this comment

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

Overall, the PR looks good, there're a few gripes here and there so please fix it a long with critical coderabbit comments and the PR should be good merge
Coderabbit's comments on shadcn/ui components could be ignored, though

Choose a reason for hiding this comment

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

The .env file should be untracked and included in .gitignore
You can try and create a new .env.example file containing the required fields with empty values

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.

3 participants