diff --git a/stay-scout-hub/.env.example b/stay-scout-hub/.env.example new file mode 100644 index 000000000..7e06218cb --- /dev/null +++ b/stay-scout-hub/.env.example @@ -0,0 +1,5 @@ +# TinyFish API key — https://agent.tinyfish.ai/api-keys +TINYFISH_API_KEY=your-tinyfish-api-key + +# Google Gemini API key — https://aistudio.google.com/apikey +GEMINI_API_KEY=your-gemini-api-key diff --git a/stay-scout-hub/.gitignore b/stay-scout-hub/.gitignore new file mode 100644 index 000000000..87ef3968f --- /dev/null +++ b/stay-scout-hub/.gitignore @@ -0,0 +1,30 @@ +# Environment files — never commit these +.env +.env.local +.env.*.local +.env.development +.env.production + +# Next.js +.next/ +out/ + +# Dependencies +node_modules/ + +# Build output +dist/ +build/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Editor +.vscode/ +.idea/ +next-env.d.ts diff --git a/stay-scout-hub/README.md b/stay-scout-hub/README.md index 3029e78fc..4141975aa 100644 --- a/stay-scout-hub/README.md +++ b/stay-scout-hub/README.md @@ -1,160 +1,154 @@ -# StayScout Hub +# Stay Scout Hub +**Live Demo:** _add URL after deploy_ -**Live:** [https://stayscouthub.lovable.app/](https://stayscouthub.lovable.app/) +**Smart hotel research tool — AI agents find the right neighborhood and booking platform before you book.** -StayScout Hub helps travelers decide **where to stay in a city — and why**. -Instead of jumping straight to hotel booking sites, it analyzes **neighborhoods first**, using AI-powered area discovery and live browser research to explain the pros, cons, risks, and best hotels in each area. +Enter your destination, travel purpose, and dates. Stay Scout discovers the best neighborhoods for your trip using real travel guides, researches each area via Google Maps, and checks availability across all relevant booking platforms simultaneously. -The app combines: -- **Gemini** for intelligent area suggestions -- **Mino autonomous browser agents** for real-time hotel and neighborhood research +## Architecture ---- - -## Demo - -https://github.com/user-attachments/assets/0413dc34-c20d-481e-9a70-ca860cdf36e1 - ---- - -## Mino API Usage - -StayScout Hub uses the **Mino SSE Browser Automation API** to research each recommended neighborhood in parallel. - -For every suggested area, a Mino agent: -- Opens Google Maps and hotel listings -- Observes location context, nearby landmarks, and transport -- Scans hotel ratings and review signals -- Returns a structured analysis with suitability, pros/cons, risks, and top hotels - -### Example Mino API Call - -```ts -const response = await fetch("https://agent.tinyfish.ai/v1/automation/run-sse", { - method: "POST", - headers: { - "X-API-Key": process.env.TINYFISH_API_KEY, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - url: `https://www.google.com/maps/search/hotels+in+${areaName},+${city}`, - goal: ` -You are researching "${areaName}" in ${city} to help a traveler -decide if it's a good place to stay for a ${purpose} trip. - -Return structured JSON with suitability, pros, cons, risks, -and top hotel recommendations. -`, - }), -}) +``` +┌─────────────────────────────────────────────────────────────┐ +│ Browser (Client) │ +│ │ +│ SearchFormV2 → PurposeSelector → AreaResultsSection │ +│ (results stream in as agents finish) │ +└────────────────────────────┬────────────────────────────────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌───────────────┐ ┌────────────────────┐ +│ /api/discover- │ │ /api/discover-│ │ /api/research-area │ +│ areas │ │ platforms │ │ /api/check-platform│ +│ │ │ │ │ │ +│ TinyFish Search │ │ TinyFish │ │ TinyFish Agent │ +│ → find guides │ │ Search │ │ client.agent │ +│ │ │ → find which │ │ .stream(...) │ +│ TinyFish Fetch │ │ platforms │ │ │ +│ → extract │ │ operate here │ │ SSE → client │ +│ neighborhoods │ │ │ │ │ +│ │ │ Gemini LLM │ │ │ +│ Gemini LLM │ │ → build URLs │ │ │ +│ → structure │ │ │ │ │ +└─────────────────┘ └───────────────┘ └────────────────────┘ ``` -The response streams **Server-Sent Events (SSE)**: +### All three TinyFish APIs — each used for what it does best -- `STREAMING_URL` → live browser view +``` +Search API → client.search.query({ query }) + Discovers relevant travel guide URLs and booking platform URLs + No browser needed — fast structured results + +Fetch API → client.fetch.get_contents({ urls, format: "markdown" }) + Extracts clean text from travel guides found by Search + Up to 10 URLs per call, returned as clean markdown + +Agent API → client.agent.stream({ url, goal }) + Full browser navigation for Google Maps area research + and entering dates/guests on booking platforms + EventType.STREAMING_URL → live iframe in UI + EventType.COMPLETE + RunStatus.COMPLETED → event.result +``` -- `STATUS` → progress updates +## Flow -- `COMPLETE` → final area analysis JSON +1. User enters city, purpose, dates, guests +2. **`/api/discover-areas`** — Search finds travel guides → Fetch extracts neighborhood content → Gemini structures into area recommendations +3. **`/api/discover-platforms`** — Search finds which booking platforms operate in that city/region +4. **`/api/research-area`** — one TinyFish agent per area navigates Google Maps, returns suitability score, pros/cons, walkability, noise level, top hotels (streamed via SSE) +5. **`/api/check-platform`** — one TinyFish agent per platform enters dates + guests, returns direct search results URL (streamed via SSE) -## How It Works +## Purpose Modes -- User enters city and travel purpose (e.g., San Francisco — Business trip) +| Purpose | What it optimises for | +|---|---| +| Business | Proximity to business district, conference centers | +| Exam / Interview | Quiet area, good sleep, low noise | +| Family Visit | Family-friendly, comfortable, residential | +| Sightseeing | Walking distance to attractions, transport | +| Late Night | Nightlife access, flexible check-in | +| Airport Transit | Proximity to airport, shuttle access | -- Area discovery (Gemini) suggests 3–6 relevant neighborhoods +## Setup -- Parallel Mino agents launch — one per area +### Prerequisites -- Live research streams into the UI with screenshots and status updates +- Node.js 18+ +- TinyFish API key +- Gemini API key -- Final recommendations explain where to stay, with reasons and hotel examples +### Environment Variables -## Architecture Overview ```bash -┌─────────────────────────────────────────────────────────┐ -│ User (Browser) │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ React Frontend │ │ -│ │ │ │ -│ │ 1. Enter city & purpose │ │ -│ │ 2. View live area research cards │ │ -│ │ 3. Compare neighborhoods │ │ -│ └──────────────────┬──────────────────────────────┘ │ -└─────────────────────┼───────────────────────────────────┘ - │ - Stage 1 │ POST /discover-areas - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Gemini API │ -│ - Suggests best neighborhoods for the trip │ -└─────────────────────┬───────────────────────────────────┘ - │ - Stage 2 │ POST /research-area (x N, parallel) - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Mino API │ -│ - Launches browser agents per area │ -│ - Streams live previews and status │ -│ - Returns structured area analysis │ -└──────────┬──────────┬──────────┬──────────┬────────────┘ - ▼ ▼ ▼ ▼ - Area 1 Area 2 Area 3 Area N +cp .env.example .env.local ``` -## What the App Analyzes - -For each area, StayScout Hub returns: - - - Overall suitability score (1–10) - - - Who the area is best for (business, family, sightseeing, etc.) - - - Pros, cons, and potential risks - - - Walkability, noise level, safety notes - - - Top 3–5 recommended hotels with ratings +Then fill in: -This helps users choose the right neighborhood first, before comparing hotel prices. +```env +# TinyFish (required) — https://agent.tinyfish.ai/api-keys +TINYFISH_API_KEY=your-tinyfish-api-key -## How to Run -Prerequisites : -- Node.js 18+ -- Gemini API key -- Mino API key [get one her](https://mino.ai/api-keys) - -## Setup - -1. Install dependencies: -```bash -cd stay-scout-hub -npm install +# Google Gemini (required) — https://aistudio.google.com/apikey +GEMINI_API_KEY=your-gemini-api-key ``` -2. Create a .env.local file: -```bash -GEMINI_API_KEY=your_gemini_api_key -TINYFISH_API_KEY=your_mino_api_key -``` +### Install & Run -3. Start the dev server: ```bash +npm install npm run dev ``` -4. Open http://localhost:3000 - -## Environment Variables - -- GEMINI_API_KEY - Area discovery and neighborhood reasoning -- TINYFISH_API_KEY - Live browser automation for area research +Open http://localhost:3000 -## Notes +## Project Structure -- Area discovery uses Gemini for reasoning, not web scraping - -- All neighborhood research uses live browser automation via Mino - -- No booking platforms or hotel pricing APIs are required +``` +stay-scout-hub/ +├── src/ +│ ├── app/ +│ │ ├── layout.tsx # Root layout +│ │ ├── page.tsx # Main UI +│ │ ├── globals.css +│ │ └── api/ +│ │ ├── discover-areas/ # Search + Fetch → neighborhood discovery +│ │ ├── discover-platforms/ # Search → booking platform discovery +│ │ ├── research-area/ # Agent → Google Maps area research (SSE) +│ │ └── check-platform/ # Agent → platform date/guest search (SSE) +│ ├── components/ +│ │ ├── SearchFormV2.tsx +│ │ ├── PurposeSelector.tsx +│ │ ├── AreaCard.tsx +│ │ └── LiveBrowserPreview.tsx # Live agent iframe grid +│ ├── hooks/ +│ │ ├── useAreaSearch.ts # Area discovery + research state +│ ├── lib/ +│ │ ├── api/area-search.ts +│ │ └── utils.ts +│ └── types/hotel.ts +├── next.config.ts +└── package.json +``` -- Results explain why an area is good or bad — not just where to book +## Constraint Checklist + +| Constraint | Status | +|---|---| +| External database used? | NO (pure in-memory) | +| Cache layer used? | NO (all results fetched live) | +| All three TinyFish APIs used? | YES (Search, Fetch, Agent) | +| Area research via real browser? | YES (`client.agent.stream` → Google Maps) | +| Platform check via real browser? | YES (`client.agent.stream` → booking sites) | +| Live browser preview? | YES (`EventType.STREAMING_URL` → iframe) | + +## Tech Stack + +- **Framework:** Next.js 15 (App Router), TypeScript, Tailwind CSS +- **Animations:** Framer Motion +- **Icons:** Lucide React +- **Browser Agents:** TinyFish SDK (`client.agent.stream`, `client.search.query`, `client.fetch.get_contents`) +- **LLM:** Gemini (`gemini-2.0-flash`) for structuring extracted content +- **Deployment:** Vercel diff --git a/stay-scout-hub/docs/MINO_AREA_RESEARCH_API.md b/stay-scout-hub/docs/MINO_AREA_RESEARCH_API.md deleted file mode 100644 index aea873500..000000000 --- a/stay-scout-hub/docs/MINO_AREA_RESEARCH_API.md +++ /dev/null @@ -1,640 +0,0 @@ -# Mino API Developer Documentation: Area Research Use Case - -> **Purpose:** This document provides a complete technical reference for developers integrating the Mino browser automation API for intelligent hotel area research. - ---- - -## Table of Contents - -1. [Product Architecture Overview](#product-architecture-overview) -2. [API Relationships](#api-relationships) -3. [API Call Frequency](#api-call-frequency) -4. [Orchestration Flow](#orchestration-flow) -5. [Code Examples](#code-examples) -6. [Goal Prompt Reference](#goal-prompt-reference) -7. [Sample Streaming Output](#sample-streaming-output) -8. [Error Handling](#error-handling) - ---- - -## Product Architecture Overview - -This system helps travelers decide **where to stay** by combining AI-powered area discovery with live browser research. It uses a two-stage pipeline: - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ USER INPUT │ -│ City: "Bangalore" | Purpose: "Business trip" | Dates: optional │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ STAGE 1: AREA DISCOVERY (Gemini) │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Edge Function: discover-areas │ │ -│ │ API: Google Gemini 2.0 Flash │ │ -│ │ Output: 3-6 neighborhood recommendations │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ┌───────────────┼───────────────┐ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ STAGE 2: PARALLEL RESEARCH (Mino) │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ research- │ │ research- │ │ research- │ │ research- │ │ -│ │ area │ │ area │ │ area │ │ area │ │ -│ │ (Area 1) │ │ (Area 2) │ │ (Area 3) │ │ (Area N) │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ Mino Agent │ │ Mino Agent │ │ Mino Agent │ │ Mino Agent │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ │ │ │ │ │ -│ └───────────────┴───────────────┴───────────────┘ │ -│ │ │ -│ SSE Streams (real-time updates) │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ FRONTEND DISPLAY │ -│ • Live browser screenshots via streamingUrl │ -│ • Real-time status updates │ -│ • Final analysis with pros/cons/risks/top hotels │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Components - -| Component | Technology | Purpose | -|-----------|------------|---------| -| Frontend | React + TypeScript | User interface, SSE consumption | -| `discover-areas` | Supabase Edge Function | Calls Gemini to suggest neighborhoods | -| `research-area` | Supabase Edge Function | Calls Mino to research each area | -| Gemini API | Google AI | Natural language area recommendations | -| Mino API | Browser Automation | Live web research with screenshots | - ---- - -## API Relationships - -### Dependency Chain - -``` -User Request - │ - ├──► discover-areas (Gemini) - │ │ - │ └──► Returns: AreaSuggestion[] - │ - └──► research-area × N (Mino) [PARALLEL] - │ - └──► Returns: SSE Stream → AreaResearchResult -``` - -### API Details - -| API | Endpoint | Auth | Rate Limits | -|-----|----------|------|-------------| -| Gemini | `generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent` | API Key | Standard Gemini limits | -| Mino | `agent.tinyfish.ai/v1/automation/run-sse` | X-API-Key header | Per-account limits | - ---- - -## API Call Frequency - -For a typical search request: - -| Stage | API | Calls | Notes | -|-------|-----|-------|-------| -| Discovery | Gemini | **1** | Single call to get area suggestions | -| Research | Mino | **3-6** | One per discovered area (parallel) | - -**Total per search:** 1 Gemini call + 3-6 Mino calls - ---- - -## Orchestration Flow - -### Sequence Diagram - -``` -┌──────────┐ ┌──────────────┐ ┌─────────────────┐ ┌────────┐ ┌──────┐ -│ React │ │ useAreaSearch│ │ discover-areas │ │ Gemini │ │ Mino │ -│ UI │ │ Hook │ │ Edge Function │ │ API │ │ API │ -└────┬─────┘ └──────┬───────┘ └────────┬────────┘ └───┬────┘ └──┬───┘ - │ │ │ │ │ - │ search(params) │ │ │ │ - │─────────────────►│ │ │ │ - │ │ │ │ │ - │ │ POST /discover-areas │ │ │ - │ │─────────────────────►│ │ │ - │ │ │ │ │ - │ │ │ generateContent │ │ - │ │ │──────────────────►│ │ - │ │ │ │ │ - │ │ │ ◄─────────────────│ │ - │ │ │ areas[] │ │ - │ │ ◄────────────────────│ │ │ - │ │ areas[] │ │ │ - │ │ │ │ │ - │ setResults │ │ │ │ - │◄─────────────────│ │ │ │ - │ (pending cards) │ │ │ │ - │ │ │ │ │ - │ │ ┌─────────────── PARALLEL LOOP ─────────────────┐ │ - │ │ │ For each area: │ │ - │ │ │ │ │ - │ │ │ POST /research-area (SSE) │ │ - │ │ │───────────────────────────────────────────────│────► - │ │ │ │ │ - │ │ │ ◄──────────────────────────────────────────────────│ - │ │ │ SSE: STATUS, SCREENSHOT, COMPLETE │ │ - │ │ │ │ │ - │ onStatus/ │ │ │ │ - │ onComplete │ │ │ │ - │◄─────────────────│─┘ │ │ - │ │ │ │ - │ │ │ │ -``` - -### React Hook Orchestration - -```typescript -// useAreaSearch.ts - Simplified flow -const search = async (params: SearchParams) => { - // Stage 1: Discover areas (single Gemini call) - const areas = await discoverAreas(params); - - // Initialize UI with pending cards - setResults(areas.map(a => ({ ...a, status: 'pending' }))); - - // Stage 2: Research in parallel (multiple Mino calls) - const promises = areas.map(area => { - return new Promise((resolve) => { - researchArea( - area, - params, - onStatus, // Live updates - onComplete, // Final result - onError // Error handling - ); - }); - }); - - await Promise.all(promises); -}; -``` - ---- - -## Code Examples - -### cURL: Discover Areas (Gemini) - -```bash -curl -X POST 'https://YOUR_PROJECT.supabase.co/functions/v1/discover-areas' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_ANON_KEY' \ - -d '{ - "city": "Bangalore", - "purpose": "business", - "checkIn": "2024-03-15", - "checkOut": "2024-03-18" - }' -``` - -**Response:** -```json -{ - "areas": [ - { - "id": "whitefield", - "name": "Whitefield", - "type": "neighborhood", - "description": "Major IT hub in East Bangalore", - "whyRecommended": "Home to major tech parks, excellent for business travelers with corporate hotels", - "keyLocations": ["ITPL", "Phoenix Marketcity", "VR Mall"] - }, - { - "id": "mg-road", - "name": "MG Road", - "type": "neighborhood", - "description": "Central business and shopping district", - "whyRecommended": "Well-connected metro access, walking distance to key business addresses", - "keyLocations": ["UB City", "Cubbon Park", "Brigade Road"] - } - ] -} -``` - -### cURL: Research Area (Mino SSE) - -```bash -curl -X POST 'https://YOUR_PROJECT.supabase.co/functions/v1/research-area' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_ANON_KEY' \ - -d '{ - "area": { - "id": "whitefield", - "name": "Whitefield", - "type": "neighborhood", - "description": "Major IT hub", - "whyRecommended": "Close to tech parks" - }, - "params": { - "city": "Bangalore", - "purpose": "business" - } - }' -``` - -### TypeScript: Full Integration - -```typescript -import { AreaSuggestion, AreaResearchResult, SearchParams } from './types'; - -const API_BASE = 'https://YOUR_PROJECT.supabase.co'; - -// Stage 1: Discover areas via Gemini -async function discoverAreas(params: SearchParams): Promise { - const response = await fetch(`${API_BASE}/functions/v1/discover-areas`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${ANON_KEY}`, - }, - body: JSON.stringify(params), - }); - - const data = await response.json(); - return data.areas; -} - -// Stage 2: Research each area via Mino (SSE) -function researchArea( - area: AreaSuggestion, - params: SearchParams, - onStatus: (update: Partial) => void, - onComplete: (result: AreaResearchResult) => void, - onError: (error: string) => void -): AbortController { - const controller = new AbortController(); - - const fetchStream = async () => { - const response = await fetch(`${API_BASE}/functions/v1/research-area`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${ANON_KEY}`, - }, - body: JSON.stringify({ area, params }), - signal: controller.signal, - }); - - const reader = response.body?.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - 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: ')) continue; - const jsonStr = line.slice(6).trim(); - if (jsonStr === '[DONE]') continue; - - const event = JSON.parse(jsonStr); - - switch (event.type) { - case 'STATUS': - onStatus({ currentAction: event.message }); - break; - case 'SCREENSHOT': - onStatus({ streamingUrl: event.data.streamingUrl }); - break; - case 'COMPLETE': - onComplete({ - areaId: area.id, - areaName: area.name, - status: 'complete', - analysis: event.data.analysis, - }); - break; - case 'ERROR': - onError(event.message); - break; - } - } - } - }; - - fetchStream(); - return controller; -} - -// Usage: Orchestrate the full flow -async function searchHotelAreas(city: string, purpose: string) { - const params = { city, purpose }; - - // Stage 1 - const areas = await discoverAreas(params); - console.log(`Discovered ${areas.length} areas`); - - // Stage 2 (parallel) - const results = await Promise.all( - areas.map(area => - new Promise((resolve) => { - researchArea( - area, - params, - (update) => console.log(`[${area.name}] Status:`, update), - (result) => resolve(result), - (error) => resolve({ areaId: area.id, areaName: area.name, status: 'error', error }) - ); - }) - ) - ); - - return results; -} -``` - -### Python: Mino API Call - -```python -import requests -import json - -MINO_API_URL = "https://agent.tinyfish.ai/v1/automation/run-sse" -TINYFISH_API_KEY = "your-mino-api-key" - -def research_area_with_mino(area_name: str, city: str, purpose: str): - """ - Call Mino API to research a hotel area with browser automation. - Returns a generator that yields SSE events. - """ - - goal = f'''You are researching "{area_name}" in {city} to help a traveler decide if it's a good place to stay. - -TRAVELER'S PURPOSE: {purpose} - -RESEARCH TASKS (do these quickly, ~45 seconds total): - -1. GOOGLE MAPS SEARCH: - - Search for "hotels in {area_name}, {city}" on Google Maps - - Note the general location, nearby landmarks, and transport options - -2. FIND TOP HOTELS: - - Look for 3-5 best rated hotels in this specific area - - Note their names, ratings, and a brief description - -3. CONTEXTUAL ANALYSIS: - Based on what you see, evaluate: - - Is this area suitable for: {purpose}? - - What are the pros of staying here? - - What are the cons or potential issues? - -RETURN JSON ONLY: -{{ - "suitability": "excellent|good|moderate|poor", - "suitabilityScore": 1-10, - "summary": "2-3 sentence summary", - "pros": ["pro1", "pro2"], - "cons": ["con1", "con2"], - "topHotels": [ - {{"name": "Hotel Name", "rating": "4.5", "description": "Brief description"}} - ] -}}''' - - search_url = f"https://www.google.com/maps/search/{area_name}, {city}" - - response = requests.post( - MINO_API_URL, - headers={ - "X-API-Key": TINYFISH_API_KEY, - "Content-Type": "application/json", - }, - json={ - "url": search_url, - "goal": goal, - }, - stream=True - ) - - for line in response.iter_lines(): - if line: - line_str = line.decode('utf-8') - if line_str.startswith('data: '): - json_str = line_str[6:].strip() - if json_str and json_str != '[DONE]': - try: - event = json.loads(json_str) - yield event - except json.JSONDecodeError: - pass - - -# Usage -if __name__ == "__main__": - for event in research_area_with_mino("Whitefield", "Bangalore", "Business trip"): - print(f"Event type: {event.get('type')}") - if event.get('type') == 'COMPLETE': - print(f"Result: {json.dumps(event.get('resultJson'), indent=2)}") -``` - ---- - -## Goal Prompt Reference - -The **exact natural language prompt** sent to Mino for area research: - -``` -You are researching "{AREA_NAME}" in {CITY} to help a traveler decide if it's a good place to stay. - -TRAVELER'S PURPOSE: {PURPOSE_DESCRIPTION} - -RESEARCH TASKS (do these quickly, ~45 seconds total): - -1. GOOGLE MAPS SEARCH: - - Search for "hotels in {AREA_NAME}, {CITY}" on Google Maps - - Note the general location, nearby landmarks, and transport options - - Check distance to key locations relevant to their purpose - -2. FIND TOP HOTELS: - - Look for 3-5 best rated hotels in this specific area - - Note their names, ratings, and a brief description of why they stand out - - Focus on hotels with high ratings (4.0+) and relevant amenities for the traveler's purpose - -3. QUICK REVIEW SCAN: - - Look for any visible ratings or review snippets for hotels in this area - - Note any common themes in reviews (noise, safety, convenience) - -4. CONTEXTUAL ANALYSIS: - Based on what you see, evaluate: - - Is this area suitable for: {PURPOSE_DESCRIPTION}? - - What are the pros of staying here for this purpose? - - What are the cons or potential issues? - - Any risks or things to be aware of? - -RETURN JSON ONLY (no markdown): -{ - "suitability": "excellent|good|moderate|poor", - "suitabilityScore": 1-10, - "summary": "2-3 sentence summary of why this area is/isn't good for their purpose", - "pros": ["pro1", "pro2"], - "cons": ["con1", "con2"], - "risks": ["risk1"], - "distanceToKey": "e.g., 10 min walk to business district", - "walkability": "e.g., Very walkable, good sidewalks", - "noiseLevel": "e.g., Can be noisy at night due to bars", - "safetyNotes": "e.g., Generally safe, well-lit streets", - "nearbyAmenities": ["24h pharmacy", "metro station"], - "reviewHighlights": ["Great breakfast", "Thin walls"], - "topHotels": [ - {"name": "Hotel Name", "rating": "4.5", "description": "Brief description of why this hotel is good for the traveler's purpose"}, - {"name": "Another Hotel", "rating": "4.3", "description": "Short description highlighting key features"} - ] -} -``` - -### Purpose Mappings - -| Purpose Key | Description Sent to Mino | -|-------------|--------------------------| -| `business` | Business trip - meetings, conferences, professional work | -| `exam_interview` | Exam or interview - needs quiet, good sleep, stress-free | -| `family_visit` | Visiting family - comfortable space, family-friendly | -| `sightseeing` | Sightseeing - exploring attractions, good transport | -| `late_night` | Late night schedule - nightlife, flexible timing | -| `airport_transit` | Airport transit - early flight, proximity to airport | - ---- - -## Sample Streaming Output - -The Mino API returns Server-Sent Events (SSE). Here's what a complete stream looks like: - -``` -data: {"type":"STATUS","message":"Starting browser automation..."} - -data: {"streamingUrl":"https://stream.mino.ai/live/abc123"} - -data: {"type":"STATUS","message":"Navigating to Google Maps..."} - -data: {"type":"STATUS","message":"Searching for hotels in Whitefield, Bangalore..."} - -data: {"type":"STATUS","message":"Analyzing hotel listings..."} - -data: {"type":"STATUS","message":"Checking reviews and ratings..."} - -data: {"type":"COMPLETE","resultJson":{"suitability":"excellent","suitabilityScore":8,"summary":"Whitefield is an excellent choice for business travelers. It's home to major IT parks and has numerous business-class hotels with meeting facilities and reliable WiFi.","pros":["Very close to major tech parks (ITPL, Embassy Tech Village)","Excellent selection of business hotels (Marriott, Hyatt, Taj)","Many restaurants and cafes for client meetings","Good metro connectivity coming soon"],"cons":["Traffic congestion during peak hours","Far from airport (1-1.5 hours)","Limited nightlife options"],"risks":["Commute time can be unpredictable"],"distanceToKey":"5-10 min drive to major tech parks","walkability":"Moderate - some areas walkable, others require transport","noiseLevel":"Generally quiet residential-commercial mix","safetyNotes":"Safe area with good security in tech park vicinity","nearbyAmenities":["Phoenix Marketcity Mall","VR Mall","24h restaurants","Metro station (upcoming)"],"reviewHighlights":["Great for business stays","Good breakfast buffets","Reliable WiFi"],"topHotels":[{"name":"Marriott Whitefield","rating":"4.5","description":"Full-service business hotel with meeting rooms and executive lounge, ideal for corporate travelers"},{"name":"Hyatt Centric","rating":"4.4","description":"Modern hotel with excellent co-working spaces and proximity to tech parks"},{"name":"Taj Yeshwantpur","rating":"4.6","description":"Luxury option with premium amenities and professional service"},{"name":"Lemon Tree Premier","rating":"4.2","description":"Good value business hotel with reliable amenities"},{"name":"ibis Bangalore","rating":"4.0","description":"Budget-friendly option with essential business amenities"}]}} - -data: [DONE] -``` - -### SSE Event Types - -| Event Type | Data | Description | -|------------|------|-------------| -| `STATUS` | `{ message: string }` | Progress updates during research | -| `SCREENSHOT` | `{ streamingUrl: string }` | Live browser view URL | -| `COMPLETE` | `{ analysis: object }` | Final research results | -| `ERROR` | `{ message: string }` | Error occurred | - -### Parsed Analysis Object - -```typescript -interface AreaAnalysis { - suitability: 'excellent' | 'good' | 'moderate' | 'poor'; - suitabilityScore: number; // 1-10 - summary: string; - pros: string[]; - cons: string[]; - risks: string[]; - distanceToKey?: string; - walkability?: string; - noiseLevel?: string; - safetyNotes?: string; - nearbyAmenities: string[]; - reviewHighlights: string[]; - topHotels: Array<{ - name: string; - rating?: string; - description: string; - }>; -} -``` - ---- - -## Error Handling - -### Timeout Strategy - -The frontend implements a 180-second (3-minute) timeout for Mino research: - -```typescript -const timeoutId = setTimeout(() => { - if (!completed) { - completed = true; - controller.abort(); - onComplete({ - status: 'complete', - analysis: { - suitability: 'moderate', - summary: 'Research timed out. Consider doing your own research.', - pros: [area.whyRecommended], - cons: ['Limited research data available'], - }, - }); - } -}, 180000); // 3 minutes -``` - -### Fallback Responses - -Both edge functions provide fallback data if the primary API fails: - -**Gemini Fallback (discover-areas):** -- Returns generic area suggestions (City Center, Airport Area, Tourist District) - -**Mino Fallback (research-area):** -- Returns the original area recommendation from Gemini -- Sets suitability to "moderate" with score 5-6 - -### Error Codes - -| HTTP Status | Meaning | Action | -|-------------|---------|--------| -| 400 | Missing required parameters | Check request body | -| 500 | API key not configured | Set environment variables | -| 503 | External API unavailable | Fallback response provided | - ---- - -## Environment Variables - -| Variable | Required | Description | -|----------|----------|-------------| -| `GEMINI_API_KEY` | Yes | Google AI API key for area discovery | -| `TINYFISH_API_KEY` | Yes | Mino API key for browser automation | - ---- - -## Quick Start Checklist - -1. ✅ Set `GEMINI_API_KEY` and `TINYFISH_API_KEY` in your environment -2. ✅ Deploy `discover-areas` and `research-area` edge functions -3. ✅ Implement SSE parsing in your frontend -4. ✅ Handle the 180-second timeout gracefully -5. ✅ Display live screenshots using `streamingUrl` -6. ✅ Parse and render the `topHotels` array - ---- - -*Last updated: January 2025* diff --git a/stay-scout-hub/next.config.ts b/stay-scout-hub/next.config.ts new file mode 100644 index 000000000..cb651cdc0 --- /dev/null +++ b/stay-scout-hub/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/stay-scout-hub/package.json b/stay-scout-hub/package.json index 8eae8578e..62a27a201 100644 --- a/stay-scout-hub/package.json +++ b/stay-scout-hub/package.json @@ -1,91 +1,40 @@ { - "name": "vite_react_shadcn_ts", + "name": "stay-scout-hub", + "version": "0.1.0", "private": true, - "version": "0.0.0", - "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "build:dev": "vite build --mode development", - "lint": "eslint .", - "preview": "vite preview", - "test": "vitest run", - "test:watch": "vitest" + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint ." }, "dependencies": { - "@hookform/resolvers": "^3.10.0", - "@radix-ui/react-accordion": "^1.2.11", - "@radix-ui/react-alert-dialog": "^1.1.14", - "@radix-ui/react-aspect-ratio": "^1.1.7", - "@radix-ui/react-avatar": "^1.1.10", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-context-menu": "^2.2.15", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-hover-card": "^1.1.14", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-menubar": "^1.1.15", - "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-scroll-area": "^1.2.9", - "@radix-ui/react-select": "^2.2.5", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toast": "^1.2.14", - "@radix-ui/react-toggle": "^1.1.9", - "@radix-ui/react-toggle-group": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.7", - "@supabase/supabase-js": "^2.91.0", - "@tanstack/react-query": "^5.83.0", + "@tiny-fish/sdk": "latest", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.1.1", "date-fns": "^3.6.0", - "embla-carousel-react": "^8.6.0", "framer-motion": "^12.27.5", - "input-otp": "^1.4.2", "lucide-react": "^0.462.0", - "next-themes": "^0.3.0", + "next": "15.3.3", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.61.1", - "react-resizable-panels": "^2.1.9", - "react-router-dom": "^6.30.1", - "recharts": "^2.15.4", - "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.9", - "zod": "^3.25.76" + "zod": "^3.25.76", + "@google/generative-ai": "latest" }, "devDependencies": { - "@eslint/js": "^9.32.0", - "@tailwindcss/typography": "^0.5.16", - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.0.0", - "@types/node": "^22.16.5", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react-swc": "^3.11.0", + "@types/node": "^22", + "@types/react": "^18", + "@types/react-dom": "^18", "autoprefixer": "^10.4.21", - "eslint": "^9.32.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.15.0", - "jsdom": "^20.0.3", - "lovable-tagger": "^1.1.13", + "eslint": "^9", + "eslint-config-next": "15.3.3", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", - "typescript": "^5.8.3", - "typescript-eslint": "^8.38.0", - "vite": "^5.4.19", - "vitest": "^3.2.4" + "tailwindcss-animate": "^1.0.7", + "typescript": "^5" } -} +} \ No newline at end of file diff --git a/stay-scout-hub/postcss.config.mjs b/stay-scout-hub/postcss.config.mjs new file mode 100644 index 000000000..81a249726 --- /dev/null +++ b/stay-scout-hub/postcss.config.mjs @@ -0,0 +1,4 @@ +const config = { + plugins: { tailwindcss: {}, autoprefixer: {} }, +}; +export default config; diff --git a/stay-scout-hub/public/favicon.ico b/stay-scout-hub/public/favicon.ico deleted file mode 100644 index 3c01d6971..000000000 Binary files a/stay-scout-hub/public/favicon.ico and /dev/null differ diff --git a/stay-scout-hub/public/placeholder.svg b/stay-scout-hub/public/placeholder.svg deleted file mode 100644 index e763910b2..000000000 --- a/stay-scout-hub/public/placeholder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/stay-scout-hub/public/robots.txt b/stay-scout-hub/public/robots.txt deleted file mode 100644 index 6018e701f..000000000 --- a/stay-scout-hub/public/robots.txt +++ /dev/null @@ -1,14 +0,0 @@ -User-agent: Googlebot -Allow: / - -User-agent: Bingbot -Allow: / - -User-agent: Twitterbot -Allow: / - -User-agent: facebookexternalhit -Allow: / - -User-agent: * -Allow: / diff --git a/stay-scout-hub/src/App.tsx b/stay-scout-hub/src/App.tsx deleted file mode 100644 index 18daf2e90..000000000 --- a/stay-scout-hub/src/App.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Toaster } from "@/components/ui/toaster"; -import { Toaster as Sonner } from "@/components/ui/sonner"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; -import Index from "./pages/Index"; -import NotFound from "./pages/NotFound"; - -const queryClient = new QueryClient(); - -const App = () => ( - - - - - - - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - - - -); - -export default App; diff --git a/stay-scout-hub/src/app/api/discover-areas/route.ts b/stay-scout-hub/src/app/api/discover-areas/route.ts new file mode 100644 index 000000000..f5448ed55 --- /dev/null +++ b/stay-scout-hub/src/app/api/discover-areas/route.ts @@ -0,0 +1,110 @@ +export const runtime = "nodejs"; +export const maxDuration = 60; + +import { TinyFish } from "@tiny-fish/sdk"; +import { GoogleGenerativeAI } from "@google/generative-ai"; + +function getPurposeDescription(purpose: string, customPurpose?: string): string { + if (customPurpose) return customPurpose; + const purposes: Record = { + business: "Business trip - meetings, conferences, or professional work", + exam_interview: "Exam or interview preparation - needs quiet, good sleep, stress-free environment", + family_visit: "Visiting family - needs comfortable space, family-friendly area", + sightseeing: "Sightseeing and tourism - exploring attractions, good transport access", + late_night: "Late night schedule - nightlife, late check-in, flexible timing", + airport_transit: "Airport transit - early flight, layover, needs proximity to airport", + }; + return purposes[purpose] || "General travel"; +} + +function generateFallbackAreas(city: string, purpose: string) { + return [ + { id: "city-center", name: `${city} City Center`, type: "neighborhood", description: "The central business and commercial district", whyRecommended: "Central location with easy access to transport, restaurants, and main attractions", keyLocations: ["Main Train Station", "Central Business District"] }, + { id: "near-airport", name: `${city} Airport Area`, type: "area", description: "Hotels near the main airport", whyRecommended: "Convenient for early flights or late arrivals, shuttle services available", keyLocations: ["International Airport", "Airport Express"] }, + { id: "tourist-district", name: `${city} Tourist District`, type: "neighborhood", description: "Popular area for visitors with attractions nearby", whyRecommended: "Walking distance to major attractions, lots of dining and entertainment options", keyLocations: ["Major Attractions", "Shopping District"] }, + { id: "business-hub", name: `${city} Business Hub`, type: "neighborhood", description: "Corporate offices and convention centers area", whyRecommended: "Ideal for business travelers with proximity to offices and meeting venues", keyLocations: ["Convention Center", "Financial District"] }, + { id: "residential-quiet", name: `${city} Quiet Residential Area`, type: "neighborhood", description: "Peaceful residential neighborhood away from the bustle", whyRecommended: "Perfect for those seeking quiet, restful stays with local charm", keyLocations: ["Local Parks", "Residential Streets"] }, + ]; +} + +export async function POST(request: Request) { + const { city, purpose, customPurpose, checkIn, checkOut } = await request.json(); + + if (!city) return Response.json({ error: "City is required" }, { status: 400 }); + + const apiKey = process.env.TINYFISH_API_KEY; + if (!apiKey) return Response.json({ error: "Missing TINYFISH_API_KEY" }, { status: 500 }); + + const purposeDescription = getPurposeDescription(purpose, customPurpose); + + try { + const client = new TinyFish({ apiKey }); + + // Step 1 — Search for real travel guides about neighborhoods in this city + const searchQuery = `best neighborhoods to stay in ${city} ${purpose === "business" ? "for business travelers" : "travel guide"} ${checkIn ? new Date(checkIn).getFullYear() : ""}`.trim(); + const searchResults = await client.search.query({ query: searchQuery }); + + const urls = (searchResults.results || []) + .map((r: { url: string }) => r.url) + .slice(0, 4); + + if (!urls.length) { + return Response.json({ areas: generateFallbackAreas(city, purpose) }); + } + + // Step 2 — Fetch content from the travel guides + const fetchResult = await client.fetch.getContents({ urls, format: "markdown" }); + const pagesContent = (fetchResult.results || []) + .map((r) => r.text || "") + .filter(Boolean) + .join("\n\n---\n\n") + .slice(0, 6000); + + if (!pagesContent) { + return Response.json({ areas: generateFallbackAreas(city, purpose) }); + } + + // Step 3 — Gemini extracts structured neighborhood recommendations + const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); + const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" }); + + const prompt = `You are an expert travel advisor. Based ONLY on the travel guide content below, extract 5-8 specific neighborhood or area recommendations for someone staying in ${city}. + +TRAVELER'S PURPOSE: ${purposeDescription} +${checkIn ? `Check-in: ${checkIn}` : ""} +${checkOut ? `Check-out: ${checkOut}` : ""} + +TRAVEL GUIDE CONTENT: +${pagesContent} + +Extract real neighborhoods mentioned in the content. For each, explain why it suits the traveler's purpose. + +Return ONLY valid JSON — no markdown, no code blocks: +[ + { + "id": "unique-area-id", + "name": "Area/Neighborhood Name", + "type": "neighborhood", + "description": "Brief description of this area", + "whyRecommended": "Why this area suits their specific purpose", + "keyLocations": ["Nearby landmark 1", "Nearby landmark 2"] + } +]`; + + const result = await model.generateContent(prompt); + const text = result.response.text() || ""; + + let areas = []; + try { + const jsonMatch = text.replace(/```json|```/g, "").trim().match(/\[[\s\S]*\]/); + if (jsonMatch) areas = JSON.parse(jsonMatch[0]); + else throw new Error("No JSON array found"); + } catch { + areas = generateFallbackAreas(city, purpose); + } + + return Response.json({ areas }); + } catch { + return Response.json({ areas: generateFallbackAreas(city, purpose) }); + } +} diff --git a/stay-scout-hub/src/app/api/research-area/route.ts b/stay-scout-hub/src/app/api/research-area/route.ts new file mode 100644 index 000000000..be3ac00d1 --- /dev/null +++ b/stay-scout-hub/src/app/api/research-area/route.ts @@ -0,0 +1,160 @@ +export const runtime = "nodejs"; +export const maxDuration = 300; + +import { TinyFish, EventType, RunStatus } from "@tiny-fish/sdk"; + +const sseData = (payload: unknown) => `data: ${JSON.stringify(payload)}\n\n`; + +function getPurposeDescription(purpose: string): string { + const purposes: Record = { + business: "Business trip - meetings, conferences, professional work", + exam_interview: "Exam or interview - needs quiet, good sleep, stress-free", + family_visit: "Visiting family - comfortable space, family-friendly", + sightseeing: "Sightseeing - exploring attractions, good transport", + late_night: "Late night schedule - nightlife, flexible timing", + airport_transit: "Airport transit - early flight, proximity to airport", + }; + return purposes[purpose] || "General travel"; +} + +function parseResearchResult(result: Record, area: { name: string; whyRecommended: string }, city: string) { + const topHotels = Array.isArray(result.topHotels) + ? (result.topHotels as Record[]).slice(0, 5).map(h => ({ + name: String(h.name || "Unknown Hotel"), + rating: h.rating ? String(h.rating) : undefined, + description: String(h.description || "A well-rated hotel in this area."), + })) + : []; + + return { + suitability: String(result.suitability || "moderate"), + suitabilityScore: Number(result.suitabilityScore || 5), + summary: String(result.summary || `${area.name} is a potential option for your stay in ${city}.`), + pros: Array.isArray(result.pros) ? result.pros : [area.whyRecommended], + cons: Array.isArray(result.cons) ? result.cons : [], + risks: Array.isArray(result.risks) ? result.risks : [], + distanceToKey: result.distanceToKey ? String(result.distanceToKey) : undefined, + walkability: result.walkability ? String(result.walkability) : undefined, + noiseLevel: result.noiseLevel ? String(result.noiseLevel) : undefined, + safetyNotes: result.safetyNotes ? String(result.safetyNotes) : undefined, + nearbyAmenities: Array.isArray(result.nearbyAmenities) ? result.nearbyAmenities : [], + reviewHighlights: Array.isArray(result.reviewHighlights) ? result.reviewHighlights : [], + topHotels, + }; +} + +function generateFallbackAnalysis(area: { name: string; whyRecommended: string; keyLocations?: string[] }, city: string) { + return { + suitability: "good", + suitabilityScore: 6, + summary: `${area.name} is a commonly recommended area in ${city}. ${area.whyRecommended || "Good central location with various amenities."}`, + pros: [area.whyRecommended || "Convenient location"], + cons: ["Limited detailed research available"], + risks: [], + nearbyAmenities: area.keyLocations || [], + reviewHighlights: [], + topHotels: [], + }; +} + +export async function POST(request: Request) { + const { area, params } = await request.json(); + + if (!area || !params) return Response.json({ error: "Area and params are required" }, { status: 400 }); + + const apiKey = process.env.TINYFISH_API_KEY; + if (!apiKey) return Response.json({ error: "Missing TINYFISH_API_KEY" }, { status: 500 }); + + const { city, purpose, customPurpose } = params; + const purposeText = customPurpose || getPurposeDescription(purpose); + + const goal = `You are researching "${area.name}" in ${city} to help a traveler decide if it's a good place to stay. + +TRAVELER'S PURPOSE: ${purposeText} + +RESEARCH TASKS (do these quickly, ~45 seconds total): + +1. GOOGLE MAPS SEARCH: + - Search for "hotels in ${area.name}, ${city}" on Google Maps + - Note the general location, nearby landmarks, and transport options + - Check distance to key locations relevant to their purpose + +2. FIND TOP HOTELS: + - Look for 3-5 best rated hotels in this specific area + - Note their names, ratings, and a brief description + - Focus on hotels with high ratings (4.0+) and relevant amenities + +3. CONTEXTUAL ANALYSIS: + Based on what you see, evaluate: + - Is this area suitable for: ${purposeText}? + - What are the pros of staying here for this purpose? + - What are the cons or potential issues? + +RETURN JSON ONLY (no markdown): +{ + "suitability": "excellent|good|moderate|poor", + "suitabilityScore": 1-10, + "summary": "2-3 sentence summary of why this area is/isn't good for their purpose", + "pros": ["pro1", "pro2"], + "cons": ["con1", "con2"], + "risks": ["risk1"], + "distanceToKey": "e.g., 10 min walk to business district", + "walkability": "e.g., Very walkable, good sidewalks", + "noiseLevel": "e.g., Can be noisy at night due to bars", + "safetyNotes": "e.g., Generally safe, well-lit streets", + "nearbyAmenities": ["24h pharmacy", "metro station"], + "reviewHighlights": ["Great breakfast", "Thin walls"], + "topHotels": [ + {"name": "Hotel Name", "rating": "4.5", "description": "Brief description"}, + {"name": "Another Hotel", "rating": "4.3", "description": "Short description"} + ] +}`; + + const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(area.name + ", " + city)}`; + + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder(); + const enqueue = (payload: unknown) => controller.enqueue(encoder.encode(sseData(payload))); + + try { + enqueue({ type: "CONNECTED", message: `Starting research on ${area.name}...` }); + + const client = new TinyFish({ apiKey }); + const tfStream = await client.agent.stream({ url: searchUrl, goal }); + + for await (const event of tfStream) { + if (event.type === EventType.STREAMING_URL) { + enqueue({ type: "SCREENSHOT", data: { streamingUrl: event.streaming_url } }); + } + + if (event.type === EventType.COMPLETE && event.status === RunStatus.COMPLETED) { + const raw = typeof event.result === "string" ? JSON.parse(event.result) : event.result; + const analysis = parseResearchResult(raw as Record, area, city); + enqueue({ type: "COMPLETE", data: { analysis } }); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + controller.close(); + return; + } + } + + // Fallback if no COMPLETE event + enqueue({ type: "COMPLETE", data: { analysis: generateFallbackAnalysis(area, city) } }); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + controller.close(); + } catch (error) { + enqueue({ type: "ERROR", message: error instanceof Error ? error.message : "Unknown error" }); + controller.close(); + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + }); +} diff --git a/stay-scout-hub/src/app/globals.css b/stay-scout-hub/src/app/globals.css new file mode 100644 index 000000000..63ae32e19 --- /dev/null +++ b/stay-scout-hub/src/app/globals.css @@ -0,0 +1,52 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Blue/teal theme matching original */ + --background: 210 40% 98%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 199 89% 48%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 199 89% 48%; + --accent-foreground: 0 0% 100%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 199 89% 48%; + --radius: 0.75rem; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +@layer utilities { + .text-gradient { + @apply bg-gradient-to-r from-cyan-500 to-blue-600 bg-clip-text text-transparent; + } + + .bg-gradient-hero { + background: linear-gradient(135deg, hsl(199, 89%, 48%), hsl(217, 91%, 60%)); + } + + .shadow-glow { + box-shadow: 0 0 20px hsl(199 89% 48% / 0.3); + } +} diff --git a/stay-scout-hub/src/app/layout.tsx b/stay-scout-hub/src/app/layout.tsx new file mode 100644 index 000000000..a3d4cd943 --- /dev/null +++ b/stay-scout-hub/src/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Stay Scout Hub", + description: "Find the perfect area and hotel for your trip", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/stay-scout-hub/src/pages/Index.tsx b/stay-scout-hub/src/app/page.tsx similarity index 69% rename from stay-scout-hub/src/pages/Index.tsx rename to stay-scout-hub/src/app/page.tsx index e92ea15e8..a259bec84 100644 --- a/stay-scout-hub/src/pages/Index.tsx +++ b/stay-scout-hub/src/app/page.tsx @@ -1,3 +1,5 @@ +'use client'; + import { SearchFormV2 } from '@/components/SearchFormV2'; import { AreaResultsSection } from '@/components/AreaResultsSection'; import { useAreaSearch } from '@/hooks/useAreaSearch'; @@ -6,22 +8,23 @@ import { motion } from 'framer-motion'; import { TRIP_PURPOSES } from '@/types/hotel'; import { useState } from 'react'; -const Index = () => { +export default function Home() { const { search, isSearching, results, error } = useAreaSearch(); - const [searchContext, setSearchContext] = useState<{ - city?: string; + const [searchContext, setSearchContext] = useState<{ + city?: string; purpose?: string; checkIn?: string; checkOut?: string; }>({}); const handleSearch = (params: Parameters[0]) => { - const purposeLabel = params.purpose === 'custom' - ? params.customPurpose - : TRIP_PURPOSES.find(p => p.id === params.purpose)?.label; - - setSearchContext({ - city: params.city, + const purposeLabel = + params.purpose === 'custom' + ? params.customPurpose + : TRIP_PURPOSES.find((p) => p.id === params.purpose)?.label; + + setSearchContext({ + city: params.city, purpose: purposeLabel, checkIn: params.checkIn, checkOut: params.checkOut, @@ -30,20 +33,20 @@ const Index = () => { }; return ( -
+
{/* Header */} -
+
- +
- StayScout -

AI-Powered Location Intelligence

+ StayScout +

AI-Powered Location Intelligence

-
+
Pre-Booking Decision Engine
@@ -51,7 +54,7 @@ const Index = () => { {/* Main Content */}
- {/* Hero Section */} + {/* Hero */} { Not a booking site — a decision-making tool - -

+ +

Where should you stay?

- -

- Tell us your trip purpose, and AI agents will research neighborhoods, - analyze reviews, and explain why each - area is or isn't right for you. + +

+ Tell us your trip purpose, and AI agents will research neighborhoods, + analyze reviews, and explain{' '} + why each area is + or isn't right for you.

@@ -94,7 +98,7 @@ const Index = () => {

{step.title}

-

{step.desc}

+

{step.desc}

))} @@ -102,14 +106,14 @@ const Index = () => { {error && ( -
+
{error}
)} - { {/* Footer */} -