-
Notifications
You must be signed in to change notification settings - Fork 106
Add tutor finder to TinyFish-cookbook #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughThis 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
🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this 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 | 🟡 MinorTooltip hides zero values.
item.value &&will suppress valid0values. 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 | 🟡 MinorAlign 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 | 🟡 MinorFix AlertTitle ref element type mismatch.
AlertTitlerenders anh5element but the ref is typed asHTMLParagraphElement, which should beHTMLHeadingElementto 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 | 🟡 MinorForward DayPicker icon props to allow dynamic styling and accessibility attributes.
IconLeft/IconRightexplicitly drop incoming props, preventing DayPicker from applyingclassName,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 | 🟡 MinorTypo 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 | 🟡 MinorType mismatch:
CardTitleref type isHTMLParagraphElementbut renders an<h3>element.The
forwardRefgeneric specifiesHTMLParagraphElement, but the component renders an<h3>heading element. This should beHTMLHeadingElementfor 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 | 🟡 MinorAvoid rendering "undefined" when an error lacks a message.
If an error object exists but has no
messageproperty,String(error?.message)renders the string"undefined"to the user. The existingif (!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 | 🟡 MinorGuard context values before using them to prevent undefined field IDs.
FormFieldContextandFormItemContextboth default to empty objects, so accessing.nameor.idon them returnsundefined. The current guard on line 40 checksif (!fieldContext)which always passes since an empty object is truthy. This allowsgetFieldState(fieldContext.name, formState)to execute withundefined, and the template strings on lines 49-51 produceundefined-form-item,undefined-form-item-description, andundefined-form-item-messageIDs.Guard
fieldContext?.nameanditemContext?.idbefore using them, and move thegetFieldStatecall andiddestructuring 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 | 🟡 MinorFix typo:
displaynameshould bedisplayName.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 | 🟡 MinorReplace 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 | 🟡 MinorAdd 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 | 🟡 MinorNo cleanup mechanism for in-flight requests.
If the component unmounts or
resetSearchis called whilestartSearchis in progress, the fetch requests and stream readers will continue running, potentially updating stale state.Consider using an
AbortControllerthat'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 | 🟡 MinorSame prompt injection concern as discover-tutor-websites.
User-supplied
examandwebsiteNameare 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
isLoadingprop appears unused at the call site.In
Index.tsx(line 71),isLoadingis hardcoded tofalse. Consider passingstate.isSearchingorstate.isDiscoveringto 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 | 🟡 MinorPotential prompt injection vulnerability.
User-supplied
examandlocationvalues 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
examagainst 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 overridingopts.axiswhenorientationisn’t provided.Line 42 defaults
orientationto"horizontal"and Line 46 overwritesopts.axis, soopts.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_tsand0.0.0look 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, andnoFallthroughCasesInSwitch.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 forComponentProps.
This avoids reliance on a globalReactnamespace 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 forComponentProps.
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 theseparatorprop.The
Breadcrumbcomponent accepts aseparatorprop 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
Linkfromreact-router-domso 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
examsTaughtcontains duplicate values, usingexamas 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 actualnullvalue. 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
nameandurlproperties.🔍 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 memoizingselectedTutorsto 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
Setfrom an existing one, React will always see it as changed. This is handled correctly intoggleTutorSelection(line 22), but be aware that components receivingselectedTutorIdsmay 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); + } }
| VITE_SUPABASE_PROJECT_ID="hspjxcouamcdqrvqhkgg" | ||
| VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhzcGp4Y291YW1jZHFydnFoa2dnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk2NzI4NzEsImV4cCI6MjA4NTI0ODg3MX0.1A4KloHAiPP6ObODTZnXicntDqMfwDbQP-_FoW4eyUY" | ||
| VITE_SUPABASE_URL="https://hspjxcouamcdqrvqhkgg.supabase.co" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| # 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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -30Repository: 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 -5Repository: 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:
- Add
.envand.env.localto.gitignore - Remove the file from git history:
git rm --cached tutor-finder/.envand commit - 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.
| # 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.
| {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" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 (oftenOrigin: 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
sandboxat 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"withoutallow-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
postMessagewith strict origin/source checks (opaque-origin sandboxed frames may havenullorigin). [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 -80Repository: tinyfish-io/tinyfish-cookbook
Length of output: 3649
🏁 Script executed:
rg "type AgentStatus|interface AgentStatus" --type ts --type tsx -A 10Repository: tinyfish-io/tinyfish-cookbook
Length of output: 100
🏁 Script executed:
rg "type AgentStatus|interface AgentStatus" -A 10Repository: 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.
| React.useEffect(() => { | ||
| if (!api) { | ||
| return; | ||
| } | ||
|
|
||
| onSelect(api); | ||
| api.on("reInit", onSelect); | ||
| api.on("select", onSelect); | ||
|
|
||
| return () => { | ||
| api?.off("select", onSelect); | ||
| }; | ||
| }, [api, onSelect]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd carousel.tsx --type fRepository: tinyfish-io/tinyfish-cookbook
Length of output: 211
🏁 Script executed:
sed -n '85,110p' tutor-finder/src/components/ui/carousel.tsxRepository: tinyfish-io/tinyfish-cookbook
Length of output: 516
🏁 Script executed:
rg 'api\.on\(' tutor-finder/src/components/ui/carousel.tsx -A 1 -B 1Repository: tinyfish-io/tinyfish-cookbook
Length of output: 162
🏁 Script executed:
rg 'api\.off\(' tutor-finder/src/components/ui/carousel.tsx -A 1 -B 1Repository: 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.
| 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"), | ||
| }} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat tutor-finder/src/components/ui/chart.tsxRepository: 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.
| 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" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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/nullRepository: 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.tsRepository: 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.cssRepository: 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 toenter/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.
| 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> | ||
| )); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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, | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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") }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.tsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 467
🏁 Script executed:
cat tutor-finder/package.jsonRepository: 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.
There was a problem hiding this 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
There was a problem hiding this comment.
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
No description provided.