Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions tenders-finder/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your_supabase_anon_key
# TinyFish Web Agent API key
# Get yours at: https://agent.tinyfish.ai/api-keys
TINYFISH_API_KEY="your_api_key_here"

# Set in Supabase Edge Function secrets:
# TINYFISH_API_KEY=your_tinyfish_api_key
# Groq API key
# Get yours at: https://console.groq.com
GROQ_API_KEY="your_api_key_here"
41 changes: 17 additions & 24 deletions tenders-finder/.gitignore
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules/

node_modules
dist
dist-ssr
*.local
# Next.js
.next/
out/

# env files
.env*
!.env.example
# Environment files
.env
.env.local
.env.*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
# Build outputs
*.tsbuildinfo

# Vercel
.vercel

# OS
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Thumbs.db
209 changes: 131 additions & 78 deletions tenders-finder/README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,168 @@
# Government Tender Finder - Singapore
# Tenders Finder
**Live Demo:** _add URL after deploy_

**Live Demo:** https://tender-scout-singapore.lovable.app
**Singapore government tender tracker — parallel TinyFish browser agents scrape multiple tender portals simultaneously and stream results in real time.**

## What This Project Is
Select a sector and the app fires one TinyFish browser agent per tender portal in parallel. Each agent extracts upcoming tenders with deadlines after today's date, streams results back as it completes. Compare tenders side-by-side before deciding which to pursue.

An AI-powered government tender discovery tool for Singapore. It scrapes multiple tender portals in parallel using the TinyFish API, extracts structured tender data, and presents results in a clean, comparable format.
## Architecture

**How TinyFish API is used:** TinyFish browser agents are deployed in parallel to scrape Singapore government tender portals (GeBIZ, Tenders On Time, Bid Detail, etc.), extracting structured fields like tender title, ID, deadline, and eligibility from dynamic pages.

---

## Demo
```
┌─────────────────────────────────────────────────────────────┐
│ Browser (Client) │
│ │
│ SectorSelector → LinkConfigPage → TenderResultsList │
│ AgentPreviewGrid (live iframes) → CompareModal │
│ (results stream in as agents finish) │
└──────────────────────────┬──────────────────────────────────┘
┌────────┴────────┐
▼ ▼
GET /api/discover-links POST /api/scrape
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────────────────┐
│ Returns curated │ │ TinyFish SDK │
│ list of Singapore │ │ │
│ tender portals │ │ client.agent.stream({ url, goal })│
│ │ │ │
│ GeBIZ, Tenders On │ │ EventType.STARTED │
│ Time, Bid Detail, │ │ → agent confirmed │
│ Tenders Info, │ │ EventType.STREAMING_URL │
│ Global Tenders, │ │ → live iframe per agent │
│ Tender Board │ │ EventType.PROGRESS │
│ │ │ → status updates │
│ User can add custom │ │ EventType.COMPLETE │
│ URLs via config page│ │ + RunStatus.COMPLETED │
│ │ │ → tender details → SSE │
└─────────────────────┘ └──────────────────────────────────┘

No database. No cache. Pure in-memory — results fetched live every search.
```

**Demo Video:** https://drive.google.com/file/d/1GXZhJOjiVUP5XcGvTAvRGcYhTWoKXlsE/view?usp=sharing
### TinyFish SDK event flow

---
```
client.agent.stream({ url, goal })
├── EventType.STARTED → agent confirmed running
├── EventType.STREAMING_URL → live iframe URL forwarded to client
├── EventType.PROGRESS → status message forwarded to client
└── EventType.COMPLETE
└── RunStatus.COMPLETED → parse event.result.tenderdetails[]
→ tender cards → SSE → client
```

## Code Snippet
## Covered Portals

```bash
curl -N -X POST "https://agent.tinyfish.ai/v1/automation/run-sse" \
-H "X-API-Key: $TINYFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.gebiz.gov.sg",
"goal": "Extract the latest open government tenders. Return JSON with tenderTitle, agency, tenderID, submissionDeadline, tenderStatus, and tenderLink."
}'
```
| Portal | URL |
|---|---|
| GeBIZ | gebiz.gov.sg |
| GeBIZ Opportunities | gebiz.gov.sg/ptn/opportunity |
| Tenders On Time | tendersontime.com |
| Bid Detail | biddetail.com |
| Tenders Info | tendersinfo.com |
| Global Tenders | globaltenders.com |
| Tender Board | tenderboard.biz |

---
Users can also add custom tender portal URLs via the Link Config page.

## Tech Stack
## Scraping Flow

- **Vite + React (TypeScript)**
- **TinyFish API** (browser automation)
- **Supabase** (edge functions for API proxying)
1. User selects a sector (IT, Construction, Healthcare, etc.)
2. `/api/discover-links` returns the list of curated tender portal URLs
3. User can customise the list on the Link Config page before searching
4. One TinyFish browser agent fires per portal — all in parallel
5. Each agent extracts only tenders with submission deadlines **after today's date**
6. `EventType.STREAMING_URL` events forward live iframe URLs to the client as agents start
7. `EventType.COMPLETE` + `RunStatus.COMPLETED` → parse `event.result.tenderdetails` → stream to client
8. UI updates as each portal finishes — no waiting for the slowest one
9. Select tenders and compare side-by-side in the Compare Modal

## How to Run
## Setup

### Prerequisites

- Node.js 18+
- Supabase project (for edge functions)
- TinyFish API key (get from [tinyfish.ai](https://tinyfish.ai))
- TinyFish API key

### Setup

1. Clone the repository:
```bash
git clone <repo-url>
cd tenders-finder
```
### Environment Variables

2. Install dependencies:
```bash
npm install
cp .env.example .env.local
```

3. Create `.env` from the example:
```bash
cp .env.example .env
```
Then fill in:

4. Set your Supabase credentials in `.env`:
```
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your_supabase_anon_key
```
```env
# TinyFish (required) — https://agent.tinyfish.ai/api-keys
TINYFISH_API_KEY=

5. Set TinyFish API key in Supabase secrets:
```bash
supabase secrets set TINYFISH_API_KEY=your_tinyfish_api_key
# Groq (optional) — https://console.groq.com
GROQ_API_KEY=
```

6. Deploy Supabase edge functions:
```bash
supabase functions deploy tinyfish-tender-search
supabase functions deploy discover-tender-links
```
### Install & Run

7. Run the development server:
```bash
npm install
npm run dev
```

---
Open http://localhost:3000

## Architecture Diagram
## Project Structure

```mermaid
flowchart TB
UI["USER INTERFACE<br/>(React + Tailwind)"]
ORCH["Tender Search Orchestration Layer"]
DB["SUPABASE<br/>(Edge Functions)"]
TF["TINYFISH API<br/>(Browser Automation)"]
TFD["• Parallel web agents<br/>• Browse govt tender portals<br/>• Extract structured fields<br/>• SSE streaming updates"]

UI --> ORCH
ORCH --> DB
DB --> TF
TF --> TFD
```
tenders-finder/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx # Main UI
│ │ ├── globals.css
│ │ └── api/
│ │ ├── discover-links/route.ts # GET — returns curated portal list
│ │ └── scrape/route.ts # POST — TinyFish agent stream per portal
│ ├── components/
│ │ ├── tender/
│ │ │ ├── SectorSelector.tsx # Sector picker
│ │ │ ├── SectorIcon.tsx # Sector icons
│ │ │ ├── LinkConfigPage.tsx # Custom URL configuration
│ │ │ ├── AgentPreviewGrid.tsx # Live agent iframe grid
│ │ │ ├── AgentPreviewCard.tsx # Per-agent status + iframe
│ │ │ ├── TenderResultsList.tsx # Tender result cards list
│ │ │ ├── TenderResultCard.tsx # Individual tender card
│ │ │ ├── CompareButton.tsx # Trigger comparison
│ │ │ ├── CompareModal.tsx # Side-by-side comparison
│ │ │ ├── LiveBrowserModal.tsx # Full-screen agent browser view
│ │ │ └── Header.tsx
│ │ └── ui/ # shadcn/ui components
│ ├── hooks/
│ │ └── useTenderSearch.ts # Search state + SSE client
│ ├── lib/
│ │ └── utils.ts
│ └── types/
│ └── tender.ts # TypeScript definitions
├── .env.example
├── .gitignore
└── package.json
```

---
## Constraint Checklist

## Environment Variables
| Constraint | Status |
|---|---|
| External database used? | NO (pure in-memory) |
| Cache layer used? | NO (all results fetched live) |
| Scraping parallel? | YES (one agent per portal, all concurrent) |
| Live browser preview? | YES (`EventType.STREAMING_URL` → iframe per agent) |
| Deadline filtering? | YES (only tenders with future deadlines extracted) |
| Custom portal URLs? | YES (user-configurable via Link Config page) |
| Tender comparison? | YES (select multiple, compare side-by-side) |

| Variable | Where | Description |
|----------|-------|-------------|
| `VITE_SUPABASE_URL` | `.env` | Supabase project URL |
| `VITE_SUPABASE_PUBLISHABLE_KEY` | `.env` | Supabase anon key |
| `TINYFISH_API_KEY` | Supabase secrets | TinyFish API key |
## Tech Stack

Contributor: Krishna Agarwal (@KrishnaAgarwal7531)
- **Framework:** Next.js 15 (App Router), TypeScript, Tailwind CSS
- **Browser Agents:** TinyFish SDK (`client.agent.stream`)
- **Icons:** Lucide React
- **Deployment:** Vercel
Binary file removed tenders-finder/bun.lockb
Binary file not shown.
20 changes: 0 additions & 20 deletions tenders-finder/components.json

This file was deleted.

26 changes: 0 additions & 26 deletions tenders-finder/eslint.config.js

This file was deleted.

26 changes: 0 additions & 26 deletions tenders-finder/index.html

This file was deleted.

5 changes: 5 additions & 0 deletions tenders-finder/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
3 changes: 3 additions & 0 deletions tenders-finder/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;
Loading