-
Notifications
You must be signed in to change notification settings - Fork 106
Add credit-aggregator - Mino Use Case #16
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
- ## Demo - Live demo: https://credit-aggregator.vercel.app/ - Contributor: Pranav Janakiraman (@pranavjana)
📝 WalkthroughWalkthroughThis pull request introduces a new Next.js project called credit-aggregator that enables users to compare credit cards based on specified requirements. The project includes a full-stack implementation with a React-based UI, a Node.js backend API endpoint, and integration with an external Mino API for scraping credit card data from multiple sites. The application uses Server-Sent Events (SSE) to stream real-time progress updates as the API concurrently fetches and aggregates credit card information from different sources. The project is configured with TypeScript, Tailwind CSS for styling, and includes reusable UI components for building the comparison interface. Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as Home Page (React)
participant API as /api/compare-cards<br/>(Next.js API Route)
participant Mino as Mino API<br/>(External Service)
participant Stream as SSE Stream
User->>UI: Enter requirements & click Search
UI->>API: POST /api/compare-cards with requirements
API->>Stream: Initialize SSE stream
API->>API: Validate input & API key
Note over API: Launch concurrent site fetches
loop For each credit card site
API->>Stream: Emit site_start event
API->>Mino: POST request with site URL & goal
Mino-->>API: Stream SSE events with progress
API->>Stream: Emit site_step events
API->>API: Parse & normalize card data
Mino-->>API: Completion/error event
API->>Stream: Emit site_complete event with cards
end
API->>API: Deduplicate & aggregate results
API->>Stream: Emit complete event with final results
UI->>Stream: Listen to SSE events
Stream-->>UI: site_start, site_step, site_complete events
UI->>UI: Update progress panel in real-time
Stream-->>UI: complete event with all cards
UI->>UI: Display results grid
UI-->>User: Show comparison results
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 7
🤖 Fix all issues with AI agents
In `@credit-aggregator/app/api/compare-cards/route.ts`:
- Around line 200-236: When handling SSE events inside the read loop, ensure the
stream is explicitly canceled before exiting: call await reader.cancel()
immediately before breaking out of the loop when handling the COMPLETE event
(the block that sets finalResult, computes siteCards via parseMinoResult and
calls sendEvent) and also call await reader.cancel() immediately before throwing
an error in the ERROR/FAILED handling branch (the block that constructs errorMsg
and throws). This will close the underlying SSE connection and prevent resource
leaks while keeping existing logic (finalResult, parseMinoResult, sendEvent)
unchanged.
In `@credit-aggregator/app/page.tsx`:
- Around line 130-137: The "complete" event handler currently calls
setResults(event.cards || []) which will overwrite any cards already accumulated
by the site_complete handler; modify the complete branch to avoid clobbering
accumulated results — either remove the setResults call in the event.type ===
"complete" branch or change it to a safe merge (e.g. use setResults(prev =>
prev.length ? prev : (event.cards || [])) so existing cards from the
site_complete handler are preserved), and keep the existing setLoading(false)
behavior; update the event.type === "complete" block (the handler referencing
setResults and setLoading) accordingly.
In `@credit-aggregator/components/ui/button.tsx`:
- Around line 39-58: The Button component currently renders a <button> without a
default type which can trigger form submits; update the render so when asChild
is false the element gets a safe default type="button" but still respects any
explicit type passed in props (i.e., set type to props.type ?? "button" on the
Comp when asChild is false), referencing the Button function, the asChild flag,
Comp variable and the spread of props so overrides remain allowed.
In `@credit-aggregator/components/ui/stepper.tsx`:
- Around line 139-163: StepperTrigger currently returns a bare div when asChild
is true and spreads {...props} after its internal onClick, which drops the
internal step change or external handlers; update StepperTrigger to use Slot
from `@radix-ui/react-slot` when asChild is true (rendering <Slot> instead of
<div>), and ensure the onClick handlers are merged (call setActiveStep(step)
then call any user-provided props.onClick) rather than letting props override
the internal handler; also move props spreading so internal props (ref,
disabled, className, merged onClick) are applied while still forwarding
remaining props to the Slot/button to preserve both internal and external
behavior.
In `@credit-aggregator/lib/mino-client.ts`:
- Around line 71-120: The SSE reader loop is returning early on completion/error
without closing the stream, leaking the HTTP connection; modify the block that
reads from reader (the loop using reader.read()) to ensure reader.cancel() is
always called before any early return (e.g., before the returns that follow
isCompleteEvent and isErrorEvent) or, better, wrap the entire read/parse loop in
a try { ... } finally { await reader.cancel().catch(()=>{}) } so the reader is
cancelled on success, error, or exceptions; locate symbols reader, parseSSELine,
isCompleteEvent, isErrorEvent, and formatStepMessage to implement the cleanup.
In `@credit-aggregator/README.md`:
- Line 7: Replace the bare URL after the "**Live Demo:**" text with proper
Markdown link syntax; specifically, change the line that currently reads "**Live
Demo:** https://credit-aggregator.vercel.app/" to use a link like "**Live
Demo:**
[https://credit-aggregator.vercel.app/](https://credit-aggregator.vercel.app/)"
or "**Live Demo:** [Live Demo](https://credit-aggregator.vercel.app/)" so it
conforms to markdownlint (MD034).
- Around line 15-18: Remove the duplicate "Demo" section in the README by
deleting the second "## Demo" heading and its placeholder line ("*[Demo
video/screenshot to be added]*"); keep the original demo content already present
earlier (the demo image and live demo link) and ensure no other references or
anchors rely on that duplicate heading "Demo".
🧹 Nitpick comments (6)
credit-aggregator/components/ui/dot-pattern.tsx (1)
48-48: Consider removing unusedidattribute.The
id="pattern-circle"attribute on the circle element is not referenced anywhere in the component. If multipleDotPatterncomponents are rendered, this could create duplicate IDs in the DOM, though it's harmless since the ID isn't used.🔧 Suggested fix
- <circle id="pattern-circle" cx={cx} cy={cy} r={cr} /> + <circle cx={cx} cy={cy} r={cr} />credit-aggregator/lib/utils.ts (1)
19-31: Type assertion lacks validation for requiredtypefield.The
data as MinoEventcast doesn't validate that the parsed JSON actually conforms to theMinoEventinterface. If the server sends malformed data without atypefield, downstream code usingisCompleteEventorisErrorEventmay behave unexpectedly.🔧 Suggested validation
export function parseSSELine(line: string): MinoEvent | null { if (!line.startsWith("data: ")) { return null; } try { const data = JSON.parse(line.slice(6)); + if (typeof data !== 'object' || data === null || typeof data.type !== 'string') { + console.error("Invalid MinoEvent: missing or invalid 'type' field"); + return null; + } return data as MinoEvent; } catch (error) { console.error("Failed to parse SSE line:", error); return null; } }credit-aggregator/README.md (1)
25-31: Section title is misleading.The "Code Snippet" heading suggests Mino API usage examples, but this section only shows standard npm setup commands. Consider renaming to "Quick Start" or adding actual Mino API code examples to demonstrate the integration.
credit-aggregator/app/page.tsx (3)
3-3:useEffectis imported but not used for cleanup; consider adding anAbortController.The streaming fetch (lines 60-148) has no mechanism to abort on component unmount or when the user navigates away. This could lead to memory leaks or state updates on unmounted components.
♻️ Proposed fix: Add AbortController for cleanup
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!requirements.trim()) { setError("Please enter your credit card requirements"); return; } + const controller = new AbortController(); + setLoading(true); setError(""); setResults([]); setSites({}); setSelectedSite(null); try { const response = await fetch("/api/compare-cards", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requirements }), + signal: controller.signal, });You could store the
AbortControllerin a ref and callcontroller.abort()in a cleanup effect or when the user cancels.
266-282: Cancel is cosmetic only — the backend request continues.This button updates local state to hide the site from the UI, but the server-side scraping continues. If intentional (soft skip), consider renaming to "Hide" or adding a tooltip. If users expect actual cancellation, you'd need to implement abort signaling to the server.
34-45: Minor:loadingin deps while also being set inside the effect.This is safe due to the
allDone && loadingguard, but the effect will re-run aftersetLoading(false). Consider removingloadingfrom dependencies and using a ref to track it, or accept the extra no-op run as negligible.
| const decoder = new TextDecoder(); | ||
| let buffer = ""; | ||
| let finalResult: unknown = null; | ||
|
|
||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
|
|
||
| buffer += decoder.decode(value, { stream: true }); | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() ?? ""; | ||
|
|
||
| for (const line of lines) { | ||
| if (line.startsWith("data: ")) { | ||
| try { | ||
| const event = JSON.parse(line.slice(6)); | ||
|
|
||
| // Handle completion first | ||
| if (event.type === "COMPLETE" && event.status === "COMPLETED") { | ||
| finalResult = event.resultJson; | ||
| // Parse and send cards with site_complete so frontend can show partial results | ||
| const siteCards = parseMinoResult(finalResult).map((card) => ({ ...card, source: site.name })); | ||
| await sendEvent({ | ||
| type: "site_complete", | ||
| site: site.name, | ||
| cards: siteCards, | ||
| timestamp: Date.now(), | ||
| }); | ||
| break; | ||
| } | ||
|
|
||
| // Handle errors | ||
| if (event.type === "ERROR" || event.status === "FAILED") { | ||
| const errorMsg = event.message || event.error || "Site may have bot protection"; | ||
| throw new Error(errorMsg); | ||
| } | ||
|
|
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:
Mino API SSE COMPLETE event stream closure documentation
💡 Result:
In Mino’s POST /v1/automation/run-sse SSE response, the COMPLETE event is the terminal “automation finished” event (success or failure) and contains the final status/result payload. After you receive type: "COMPLETE", you should treat the stream as finished and stop reading/close your SSE client. [1][2]
- Mino documents the SSE event types and explicitly defines
COMPLETE= “Automation finished (success or failure)”. [2] - The endpoint’s example stream shows
STARTED→STREAMING_URL(optional) →PROGRESS→COMPLETE(final). [1] - If you’re using the browser
EventSourceAPI, the client-side way to terminate the connection iseventSource.close()once you detect completion. [3]
Sources:
[1] https://tinyfish.mintlify.app/api-reference/automation/run-browser-automation-with-sse-streaming
[2] https://docs.mino.ai/quick-start
[3] https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
🏁 Script executed:
# First, let's look at the full context of the file to understand stream setup
find . -type f -name "route.ts" | grep -i "compare-cards"Repository: tinyfish-io/TinyFish-cookbook
Length of output: 123
🏁 Script executed:
# Search for where reader is created and how the response is handled
fd "compare-cards" --type f -e ts -e tsxRepository: tinyfish-io/TinyFish-cookbook
Length of output: 55
🏁 Script executed:
cat -n ./credit-aggregator/app/api/compare-cards/route.ts | sed -n '180,260p'Repository: tinyfish-io/TinyFish-cookbook
Length of output: 3296
🏁 Script executed:
# Look for the complete function that creates the reader
cat -n ./credit-aggregator/app/api/compare-cards/route.ts | sed -n '150,200p'Repository: tinyfish-io/TinyFish-cookbook
Length of output: 1683
🏁 Script executed:
# Check if there's any try-finally or cleanup logic around the streaming section
rg -A 50 "TextDecoder" ./credit-aggregator/app/api/compare-cards/route.tsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 1963
🏁 Script executed:
# Check for try-finally or cleanup logic around the entire reader section
cat -n ./credit-aggregator/app/api/compare-cards/route.ts | sed -n '140,280p'Repository: tinyfish-io/TinyFish-cookbook
Length of output: 5339
🏁 Script executed:
# Check the broader context to see if there's any finally block or cleanup
wc -l ./credit-aggregator/app/api/compare-cards/route.tsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 127
🏁 Script executed:
# Get the complete function to understand error handling scope
cat -n ./credit-aggregator/app/api/compare-cards/route.tsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 17446
Cancel the SSE reader before breaking or throwing to prevent resource leaks.
When the COMPLETE event is received (line 218) or an error occurs (line 232), the code breaks or throws without calling await reader.cancel(). This leaves the underlying SSE connection open. According to Mino's documentation, the COMPLETE event signals the end of the automation—you must close the SSE client to release the stream resource.
Add await reader.cancel(); before the break statement on line 228 and before the throw statement on line 234:
Proposed fix
if (event.type === "COMPLETE" && event.status === "COMPLETED") {
finalResult = event.resultJson;
// Parse and send cards with site_complete so frontend can show partial results
const siteCards = parseMinoResult(finalResult).map((card) => ({ ...card, source: site.name }));
await sendEvent({
type: "site_complete",
site: site.name,
cards: siteCards,
timestamp: Date.now(),
});
+ await reader.cancel();
break;
}
// Handle errors
if (event.type === "ERROR" || event.status === "FAILED") {
const errorMsg = event.message || event.error || "Site may have bot protection";
+ await reader.cancel();
throw new Error(errorMsg);
}📝 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 decoder = new TextDecoder(); | |
| let buffer = ""; | |
| let finalResult: unknown = null; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split("\n"); | |
| buffer = lines.pop() ?? ""; | |
| for (const line of lines) { | |
| if (line.startsWith("data: ")) { | |
| try { | |
| const event = JSON.parse(line.slice(6)); | |
| // Handle completion first | |
| if (event.type === "COMPLETE" && event.status === "COMPLETED") { | |
| finalResult = event.resultJson; | |
| // Parse and send cards with site_complete so frontend can show partial results | |
| const siteCards = parseMinoResult(finalResult).map((card) => ({ ...card, source: site.name })); | |
| await sendEvent({ | |
| type: "site_complete", | |
| site: site.name, | |
| cards: siteCards, | |
| timestamp: Date.now(), | |
| }); | |
| break; | |
| } | |
| // Handle errors | |
| if (event.type === "ERROR" || event.status === "FAILED") { | |
| const errorMsg = event.message || event.error || "Site may have bot protection"; | |
| throw new Error(errorMsg); | |
| } | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| let finalResult: unknown = null; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split("\n"); | |
| buffer = lines.pop() ?? ""; | |
| for (const line of lines) { | |
| if (line.startsWith("data: ")) { | |
| try { | |
| const event = JSON.parse(line.slice(6)); | |
| // Handle completion first | |
| if (event.type === "COMPLETE" && event.status === "COMPLETED") { | |
| finalResult = event.resultJson; | |
| // Parse and send cards with site_complete so frontend can show partial results | |
| const siteCards = parseMinoResult(finalResult).map((card) => ({ ...card, source: site.name })); | |
| await sendEvent({ | |
| type: "site_complete", | |
| site: site.name, | |
| cards: siteCards, | |
| timestamp: Date.now(), | |
| }); | |
| await reader.cancel(); | |
| break; | |
| } | |
| // Handle errors | |
| if (event.type === "ERROR" || event.status === "FAILED") { | |
| const errorMsg = event.message || event.error || "Site may have bot protection"; | |
| await reader.cancel(); | |
| throw new Error(errorMsg); | |
| } |
🤖 Prompt for AI Agents
In `@credit-aggregator/app/api/compare-cards/route.ts` around lines 200 - 236,
When handling SSE events inside the read loop, ensure the stream is explicitly
canceled before exiting: call await reader.cancel() immediately before breaking
out of the loop when handling the COMPLETE event (the block that sets
finalResult, computes siteCards via parseMinoResult and calls sendEvent) and
also call await reader.cancel() immediately before throwing an error in the
ERROR/FAILED handling branch (the block that constructs errorMsg and throws).
This will close the underlying SSE connection and prevent resource leaks while
keeping existing logic (finalResult, parseMinoResult, sendEvent) unchanged.
| } else if (event.type === "step") { | ||
| // Legacy step event - ignore | ||
| } else if (event.type === "complete") { | ||
| setResults(event.cards || []); | ||
| setLoading(false); | ||
| } else if (event.type === "error") { | ||
| throw new Error(event.error); | ||
| } |
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.
Potential results overwrite: complete event replaces accumulated cards.
The site_complete handler (line 119) accumulates cards incrementally, but the complete handler here overwrites results entirely. If the server sends both event types, accumulated results will be lost. Either remove the complete handler's setResults call or ensure the server doesn't send both.
🐛 Proposed fix: Remove the overwrite or make it conditional
} else if (event.type === "step") {
// Legacy step event - ignore
} else if (event.type === "complete") {
- setResults(event.cards || []);
+ // Only set results if no cards were accumulated from site_complete events
+ if (event.cards && event.cards.length > 0) {
+ setResults((prev) => prev.length === 0 ? event.cards : prev);
+ }
setLoading(false);
} else if (event.type === "error") {Alternatively, if complete is only sent without per-site cards, keep it as-is but document the expected server behavior.
📝 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.
| } else if (event.type === "step") { | |
| // Legacy step event - ignore | |
| } else if (event.type === "complete") { | |
| setResults(event.cards || []); | |
| setLoading(false); | |
| } else if (event.type === "error") { | |
| throw new Error(event.error); | |
| } | |
| } else if (event.type === "step") { | |
| // Legacy step event - ignore | |
| } else if (event.type === "complete") { | |
| // Only set results if no cards were accumulated from site_complete events | |
| if (event.cards && event.cards.length > 0) { | |
| setResults((prev) => prev.length === 0 ? event.cards : prev); | |
| } | |
| setLoading(false); | |
| } else if (event.type === "error") { | |
| throw new Error(event.error); | |
| } |
🤖 Prompt for AI Agents
In `@credit-aggregator/app/page.tsx` around lines 130 - 137, The "complete" event
handler currently calls setResults(event.cards || []) which will overwrite any
cards already accumulated by the site_complete handler; modify the complete
branch to avoid clobbering accumulated results — either remove the setResults
call in the event.type === "complete" branch or change it to a safe merge (e.g.
use setResults(prev => prev.length ? prev : (event.cards || [])) so existing
cards from the site_complete handler are preserved), and keep the existing
setLoading(false) behavior; update the event.type === "complete" block (the
handler referencing setResults and setLoading) accordingly.
| function Button({ | ||
| className, | ||
| variant = "default", | ||
| size = "default", | ||
| asChild = false, | ||
| ...props | ||
| }: React.ComponentProps<"button"> & | ||
| VariantProps<typeof buttonVariants> & { | ||
| asChild?: boolean | ||
| }) { | ||
| const Comp = asChild ? Slot : "button" | ||
|
|
||
| return ( | ||
| <Comp | ||
| data-slot="button" | ||
| data-variant={variant} | ||
| data-size={size} | ||
| className={cn(buttonVariants({ variant, size, className }))} | ||
| {...props} | ||
| /> |
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:
# Find the button.tsx file
find . -name "button.tsx" -o -name "button.ts" | head -20Repository: tinyfish-io/TinyFish-cookbook
Length of output: 117
🏁 Script executed:
# Also search for the file in the credit-aggregator directory structure
fd -t f "button.tsx" credit-aggregator/Repository: tinyfish-io/TinyFish-cookbook
Length of output: 115
🏁 Script executed:
cat -n credit-aggregator/components/ui/button.tsxRepository: tinyfish-io/TinyFish-cookbook
Length of output: 2724
🏁 Script executed:
# Search for Button usage in form contexts
rg -n --type=tsx -B2 -A2 '<Button' credit-aggregator/ | head -100Repository: tinyfish-io/TinyFish-cookbook
Length of output: 100
🏁 Script executed:
# Search for Button usage patterns
rg -n '<Button' credit-aggregator/ --no-type-list -A2 -B2 | head -150Repository: tinyfish-io/TinyFish-cookbook
Length of output: 516
🏁 Script executed:
# Search for Button usage patterns in the codebase
rg '<Button' credit-aggregator/ -A2 -B2 | head -200Repository: tinyfish-io/TinyFish-cookbook
Length of output: 415
🏁 Script executed:
# Search for all Button component usages
rg 'Button' credit-aggregator/ -A5 | grep -E '(<Button|type=)' | head -100Repository: tinyfish-io/TinyFish-cookbook
Length of output: 182
🏁 Script executed:
# More targeted search for Button usages and check if type is specified
rg '<Button' credit-aggregator/ -A10 | grep -v '^--$' | head -150Repository: tinyfish-io/TinyFish-cookbook
Length of output: 797
Default button type can trigger unintended form submits.
The Button component renders a <button> without a default type attribute, so it will submit surrounding forms by default per HTML specification. Add a safe default type="button" when asChild is false while allowing overrides.
Proposed fix to set a safe default type
function Button({
className,
variant = "default",
size = "default",
asChild = false,
+ type,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
+ type={asChild ? undefined : type ?? "button"}
{...props}
/>
)
}🤖 Prompt for AI Agents
In `@credit-aggregator/components/ui/button.tsx` around lines 39 - 58, The Button
component currently renders a <button> without a default type which can trigger
form submits; update the render so when asChild is false the element gets a safe
default type="button" but still respects any explicit type passed in props
(i.e., set type to props.type ?? "button" on the Comp when asChild is false),
referencing the Button function, the asChild flag, Comp variable and the spread
of props so overrides remain allowed.
| const StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>( | ||
| ({ asChild = false, className, children, ...props }, ref) => { | ||
| const { setActiveStep } = useStepper(); | ||
| const { step, isDisabled } = useStepItem(); | ||
|
|
||
| if (asChild) { | ||
| return <div className={className}>{children}</div>; | ||
| } | ||
|
|
||
| return ( | ||
| <button | ||
| ref={ref} | ||
| className={cn( | ||
| "inline-flex items-center gap-3 disabled:pointer-events-none disabled:opacity-50", | ||
| className, | ||
| )} | ||
| onClick={() => setActiveStep(step)} | ||
| disabled={isDisabled} | ||
| {...props} | ||
| > | ||
| {children} | ||
| </button> | ||
| ); | ||
| }, | ||
| ); |
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:
# First, locate and read the stepper.tsx file
find . -name "stepper.tsx" -type f | head -5Repository: tinyfish-io/TinyFish-cookbook
Length of output: 118
🏁 Script executed:
# Get file size and read the specific lines mentioned
cat -n credit-aggregator/components/ui/stepper.tsx | sed -n '130,170p'Repository: tinyfish-io/TinyFish-cookbook
Length of output: 1375
🏁 Script executed:
# Search for StepperTrigger usages in the codebase
rg -n --type=tsx 'StepperTrigger' -C2Repository: tinyfish-io/TinyFish-cookbook
Length of output: 100
🏁 Script executed:
# Search for StepperTrigger usages without tsx filter
rg -n 'StepperTrigger' -C2Repository: tinyfish-io/TinyFish-cookbook
Length of output: 1483
🏁 Script executed:
# Check package.json for `@radix-ui/react-slot` dependency
cat package.json | grep -A20 '"dependencies"' | head -30Repository: tinyfish-io/TinyFish-cookbook
Length of output: 117
🏁 Script executed:
# Find package.json in the repo
find . -name "package.json" -type fRepository: tinyfish-io/TinyFish-cookbook
Length of output: 128
🏁 Script executed:
# Check the repo root structure
ls -la | head -20Repository: tinyfish-io/TinyFish-cookbook
Length of output: 988
🏁 Script executed:
# Search for any files that import from stepper.tsx
rg -n "from.*stepper" -iRepository: tinyfish-io/TinyFish-cookbook
Length of output: 55
🏁 Script executed:
# Check credit-aggregator package.json for dependencies
cat credit-aggregator/package.json | grep -E '("dependencies"|"@radix-ui")' -A30Repository: tinyfish-io/TinyFish-cookbook
Length of output: 656
🏁 Script executed:
# Search for StepperTrigger usage specifically in credit-aggregator
rg -n 'StepperTrigger' credit-aggregator/ -C3Repository: tinyfish-io/TinyFish-cookbook
Length of output: 1875
🏁 Script executed:
# Search for actual usage of Stepper component in pages or app
find credit-aggregator -type f \( -name "*.tsx" -o -name "*.ts" \) -exec grep -l "Stepper" {} \; | grep -v "stepper.tsx"Repository: tinyfish-io/TinyFish-cookbook
Length of output: 55
🏁 Script executed:
# Look for actual StepperTrigger usage in context
rg -n '<Stepper' credit-aggregator/ -A5 -B5Repository: tinyfish-io/TinyFish-cookbook
Length of output: 1659
Fix asChild and onClick handling in StepperTrigger.
When asChild is true (line 144-145), the component returns a plain <div> without any click handler, preventing step advancement. Additionally, ...props spreads after onClick (line 157), allowing a consumer-provided onClick in props to override the internal step change handler.
Use Slot from @radix-ui/react-slot (available as a dependency) to support asChild properly, and merge onClick handlers to preserve both internal and external callbacks:
Proposed fix
+import { Slot } from "@radix-ui/react-slot";
const StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>(
- ({ asChild = false, className, children, ...props }, ref) => {
+ ({ asChild = false, className, children, onClick, ...props }, ref) => {
const { setActiveStep } = useStepper();
const { step, isDisabled } = useStepItem();
- if (asChild) {
- return <div className={className}>{children}</div>;
- }
-
+ const Comp = asChild ? Slot : "button";
+
return (
- <button
+ <Comp
ref={ref}
className={cn(
"inline-flex items-center gap-3 disabled:pointer-events-none disabled:opacity-50",
className,
)}
- onClick={() => setActiveStep(step)}
+ onClick={(event) => {
+ if (isDisabled) {
+ event.preventDefault();
+ return;
+ }
+ setActiveStep(step);
+ onClick?.(event);
+ }}
disabled={isDisabled}
{...props}
>
{children}
- </button>
+ </Comp>
);
},
);📝 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 StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>( | |
| ({ asChild = false, className, children, ...props }, ref) => { | |
| const { setActiveStep } = useStepper(); | |
| const { step, isDisabled } = useStepItem(); | |
| if (asChild) { | |
| return <div className={className}>{children}</div>; | |
| } | |
| return ( | |
| <button | |
| ref={ref} | |
| className={cn( | |
| "inline-flex items-center gap-3 disabled:pointer-events-none disabled:opacity-50", | |
| className, | |
| )} | |
| onClick={() => setActiveStep(step)} | |
| disabled={isDisabled} | |
| {...props} | |
| > | |
| {children} | |
| </button> | |
| ); | |
| }, | |
| ); | |
| import { Slot } from "@radix-ui/react-slot"; | |
| const StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>( | |
| ({ asChild = false, className, children, onClick, ...props }, ref) => { | |
| const { setActiveStep } = useStepper(); | |
| const { step, isDisabled } = useStepItem(); | |
| const Comp = asChild ? Slot : "button"; | |
| return ( | |
| <Comp | |
| ref={ref} | |
| className={cn( | |
| "inline-flex items-center gap-3 disabled:pointer-events-none disabled:opacity-50", | |
| className, | |
| )} | |
| onClick={(event) => { | |
| if (isDisabled) { | |
| event.preventDefault(); | |
| return; | |
| } | |
| setActiveStep(step); | |
| onClick?.(event); | |
| }} | |
| disabled={isDisabled} | |
| {...props} | |
| > | |
| {children} | |
| </Comp> | |
| ); | |
| }, | |
| ); |
🤖 Prompt for AI Agents
In `@credit-aggregator/components/ui/stepper.tsx` around lines 139 - 163,
StepperTrigger currently returns a bare div when asChild is true and spreads
{...props} after its internal onClick, which drops the internal step change or
external handlers; update StepperTrigger to use Slot from `@radix-ui/react-slot`
when asChild is true (rendering <Slot> instead of <div>), and ensure the onClick
handlers are merged (call setActiveStep(step) then call any user-provided
props.onClick) rather than letting props override the internal handler; also
move props spreading so internal props (ref, disabled, className, merged
onClick) are applied while still forwarding remaining props to the Slot/button
to preserve both internal and external behavior.
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
|
|
||
| buffer += decoder.decode(value, { stream: true }); | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() ?? ""; | ||
|
|
||
| for (const line of lines) { | ||
| const event = parseSSELine(line); | ||
| if (!event) continue; | ||
|
|
||
| events.push(event); | ||
|
|
||
| // Capture streaming URL if available | ||
| if (event.streamingUrl) { | ||
| streamingUrl = event.streamingUrl; | ||
| } | ||
|
|
||
| // Log progress if verbose | ||
| if (verbose && event.type === "STEP") { | ||
| console.log(formatStepMessage(event)); | ||
| } | ||
|
|
||
| // Check for completion | ||
| if (isCompleteEvent(event)) { | ||
| if (verbose) { | ||
| console.log("[SUCCESS] Automation completed"); | ||
| } | ||
| return { | ||
| success: true, | ||
| result: event.resultJson, | ||
| streamingUrl, | ||
| events, | ||
| }; | ||
| } | ||
|
|
||
| // Check for errors | ||
| if (isErrorEvent(event)) { | ||
| const errorMsg = event.message || "Automation failed"; | ||
| if (verbose) { | ||
| console.error(`[ERROR] ${errorMsg}`); | ||
| } | ||
| return { | ||
| success: false, | ||
| error: errorMsg, | ||
| streamingUrl, | ||
| events, | ||
| }; | ||
| } |
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 -n credit-aggregator/lib/mino-client.ts | sed -n '1,150p'Repository: tinyfish-io/TinyFish-cookbook
Length of output: 5016
🏁 Script executed:
cat -n credit-aggregator/lib/mino-client.ts | sed -n '48,142p'Repository: tinyfish-io/TinyFish-cookbook
Length of output: 3179
🏁 Script executed:
# Let me verify the ReadableStreamDefaultReader API and check if there are any other early return paths
cat -n credit-aggregator/lib/mino-client.ts | sed -n '34,142p' | grep -n "return"Repository: tinyfish-io/TinyFish-cookbook
Length of output: 180
🌐 Web query:
ReadableStreamDefaultReader cancel() method JavaScript Node.js resource cleanup
💡 Result:
In Node.js (Web Streams API), ReadableStreamDefaultReader.prototype.cancel(reason) is the consumer-side way to say “I’m done—stop producing data and clean up.”
What it does:
- Signals loss of interest and cancels the stream, even if chunks are still queued; any unread queued data is discarded and the stream won’t be readable afterward. [1]
- Runs underlying-source cleanup: the
reasonis forwarded to the stream’s underlying sourcecancel()algorithm (if provided), which is where you should stop I/O, clear timers, close file handles/sockets, etc. [2] - If you already hold the reader lock,
reader.cancel()is effectively the same as canceling the associated stream. [1]
How this relates to resource cleanup in Node:
- If your stream is backed by something expensive (network/file/timers), implement the underlying source’s
cancel(reason)so it actually frees those resources.reader.cancel()is the trigger that calls into that cleanup path. [2]
Practical pattern (early-exit cleanup):
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (shouldStop(value)) {
await reader.cancel("no longer needed"); // triggers underlying cleanup
break;
}
}
} finally {
// Optional: if you intend to hand the stream to someone else instead of canceling it,
// release the lock (this does NOT cancel).
// reader.releaseLock();
}If you don’t want to discard unread queued chunks and you want another consumer to keep reading later, use releaseLock() instead of cancel() (it releases the lock and leaves queued chunks available). [3]
Sources: MDN ReadableStreamDefaultReader.cancel() [1], WHATWG Streams spec (cancel forwards reason to underlying source cancel) [2], MDN releaseLock() [3].
[1] https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/cancel
[2] https://streams.spec.whatwg.org/
[3] https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/releaseLock
🌐 Web query:
Mino API run-sse COMPLETE event documentation stream close
💡 Result:
For Mino’s POST /v1/automation/run-sse, the response is a Server-Sent Events stream (text/event-stream) that emits these message types: STARTED, optional STREAMING_URL, PROGRESS, COMPLETE, plus periodic HEARTBEAT keep-alives. COMPLETE includes the final status (e.g., COMPLETED) and resultJson payload. [1]
How to close the stream: Mino does not document a separate “close” event—treat type: "COMPLETE" as the terminal message (no more meaningful events are expected after it). On the client side, close your SSE connection immediately when you receive COMPLETE (e.g., eventSource.close() or abort your streaming HTTP request). [1]
Source
- Mino docs: “Run browser automation with SSE streaming” (
/v1/automation/run-sse). [1]
Cancel the SSE reader on completion/error to avoid dangling connections.
Early returns at lines 100 and 114 exit without closing the reader, keeping the HTTP connection open and leaking resources. Additionally, the catch block at line 131 also doesn't close the reader if an error occurs during stream reading. Since the Mino API does not auto-close the stream on COMPLETE, the client must explicitly cancel the reader.
Call await reader.cancel() before all early returns, or wrap the reader operations in a try-finally block to ensure cleanup in all cases.
🐛 Proposed fix to cancel the reader
if (isCompleteEvent(event)) {
if (verbose) {
console.log("[SUCCESS] Automation completed");
}
+ await reader.cancel();
return {
success: true,
result: event.resultJson,
streamingUrl,
events,
};
}
// Check for errors
if (isErrorEvent(event)) {
const errorMsg = event.message || "Automation failed";
if (verbose) {
console.error(`[ERROR] ${errorMsg}`);
}
+ await reader.cancel();
return {
success: false,
error: errorMsg,
streamingUrl,
events,
};
}Note: Consider also adding await reader.cancel() before the return at line 125 and wrapping the reader section in try-finally to handle the catch block scenario at line 131.
🤖 Prompt for AI Agents
In `@credit-aggregator/lib/mino-client.ts` around lines 71 - 120, The SSE reader
loop is returning early on completion/error without closing the stream, leaking
the HTTP connection; modify the block that reads from reader (the loop using
reader.read()) to ensure reader.cancel() is always called before any early
return (e.g., before the returns that follow isCompleteEvent and isErrorEvent)
or, better, wrap the entire read/parse loop in a try { ... } finally { await
reader.cancel().catch(()=>{}) } so the reader is cancelled on success, error, or
exceptions; locate symbols reader, parseSSELine, isCompleteEvent, isErrorEvent,
and formatStepMessage to implement the cleanup.
|
|
||
|  | ||
|
|
||
| **Live Demo:** https://credit-aggregator.vercel.app/ |
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.
Use markdown link syntax for the URL.
Per markdownlint (MD034), bare URLs should be wrapped in angle brackets or use proper link syntax.
Proposed fix
-**Live Demo:** https://credit-aggregator.vercel.app/
+**Live Demo:** <https://credit-aggregator.vercel.app/>📝 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.
| **Live Demo:** https://credit-aggregator.vercel.app/ | |
| **Live Demo:** <https://credit-aggregator.vercel.app/> |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
7-7: Bare URL used
(MD034, no-bare-urls)
🤖 Prompt for AI Agents
In `@credit-aggregator/README.md` at line 7, Replace the bare URL after the
"**Live Demo:**" text with proper Markdown link syntax; specifically, change the
line that currently reads "**Live Demo:** https://credit-aggregator.vercel.app/"
to use a link like "**Live Demo:**
[https://credit-aggregator.vercel.app/](https://credit-aggregator.vercel.app/)"
or "**Live Demo:** [Live Demo](https://credit-aggregator.vercel.app/)" so it
conforms to markdownlint (MD034).
| ## Demo | ||
|
|
||
| *[Demo video/screenshot to be added]* | ||
|
|
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.
Remove duplicate "Demo" section.
This section duplicates the heading from line 3 and contains only placeholder text. Consider removing it entirely since line 5 already shows a demo image and line 7 links to the live demo.
Proposed fix
-## Demo
-
-*[Demo video/screenshot to be added]*
-
----
-🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
15-15: Multiple headings with the same content
(MD024, no-duplicate-heading)
🤖 Prompt for AI Agents
In `@credit-aggregator/README.md` around lines 15 - 18, Remove the duplicate
"Demo" section in the README by deleting the second "## Demo" heading and its
placeholder line ("*[Demo video/screenshot to be added]*"); keep the original
demo content already present earlier (the demo image and live demo link) and
ensure no other references or anchors rely on that duplicate heading "Demo".
credit-aggregator
Live Demo: https://credit-aggregator.vercel.app/
Overview
Live Demo: https://credit-aggregator.vercel.app/
A real-time credit card comparison tool that aggregates data from 6 major Singapore financial comparison websites using parallel Mino browser agents. Users describe their requirements in natural language, and the system dispatches concurrent AI agents to scrape and consolidate matching credit cards.
Mino API Integration
This use case demonstrates Mino API usage for browser automation.
Tech Stack
Contributor: Pranav Janakiraman (@pranavjana)