|
1 | 1 | # SafeDine |
| 2 | +**Live Demo: https://restaurant-comparison-tool.vercel.app** |
2 | 3 |
|
3 | | -**Live:** [https://restaurant-comparison-tool.vercel.app](https://restaurant-comparison-tool.vercel.app) |
| 4 | +**Pre-visit restaurant safety intelligence — AI agents compare 2–5 restaurants simultaneously before you dine.** |
4 | 5 |
|
5 | | -SafeDine is a pre-visit restaurant safety intelligence tool that compares 2–5 restaurants before dining by analyzing Google Maps reviews, menu photos, and allergen signals. It uses the TinyFish API to dispatch parallel web agents — one per restaurant — that each navigate Google Maps, read 8–12 reviews, check menu images, and return a structured safety report with scores, allergen risks, and dietary suitability ratings. |
| 6 | +Enter a city and up to 5 restaurant names, select your allergens and dietary preferences, and SafeDine dispatches one TinyFish browser agent per restaurant in parallel. Each agent navigates Google Maps, reads 8–12 reviews, checks menu photos for allergen signals, and returns a structured safety report. Results stream back as each agent completes. |
6 | 7 |
|
7 | | -## Demo |
| 8 | +## Architecture |
8 | 9 |
|
9 | | -https://github.com/user-attachments/assets/c684dac5-5e89-43fe-9592-0665a31513f6 |
10 | | - |
11 | | - |
12 | | -## TinyFish API Usage |
| 10 | +``` |
| 11 | +┌─────────────────────────────────────────────────────────────┐ |
| 12 | +│ Browser (Client) │ |
| 13 | +│ │ |
| 14 | +│ SearchForm → AllergenSelector → ComparisonDashboard │ |
| 15 | +│ (live previews + results stream in) │ |
| 16 | +└──────────────────────────┬──────────────────────────────────┘ |
| 17 | + │ POST /api/scrape (one per restaurant, parallel) |
| 18 | + │ (SSE — results stream as agents finish) |
| 19 | +┌──────────────────────────▼──────────────────────────────────┐ |
| 20 | +│ Next.js API Route │ |
| 21 | +│ /api/scrape/route.ts │ |
| 22 | +│ │ |
| 23 | +│ TinyFish SDK — client.agent.stream() per restaurant │ |
| 24 | +│ TINYFISH_API_KEY stored server-side in .env.local │ |
| 25 | +│ │ |
| 26 | +│ SSE events: │ |
| 27 | +│ STREAMING_URL → live browser iframe per agent │ |
| 28 | +│ STEP → agent progress updates │ |
| 29 | +│ COMPLETE → structured safety JSON │ |
| 30 | +│ ERROR → failure message │ |
| 31 | +└──────────┬──────────────┬──────────────┬────────────────────┘ |
| 32 | + │ │ │ |
| 33 | + ▼ ▼ ▼ |
| 34 | + ┌────────────┐ ┌────────────┐ ┌────────────┐ |
| 35 | + │ Google │ │ Google │ │ Google │ ... (2–5) |
| 36 | + │ Maps: │ │ Maps: │ │ Maps: │ |
| 37 | + │ Rest. A │ │ Rest. B │ │ Rest. C │ |
| 38 | + └────────────┘ └────────────┘ └────────────┘ |
| 39 | +
|
| 40 | +No database. No cache. Pure in-memory — results fetched live every search. |
| 41 | +``` |
13 | 42 |
|
14 | | -The app calls the TinyFish SSE endpoint once per restaurant, in parallel. Each agent navigates Google Maps, samples reviews for safety signals, checks menu photos for allergen labeling, and returns a structured JSON report: |
| 43 | +### TinyFish SDK event flow |
15 | 44 |
|
16 | | -```typescript |
17 | | -const response = await fetch("https://agent.tinyfish.ai/v1/automation/run-sse", { |
18 | | - method: "POST", |
19 | | - headers: { |
20 | | - "X-API-Key": import.meta.env.VITE_TINYFISH_API_KEY, |
21 | | - "Content-Type": "application/json", |
22 | | - }, |
23 | | - body: JSON.stringify({ |
24 | | - url: "https://www.google.com/maps", |
25 | | - goal: `You are a fast food-safety research agent. Investigate "${restaurantName}" in ${city}. |
26 | | - Stay ONLY on Google Maps — do NOT visit external websites. |
| 45 | +``` |
| 46 | +client.agent.stream({ url, goal }, { |
| 47 | + onStreamingUrl: (event) => forward iframe URL to client, |
| 48 | + onProgress: (event) => forward step description to client, |
| 49 | + onComplete: (event) => { |
| 50 | + if (event.status === RunStatus.FAILED) → ERROR |
| 51 | + else → event.result → structured safety JSON → COMPLETE → SSE |
| 52 | + } |
| 53 | +}) |
| 54 | +
|
| 55 | +// for-await drains the stream as fallback |
| 56 | +for await (const event of stream) { |
| 57 | + if (event.type === EventType.COMPLETE) → handle result |
| 58 | +} |
| 59 | +``` |
27 | 60 |
|
28 | | - STEP 1 — FIND THE RESTAURANT on Google Maps: |
29 | | - Search "${restaurantName} ${city}". Confirm the correct listing. |
| 61 | +## What Each Agent Extracts |
30 | 62 |
|
31 | | - STEP 2 — SAMPLE REVIEWS (keep it fast): |
32 | | - Open the Reviews tab. Read 8–12 recent reviews. Prioritize mentions of: |
33 | | - - Food poisoning, allergic reactions, cross-contamination |
34 | | - - Hygiene, cleanliness, staff responsiveness |
35 | | - Focus on user allergens: ${allergenList} |
| 63 | +Each agent navigates Google Maps for the restaurant and returns: |
36 | 64 |
|
37 | | - STEP 3 — CHECK MENU IMAGES (if available on Maps): |
38 | | - Look at the Menu tab or Photos section (3–4 images max). |
| 65 | +- **Safety score** (0–100) based on review signals and allergen mentions |
| 66 | +- **Allergen risks** — detected allergens flagged against your selections |
| 67 | +- **Dietary suitability** — vegan, vegetarian, gluten-free, halal, kosher ratings |
| 68 | +- **Review highlights** — safety-relevant quotes from real reviews |
| 69 | +- **Menu photo analysis** — allergen labeling visible in menu images |
| 70 | +- **Confidence level** — how much data the agent found |
39 | 71 |
|
40 | | - STEP 4 — RETURN RESULTS as JSON: |
41 | | - { "restaurantName": "...", "overallSafetyScore": 75, |
42 | | - "allergenRisks": [...], "safetySignals": [...], ... }`, |
43 | | - }), |
44 | | -}); |
45 | | -``` |
| 72 | +## Features |
46 | 73 |
|
47 | | -The response streams SSE events including a `streamingUrl` (live view of the agent navigating Google Maps) and a final `COMPLETE` event with the extracted safety data JSON. |
| 74 | +- Compare **2–5 restaurants** simultaneously |
| 75 | +- Select from **common allergens** (nuts, dairy, gluten, shellfish, etc.) |
| 76 | +- Set **dietary preferences** (vegan, vegetarian, halal, etc.) |
| 77 | +- Watch **live browser agent iframes** as each agent navigates Google Maps |
| 78 | +- Results ranked by safety score — worst risks surfaced first |
48 | 79 |
|
49 | | -## How to Run |
| 80 | +## Setup |
50 | 81 |
|
51 | 82 | ### Prerequisites |
52 | 83 |
|
53 | 84 | - Node.js 18+ |
54 | | -- A TinyFish API key ([get one here](https://agent.tinyfish.ai)) |
55 | | - |
56 | | -### Setup |
| 85 | +- TinyFish API key |
57 | 86 |
|
58 | | -1. Install dependencies: |
| 87 | +### Environment Variables |
59 | 88 |
|
60 | 89 | ```bash |
61 | | -cd restaurant-comparison-tool |
62 | | -npm install |
| 90 | +cp .env.example .env.local |
63 | 91 | ``` |
64 | 92 |
|
65 | | -2. Create a `.env` file with your TinyFish API key: |
| 93 | +Then fill in: |
66 | 94 |
|
67 | | -``` |
68 | | -VITE_TINYFISH_API_KEY=your_tinyfish_api_key_here |
| 95 | +```env |
| 96 | +# TinyFish (required) — https://agent.tinyfish.ai/api-keys |
| 97 | +TINYFISH_API_KEY=your-tinyfish-api-key |
69 | 98 | ``` |
70 | 99 |
|
71 | | -3. Start the dev server: |
| 100 | +### Install & Run |
72 | 101 |
|
73 | 102 | ```bash |
| 103 | +npm install |
74 | 104 | npm run dev |
75 | 105 | ``` |
76 | 106 |
|
77 | | -4. Open [http://localhost:5173](http://localhost:5173) |
| 107 | +Open http://localhost:3000 |
78 | 108 |
|
79 | | -## Architecture Diagram |
| 109 | +## Project Structure |
80 | 110 |
|
81 | 111 | ``` |
82 | | -┌──────────────────────────────────────────────────────────────┐ |
83 | | -│ User (Browser) │ |
84 | | -│ ┌────────────────────────────────────────────────────────┐ │ |
85 | | -│ │ React + Vite Frontend (Tailwind + shadcn + Framer) │ │ |
86 | | -│ │ │ │ |
87 | | -│ │ 1. Enter city + 2–5 restaurant names │ │ |
88 | | -│ │ 2. Select allergens & dietary preferences │ │ |
89 | | -│ │ 3. Click "Compare Restaurants" │ │ |
90 | | -│ │ 4. Watch live browser previews as agents research │ │ |
91 | | -│ │ 5. View ranked safety cards + detail panel │ │ |
92 | | -│ └────────────────────┬───────────────────────────────────┘ │ |
93 | | -└───────────────────────┼──────────────────────────────────────┘ |
94 | | - │ POST /v1/automation/run-sse (x N restaurants, parallel) |
95 | | - ▼ |
96 | | -┌──────────────────────────────────────────────────────────────┐ |
97 | | -│ TinyFish API (agent.tinyfish.ai) │ |
98 | | -│ │ |
99 | | -│ Receives goal prompt + Google Maps URL per restaurant │ |
100 | | -│ Spins up a web agent for each request │ |
101 | | -│ │ |
102 | | -│ SSE Stream Events: │ |
103 | | -│ • streamingUrl → live browser preview (iframe) │ |
104 | | -│ • STEP → agent progress updates │ |
105 | | -│ • COMPLETE → structured safety JSON │ |
106 | | -│ • ERROR → failure message │ |
107 | | -└────────┬──────────────┬──────────────┬───────────────────────┘ |
108 | | - │ │ │ |
109 | | - ▼ ▼ ▼ |
110 | | - ┌───────────┐ ┌───────────┐ ┌───────────┐ |
111 | | - │ Google │ │ Google │ │ Google │ ... (2–5 restaurants) |
112 | | - │ Maps: │ │ Maps: │ │ Maps: │ |
113 | | - │ Rest. A │ │ Rest. B │ │ Rest. C │ |
114 | | - └───────────┘ └───────────┘ └───────────┘ |
| 112 | +restaurant-comparison-tool/ |
| 113 | +├── src/ |
| 114 | +│ ├── app/ |
| 115 | +│ │ ├── layout.tsx # Root layout |
| 116 | +│ │ ├── page.tsx # Main UI |
| 117 | +│ │ ├── globals.css |
| 118 | +│ │ └── api/ |
| 119 | +│ │ └── scrape/route.ts # POST /api/scrape — SSE + TinyFish per restaurant |
| 120 | +│ ├── components/ |
| 121 | +│ │ ├── search/ |
| 122 | +│ │ │ ├── SearchForm.tsx # City + restaurant name inputs |
| 123 | +│ │ │ ├── AllergenSelector.tsx # Allergen multi-select |
| 124 | +│ │ │ ├── PreferenceSelector.tsx # Dietary preference selector |
| 125 | +│ │ │ └── RestaurantInput.tsx # Individual restaurant input row |
| 126 | +│ │ ├── live/ |
| 127 | +│ │ │ ├── LiveSearchPanel.tsx # Live agent status panel |
| 128 | +│ │ │ ├── LiveBrowserPreview.tsx # Agent iframe grid |
| 129 | +│ │ │ ├── RestaurantAgentCard.tsx # Per-agent status card |
| 130 | +│ │ │ └── AgentStatusIndicator.tsx # Running/done indicator |
| 131 | +│ │ ├── results/ |
| 132 | +│ │ │ ├── ComparisonDashboard.tsx # Ranked results layout |
| 133 | +│ │ │ ├── RestaurantResultCard.tsx # Per-restaurant result card |
| 134 | +│ │ │ ├── SafetyScoreRing.tsx # Score visualisation |
| 135 | +│ │ │ ├── AllergenRiskPanel.tsx # Allergen risk breakdown |
| 136 | +│ │ │ ├── AllergenRiskBadge.tsx # Individual allergen badge |
| 137 | +│ │ │ ├── FitExplanation.tsx # Dietary fit explanation |
| 138 | +│ │ │ ├── ConfidenceIndicator.tsx # Data confidence level |
| 139 | +│ │ │ ├── ResultDetailPanel.tsx # Full detail drawer |
| 140 | +│ │ │ ├── GoogleMapsLink.tsx # Link to Google Maps listing |
| 141 | +│ │ │ └── AgentLoadingCard.tsx # Skeleton while agent runs |
| 142 | +│ │ ├── layout/ |
| 143 | +│ │ │ ├── Header.tsx |
| 144 | +│ │ │ └── Footer.tsx |
| 145 | +│ │ └── ui/ # badge, button, card, dialog, input, etc. |
| 146 | +│ ├── hooks/ |
| 147 | +│ │ └── useRestaurantSearch.ts # SSE client + state management |
| 148 | +│ ├── context/ |
| 149 | +│ │ └── SearchContext.tsx # Search state context |
| 150 | +│ └── lib/ |
| 151 | +│ ├── goal-builder.ts # Builds TinyFish goal prompt per restaurant |
| 152 | +│ ├── score-calculator.ts # Safety score computation |
| 153 | +│ ├── allergens.ts # Allergen definitions |
| 154 | +│ ├── tinyfish-client.ts # SDK wrapper |
| 155 | +│ ├── constants.ts # App-wide constants |
| 156 | +│ └── utils.ts # Shared helpers |
| 157 | +├── next.config.ts |
| 158 | +└── package.json |
115 | 159 | ``` |
| 160 | + |
| 161 | +## Constraint Checklist |
| 162 | + |
| 163 | +| Constraint | Status | |
| 164 | +|---|---| |
| 165 | +| External database used? | NO (pure in-memory) | |
| 166 | +| Cache layer used? | NO (all results fetched live) | |
| 167 | +| Scraping parallel? | YES (one `client.agent.stream` per restaurant, all concurrent) | |
| 168 | +| Live browser preview? | YES (`onStreamingUrl` → iframe per agent) | |
| 169 | +| Allergen risk detection? | YES (review + menu photo analysis) | |
| 170 | +| Results ranked? | YES (by safety score, worst risks first) | |
| 171 | + |
| 172 | +## Tech Stack |
| 173 | + |
| 174 | +- **Framework:** Next.js 15 (App Router), TypeScript, Tailwind CSS |
| 175 | +- **UI Components:** shadcn/ui |
| 176 | +- **Browser Agents:** TinyFish SDK (`client.agent.stream`) |
| 177 | +- **Icons:** Lucide React |
| 178 | +- **Deployment:** Vercel |
0 commit comments