# Project export: HomeEase

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: HomeEase helps you renovate, repair, and redesign your home effortlessly using AI — from DIY guides to local service recommendations, all in one platform.
- Devpost: https://devpost.com/software/homeease
- GitHub: https://github.com/Preetam3620/HomeEase.git
- Video: https://www.youtube.com/embed/eVTpOdrcrg8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Preetam3620 (1 commits)

## Devpost submission (written by the team)

### Overview

Home maintenance and decor often require navigating multiple apps — one for plumbing, another for interior design, and yet another for product shopping. We wanted to create a one-stop, intelligent home platform that helps users solve any home-related problem — whether it’s fixing a broken faucet, repainting a wall, or visualizing a new decor idea — all powered by AI automation and smart agents. Our goal was to make home management as easy as asking a question — speak, type, or upload an image, and let HomeEase handle the rest. HomeEase provides AI-driven assistance for decor, renovation, and home maintenance. Users interact through voice, text, or images, and the system intelligently creates a job request. They can choose between two main paths: DIY Mode — The Fetch.ai agent analyzes the request and generates: A step-by-step repair or renovation plan A list of required tools and products Amazon links (scraped via Bright Data) for easy purchase Service Mode — Another Fetch.ai agent locates nearby service providers for the job. For home decor, users upload an image of their room. The system suggests products, scrapes Amazon, and then uses another Fetch.ai image-generation agent to merge the product visuals into the original image, helping the user visualize how the decor would look. Frontend: React with TailwindCSS for a clean, responsive user experience Backend: FastAPI, python Voice Processing: LiveKit for real-time voice input and transcription AI Agents: Multiple Fetch.ai agents for: Generating DIY steps and tool lists Finding local service providers Combining decor images with product visuals Web Scraping: Bright Data to scrape Amazon for recommended products Integration: Each agent’s output flows into the next stage, forming an autonomous task pipeline Image Processing: A generative model integrates the decor products into the uploaded image Agent Coordination: Ensuring smooth data flow between multiple Fetch.ai agents with asynchronous tasks. Voice-to-Text Accuracy: Maintaining reliable speech recognition and transcript interpretation with LiveKit. Web Scraping Reliability: Handling rate limits and dynamic pages while scraping Amazon product listings. Image Blending: Combining input images with product visuals while maintaining realistic composition. Context Management: Preserving job context across DIY and Service modes without user re-entry. Built an end-to-end AI workflow combining voice, image, and agent-based automation. Successfully integrated LiveKit, Fetch.ai, and Bright Data within a single project pipeline. Achieved realistic decor visualization that gives users a tangible preview of their design ideas. Enabled a seamless transition from AI-driven DIY to local professional assistance. How to coordinate multiple autonomous AI agents for distinct subtasks while maintaining state consistency. Best practices for real-time voice input handling and integrating third-party APIs efficiently. The importance of user experience in AI-driven apps — clear feedback and interactivity make all the difference. Ethical and practical considerations when using web scraping and AI-generated imagery. Integrate budget estimations and time-to-complete predictions. Add payment and scheduling features for service bookings. Enable multi-agent collaboration for larger renovation projects. Include personalized style recommendations using computer vision. Launch a mobile version of HomeEase for on-the-go interactions.

## README (from the GitHub repository)

# HomeEase

A modern home services platform connecting users with service providers through an intelligent voice-enabled interface.

## Features

- **User Dashboard**: Browse, book, and manage home services
- **Provider Dashboard**: Manage services, bookings, and availability
- **Voice Assistant**: Real-time voice interaction powered by LiveKit
- **Authentication**: Secure user authentication with role-based access
- **Real-time Communication**: Live transcription and voice chat

## Tech Stack

### Frontend
- React 18 + TypeScript
- Vite
- Shadcn UI + Radix UI components
- TailwindCSS
- React Router
- TanStack Query
- LiveKit Components
- Supabase

### Voice Agent
- LiveKit Agents
- Deepgram STT (Speech-to-Text)
- Python

## Project Structure

```
HomeEase/
├── frontend/          # React frontend application
├── livekit-agent/     # Voice assistant agent
└── fetch-agents/      # Backend agents
```

## Getting Started

### Frontend

```bash
cd frontend
npm install
npm run dev
```

### LiveKit Agent

```bash
cd livekit-agent
# Create .env.local with your LiveKit and Deepgram credentials
python agent.py
```

## Environment Variables

Create appropriate `.env` or `.env.local` files with:
- LiveKit API credentials
- Deepgram API key
- Supabase credentials

## License

Private project


## Detected evidence (automated analysis)

Indexed codebase: 127 recognized source files, 527 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (120 of 137)

```
.gitignore
brightdata/ai_agent_client.py
brightdata/brightdata_service.py
brightdata/config.py
brightdata/main.py
brightdata/mock_service.py
brightdata/models.py
brightdata/product_filter.py
fetch-agents/anthropic_agent.py
fetch-agents/client.py
fetch-agents/config.py
fetch-agents/hs_agent.py
fetch-agents/hs_client.py
fetch-agents/hs_model.py
fetch-agents/main.py
fetch-agents/tutorial_agent.py
fetch-agents/yelp_client.py
frontend/.gitignore
frontend/bun.lockb
frontend/components.json
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/public/robots.txt
frontend/README.md
frontend/src/App.css
frontend/src/App.tsx
frontend/src/components/AnimatedCounter.tsx
frontend/src/components/GoogleMapsLoader.tsx
frontend/src/components/InfiniteScroll.tsx
frontend/src/components/layouts/ProviderLayout.tsx
frontend/src/components/layouts/UserLayout.tsx
frontend/src/components/ProtectedRoute.tsx
frontend/src/components/provider/JobsView.tsx
frontend/src/components/provider/OffersView.tsx
frontend/src/components/provider/ProfileView.tsx
frontend/src/components/ScrollCardStack.tsx
frontend/src/components/shared/DateTimePicker.tsx
frontend/src/components/shared/DispatchProgress.tsx
frontend/src/components/shared/LocationPicker.tsx
frontend/src/components/shared/ReviewForm.tsx
frontend/src/components/ui/accordion.tsx
frontend/src/components/ui/alert-dialog.tsx
frontend/src/components/ui/alert.tsx
frontend/src/components/ui/aspect-ratio.tsx
frontend/src/components/ui/avatar.tsx
frontend/src/components/ui/badge.tsx
frontend/src/components/ui/breadcrumb.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/calendar.tsx
frontend/src/components/ui/card.tsx
frontend/src/components/ui/carousel.tsx
frontend/src/components/ui/chart.tsx
frontend/src/components/ui/checkbox.tsx
frontend/src/components/ui/collapsible.tsx
frontend/src/components/ui/command.tsx
frontend/src/components/ui/context-menu.tsx
frontend/src/components/ui/dialog.tsx
frontend/src/components/ui/drawer.tsx
frontend/src/components/ui/dropdown-menu.tsx
frontend/src/components/ui/form.tsx
frontend/src/components/ui/hover-card.tsx
frontend/src/components/ui/input-otp.tsx
frontend/src/components/ui/input.tsx
frontend/src/components/ui/label.tsx
frontend/src/components/ui/menubar.tsx
frontend/src/components/ui/navigation-menu.tsx
frontend/src/components/ui/pagination.tsx
frontend/src/components/ui/popover.tsx
frontend/src/components/ui/progress.tsx
frontend/src/components/ui/radio-group.tsx
frontend/src/components/ui/resizable.tsx
frontend/src/components/ui/scroll-area.tsx
frontend/src/components/ui/select.tsx
frontend/src/components/ui/separator.tsx
frontend/src/components/ui/sheet.tsx
frontend/src/components/ui/sidebar.tsx
frontend/src/components/ui/skeleton.tsx
frontend/src/components/ui/slider.tsx
frontend/src/components/ui/sonner.tsx
frontend/src/components/ui/switch.tsx
frontend/src/components/ui/table.tsx
frontend/src/components/ui/tabs.tsx
frontend/src/components/ui/textarea.tsx
frontend/src/components/ui/toast.tsx
frontend/src/components/ui/toaster.tsx
frontend/src/components/ui/toggle-group.tsx
frontend/src/components/ui/toggle.tsx
frontend/src/components/ui/tooltip.tsx
frontend/src/components/ui/use-toast.ts
frontend/src/components/user/AddressManager.tsx
frontend/src/components/user/CreateJobView.tsx
frontend/src/components/user/ImageUpload.tsx
frontend/src/components/user/JobDetailView.tsx
frontend/src/components/user/JobsView.tsx
frontend/src/components/user/ProfileForm.tsx
frontend/src/components/user/VoiceRecorder.tsx
frontend/src/components/VoiceCommand.tsx
frontend/src/contexts/AuthContext.tsx
frontend/src/hooks/use-mobile.tsx
frontend/src/hooks/use-toast.ts
frontend/src/index.css
frontend/src/integrations/supabase/client.ts
frontend/src/integrations/supabase/types.ts
frontend/src/main.tsx
frontend/src/pages/auth/SignIn.tsx
frontend/src/pages/auth/SignUp.tsx
frontend/src/pages/Index.tsx
frontend/src/pages/Landing.tsx
frontend/src/pages/NotFound.tsx
frontend/src/pages/provider/Dashboard.tsx
frontend/src/pages/user/Dashboard.tsx
frontend/src/pages/user/Profile.tsx
frontend/src/types/index.ts
frontend/src/vite-env.d.ts
frontend/supabase/config.toml
frontend/supabase/functions/create-job/index.ts
frontend/supabase/functions/fetch-nearby-stores/index.ts
frontend/supabase/functions/generate-diy-plan/index.ts
[17 more files omitted for size]
```

### Dependencies

- frontend/package.json: @eslint/js@^9.32.0, @hookform/resolvers@^3.10.0, @livekit/components-react@^2.9.15, @livekit/components-styles@^1.1.6, @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, @react-google-maps/api@^2.20.7, @supabase/supabase-js@^2.76.1, @tailwindcss/typography@^0.5.16, @tanstack/react-query@^5.83.0, @types/node@^22.16.5, @types/react@^18.3.23, @types/react-dom@^18.3.7, @vitejs/plugin-react-swc@^3.11.0, autoprefixer@^10.4.21, 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, eslint@^9.32.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.20, framer-motion@^12.23.24, globals@^15.15.0, input-otp@^1.4.2, livekit-client@^2.15.13, lovable-tagger@^1.1.11, lucide-react@^0.462.0, next-themes@^0.3.0, postcss@^8.5.6, 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@^3.4.17, tailwindcss-animate@^1.0.7, typescript@^5.8.3, typescript-eslint@^8.38.0, vaul@^0.9.9, vite@^5.4.19, zod@^3.25.76

### Recent commits (newest first)

- Added DIY process agent
- Brightdata amazon scrape api
- initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### frontend/package.json

```
{
  "name": "vite_react_shadcn_ts",
  "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"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.10.0",
    "@livekit/components-react": "^2.9.15",
    "@livekit/components-styles": "^1.1.6",
    "@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",
    "@react-google-maps/api": "^2.20.7",
    "@supabase/supabase-js": "^2.76.1",
    "@tanstack/react-query": "^5.83.0",
    "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.23.24",
    "input-otp": "^1.4.2",
    "livekit-client": "^2.15.13",
    "lucide-react": "^0.462.0",
    "next-themes": "^0.3.0",
    "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"
  },
  "devDependencies": {
    "@eslint/js": "^9.32.0",
    "@tailwindcss/typography": "^0.5.16",
    "@types/node": "^22.16.5",
    "@types/react": "^18.3.23",
    "@types/react-dom": "^18.3.7",
    "@vitejs/plugin-react-swc": "^3.11.0",
    "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",
    "lovable-tagger": "^1.1.11",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.17",
    "typescript": "^5.8.3",
    "typescript-eslint": "^8.38.0",
    "vite": "^5.4.19"
  }
}

```

### fetch-agents/main.py

```python
# main.py
"""
Store Finder uAgent - Backend Worker (no chat)
"""

from __future__ import annotations
import os, re
from typing import Optional, List, Tuple
from datetime import datetime
from urllib.parse import quote

# env + macOS SSL
try:
    from dotenv import load_dotenv
    load_dotenv()
except Exception:
    pass
try:
    import certifi
    os.environ.setdefault("SSL_CERT_FILE", certifi.where())
except Exception:
    pass

from uagents import Agent, Context, Protocol, Model
from uagents.setup import fund_agent_if_low

from yelp_client import YelpAPI, HardwareStore
from anthropic_agent import AnthropicAgent
from config import Config

# ---------------- Models ----------------
class StoreSearchRequest(Model):
    location: str
    requirements: Optional[List[str]] = None
    radius: Optional[int] = 10000
    limit: Optional[int] = 20

class StoreSearchResponse(Model):
    success: bool
    stores_found: int
    stores: List[dict]
    ai_analysis: str
    error_message: Optional[str] = None
    timestamp: str

# (requested) robust parser — import/use from client if needed
def parse_query(user_input: str) -> Tuple[str, List[str]]:
    """
    Extract 'near/in/at/around <location>' safely. Stops at newline / ' for ' / ':'.
    Returns (location, requirement_keywords_found).
    """
    m = re.search(r"(?:near|in|around|at)\s+(.+?)\s*(?:\n| for |:|$)", user_input, re.IGNORECASE)
    location = (m.group(1).strip() if m else "")
    if len(location) > 80:
        location = location[:80].rsplit(" ", 1)[0]

    reqs: List[str] = []
    low = user_input.lower()
    for k in ["tools","screws","nails","paint","lumber","electrical","plumbing","hardware",
              "supplies","equipment","drill","drill bits"]:
        if k in low and k not in reqs:
            reqs.append(k)
    return location, reqs

# ---------------- Protocol ----------------
store_proto = Protocol(name="store_finder_protocol", version="1.0")

# ---------------- Core ----------------
class StoreFinderCore:
    def __init__(self):
        self.config = Config()
        if not self.config.validate():
            raise ValueError("Invalid configuration. Check your API keys.")
        self.yelp = YelpAPI(self.config.get_yelp_api_key())
        self.claude = AnthropicAgent(self.config.get_anthropic_api_key())

    def search(self, req: StoreSearchRequest) -> StoreSearchResponse:
        now = datetime.now().isoformat()
        try:
            results: List[HardwareStore] = self.yelp.search_hardware_stores(
                location=req.location, radius=req.radius, limit=req.limit
            )
            if not results:
                return StoreSearchResponse(
                    success=False, stores_found=0, stores=[],
                    ai_analysis="No hardware stores found in the specified area.",
                    error_message="No results found", timestamp=now,
                )

            ai = self.claude.process_hardware_store_query(
                location=req.location,
                requirements=req.requirements or [],
                hardware_stores=results,
            )

            top: List[dict] = []
            for s in results[:5]:
                top.append({
                    "name": s.name, "address": s.address, "city": s.city, "state": s.state,
                    "zip_code": s.zip_code, "phone": s.phone, "rating": s.rating,
                    "review_count": s.review_count, "distance": s.distance, "url": s.url,
                    "categories": s.categories,
                    "google_maps": f"https://www.google.com/maps/search/?api=1&query={s.name.replace(' ','+')}+{s.address.replace(' ','+')}",
                })

            return StoreSearchResponse(
                success=True, stores_found=len(results), stores=top,
                ai_analysis=ai, timestamp=now,
            )

        except Exception as e:
            msg = str(e)
            if "400" in msg:
                msg = "The location wasn’t valid for Yelp. Try a city/state or full address."
            return StoreSearchResponse(
                success=False, stores_found=0, stores=[], ai_analysis="",
                error_message=msg, timestamp=now,
            )

core = StoreFinderCore()

# ---------------- Handlers ----------------
@store_proto.on_message(model=StoreSearchRequest, replies=StoreSearchResponse)
async def handle_store_search(ctx: Context, sender: str, msg: StoreSearchRequest):
    ctx.logger.info(f"[STORE] request from {sender} | location='{msg.location}' | reqs={msg.requirements}")
    resp = core.search(msg)
    await ctx.send(sender, resp)
    ctx.logger.info("[STORE] response sent")

# ---------------- Agent ----------------
PORT_BACKEND = int(os.getenv("PORT_BACKEND", "8010"))
LOCAL_BASE = os.getenv("LOCAL_BASE_BACKEND", f"http://127.0.0.1:{PORT_BACKEND}")

agent = Agent(
    name=os.getenv("BACKEND_NAME", "store_finder_backend"),
    seed=os.getenv("BACKEND_SEED", "store-finder-backend-seed"),
    port=PORT_BACKEND,
    endpoint=[f"{LOCAL_BASE}/submit"],
    mailbox=True,
)

fund_agent_if_low(agent.wallet.address())
agent.include(store_proto, publish_manifest=True)

@agent.on_event("startup")
async def _startup(ctx: Context):
    ctx.logger.info("🏪 Store Finder Backend started")
    ctx.logger.info(f"Backend address: {agent.address}")
    ctx.logger.info(f"Local endpoint: {LOCAL_BASE}/submit")
    ctx.logger.info(
        "Inspector: https://agentverse.ai/inspect/?uri=%s&address=%s",
        quote(LOCAL_BASE, safe=""),
        agent.address,
    )

if __name__ == "__main__":
    print("Backend address:", agent.address)
    enc = quote(LOCAL_BASE, safe="")
    print("Inspector:", f"https://agentverse.ai/inspect/?uri={enc}&address={agent.address}")
    agent.run()

```

### brightdata/main.py

```python
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import logging
from datetime import datetime
from typing import List

from models import ScrapeRequest, ScrapeResponse, ErrorResponse, Product
from brightdata_service import BrightDataService
from mock_service import MockDataService
from product_filter import ProductFilterService
from config import settings

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Initialize FastAPI app
app = FastAPI(
    title="Amazon Product Scraper API",
    description="API for scraping Amazon products using Bright Data",
    version="1.0.0"
)

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Configure this properly for production
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Initialize services
brightdata_service = BrightDataService()
mock_service = MockDataService()
filter_service = ProductFilterService()

@app.get("/")
async def root():
    """Health check endpoint"""
    return {
        "message": "Amazon Product Scraper API is running",
        "version": "1.0.0",
        "timestamp": datetime.now()
    }

@app.get("/health")
async def health_check():
    """Detailed health check"""
    return {
        "status": "healthy",
        "api_key_configured": bool(settings.BRIGHTDATA_API_KEY),
        "timestamp": datetime.now()
    }

@app.post("/scrape", response_model=ScrapeResponse)
async def scrape_products(request: ScrapeRequest):
    """
    Scrape Amazon products based on keywords with filtering options
    """
    try:
        logger.info(f"Received scrape request for keywords: {request.keywords}")
        
        # Validate input
        if not request.keywords:
            raise HTTPException(status_code=400, detail="Keywords list cannot be empty")
        
        if len(request.keywords) > 10:
            raise HTTPException(status_code=400, detail="Maximum 10 keywords allowed per request")
        
        # Use request parameters or defaults
        min_rating = request.min_rating or settings.MIN_RATING
        max_price = request.max_price or settings.MAX_PRICE
        limit = request.limit or settings.TOP_PRODUCTS_LIMIT
        
        # Validate parameters
        if min_rating < 0 or min_rating > 5:
            raise HTTPException(status_code=400, detail="Rating must be between 0 and 5")
        
        if max_price <= 0:
            raise HTTPException(status_code=400, detail="Max price must be greater than 0")
        
        if limit <= 0 or limit > 50:
            raise HTTPException(status_code=400, detail="Limit must be between 1 and 50")
        
        # Step 1: Search products using Bright Data API (with fallback to mock data)
        try:
            raw_data = await brightdata_service.search_products(request.keywords)
            
            # Check if we got a snapshot_id (async job)
            if isinstance(raw_data, dict) and 'snapshot_id' in raw_data:
                snapshot_id = raw_data['snapshot_id']
                logger.info(f"Received snapshot_id: {snapshot_id}")
                
                # Return the snapshot_id so user can fetch it
                return {
                    "success": True,
                    "message": "Scraping job created successfully",
                    "snapshot_id": snapshot_id,
                    "status": "processing",
                    "fetch_url": f"http://localhost:8000/snapshot/{snapshot_id}",
                    "timestamp": datetime.now(),
                    "keywords_used": request.keywords
                }
            
            all_products = brightdata_service.parse_products(raw_data, request.keywords)
            logger.info("Using Bright Data API")
        except Exception as e:
            logger.warning(f"Bright Data API failed: {str(e)}, using mock data")
            raw_data = await mock_service.search_products(request.keywords)
            all_products = mock_service.parse_products(raw_data, request.keywords)
        
        # Step 3: Filter and rank products
        filtered_products = filter_service.filter_and_rank_products(
            all_products,
            min_rating=min_rating,
            max_price=max_price,
            limit=limit
        )
        
        # Step 4: Prepare response
        response = ScrapeResponse(
            success=True,
            message=f"Successfully scraped and filtered {len(filtered_products)} products",
            products=filtered_products,
            total_found=len(all_products),
            timestamp=datetime.now(),
            keywords_used=request.keywords
        )
        
        logger.info(f"Successfully processed request: {len(filtered_products)} products returned")
        return response
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error processing scrape request: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")

@app.post("/scrape-simple")
async def scrape_simple(keywords: List[str]):
    """
    Simplified endpoint for quick scraping with default filters
    """
    request = ScrapeRequest(keywords=keywords)
    return await scrape_products(request)

@app.get("/products/stats")
async def get_product_stats():
    """
    Get statistics about the filtering service
    """
    return {
        "default_filters": {
            "min_rating": settings.MIN_RATING,
            "max_price": settings.MAX_PRICE,
            "top_limit": settings.TOP_PRODUCTS_LIMIT
        },
        "api_status": {
            "brightdata_configured": bool(settings.BRIGHTDATA_API_KEY),
            "endpoint": settings.BRIGHTDATA_ENDPOINT
        }
    }

@app.get("/snapshot/{snapshot_id}")
async def get_snapshot(snapshot_
[truncated — 2599 more characters]
```

### frontend/src/main.tsx

```typescript
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";

createRoot(document.getElementById("root")!).render(<App />);

```

### frontend/src/App.tsx

```typescript
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 { AuthProvider } from "@/contexts/AuthContext";
import ProtectedRoute from "@/components/ProtectedRoute";
import Landing from "./pages/Landing";
import SignIn from "./pages/auth/SignIn";
import SignUp from "./pages/auth/SignUp";
import UserDashboard from "./pages/user/Dashboard";
import ProviderDashboard from "./pages/provider/Dashboard";
import NotFound from "./pages/NotFound";

const queryClient = new QueryClient();

const App = () => (
  <QueryClientProvider client={queryClient}>
    <TooltipProvider>
      <Toaster />
      <Sonner />
      <BrowserRouter>
        <AuthProvider>
          <Routes>
            <Route path="/" element={<Landing />} />
            <Route path="/auth/signin" element={<SignIn />} />
            <Route path="/auth/signup" element={<SignUp />} />
            <Route path="/app/user/*" element={<ProtectedRoute requiredRole="USER"><UserDashboard /></ProtectedRoute>} />
            <Route path="/app/provider/*" element={<ProtectedRoute requiredRole="PROVIDER"><ProviderDashboard /></ProtectedRoute>} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </AuthProvider>
      </BrowserRouter>
    </TooltipProvider>
  </QueryClientProvider>
);

export default App;

```

### frontend/src/pages/Index.tsx

```typescript
// Update this page (the content is just a fallback if you fail to update the page)

const Index = () => {
  return (
    <div className="flex min-h-screen items-center justify-center bg-background">
      <div className="text-center">
        <h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
        <p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
      </div>
    </div>
  );
};

export default Index;

```

### frontend/src/types/index.ts

```typescript
export type Role = 'USER' | 'PROVIDER' | 'ADMIN';

export type JobStatus =
  | 'DRAFT'
  | 'DISPATCHING'
  | 'OFFERED'
  | 'ACCEPTED'
  | 'SCHEDULED'
  | 'IN_PROGRESS'
  | 'COMPLETED'
  | 'PAID'
  | 'CANCELED';

export type AttemptOutcome = 'IGNORED' | 'REJECTED' | 'ACCEPTED' | 'EXPIRED';

export type PaymentStatus = 'INITIATED' | 'AUTHORIZED' | 'CAPTURED' | 'FAILED' | 'REFUNDED';

export interface User {
  id: string;
  email: string;
  name: string;
  role: Role;
  phone?: string;
  avatarUrl?: string;
  createdAt: string;
}

export interface Address {
  id: string;
  userId: string;
  label?: string;
  line1: string;
  line2?: string;
  city: string;
  state: string;
  country: string;
  pincode: string;
  latitude: number;
  longitude: number;
}

export interface Category {
  id: string;
  slug: string;
  name: string;
  icon?: string;
}

export interface ProviderProfile {
  id: string;
  userId: string;
  bio?: string;
  categories: Category[];
  ratingAvg: number;
  ratingCount: number;
  availability?: any;
  latitude: number;
  longitude: number;
  user?: User;
  name?: string;
}

export interface Job {
  id: string;
  userId: string;
  providerId?: string;
  categoryId: string;
  category?: Category;
  details: string;
  slotStart: string;
  slotEnd: string;
  latitude: number;
  longitude: number;
  status: JobStatus;
  dispatchOrder?: string[];
  createdAt: string;
  updatedAt: string;
  user?: User;
  provider?: ProviderProfile;
  payment?: Payment;
  reviews?: Review[];
}

export interface DispatchAttempt {
  id: string;
  jobId: string;
  providerId: string;
  rank: number;
  sentAt: string;
  respondedAt?: string;
  outcome?: AttemptOutcome;
  provider?: ProviderProfile;
  job?: Job;
}

export interface Payment {
  id: string;
  jobId: string;
  amountCents: number;
  currency: string;
  status: PaymentStatus;
  providerPayoutCents?: number;
  createdAt: string;
}

export interface Review {
  id: string;
  jobId: string;
  userId: string;
  providerId: string;
  rating: number;
  comment?: string;
  createdAt: string;
  user?: User;
}

export interface DispatchProgress {
  jobId: string;
  currentRank: number;
  totalProviders: number;
  attempts: DispatchAttempt[];
  status: 'active' | 'accepted' | 'completed' | 'failed';
  secondsRemaining?: number;
}

```

### frontend/supabase/functions/livekit-token/index.ts

```typescript
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { create } from "https://deno.land/x/djwt@v3.0.2/mod.ts";

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders });
  }

  try {
    const livekitApiKey = Deno.env.get('LIVEKIT_API_KEY');
    const livekitApiSecret = Deno.env.get('LIVEKIT_API_SECRET');
    const livekitUrl = Deno.env.get('LIVEKIT_URL');

    if (!livekitApiKey || !livekitApiSecret || !livekitUrl) {
      throw new Error('LiveKit credentials are not configured');
    }

    const { roomName, participantName } = await req.json();

    if (!roomName || !participantName) {
      throw new Error('Room name and participant name are required');
    }

    const now = Math.floor(Date.now() / 1000);
    const payload = {
      iss: livekitApiKey,
      sub: participantName,
      nbf: now,
      exp: now + 3600, // 1 hour expiration
      video: {
        room: roomName,
        roomJoin: true,
        canPublish: true,
        canSubscribe: true,
      },
      metadata: JSON.stringify({
        participantName,
      }),
    };

    // Create crypto key from secret
    const keyData = new TextEncoder().encode(livekitApiSecret);
    const key = await crypto.subtle.importKey(
      'raw',
      keyData,
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign', 'verify']
    );

    const token = await create({ alg: 'HS256', typ: 'JWT' }, payload, key);

    console.log('LiveKit token generated for room:', roomName);

    return new Response(
      JSON.stringify({ 
        token,
        url: livekitUrl 
      }),
      {
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      }
    );
  } catch (error) {
    console.error('Error generating LiveKit token:', error);
    return new Response(
      JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
      {
        status: 500,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      }
    );
  }
});

```

### frontend/supabase/functions/create-job/index.ts

```typescript
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.76.1";

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders });
  }

  try {
    const supabaseClient = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      {
        global: {
          headers: { Authorization: req.headers.get('Authorization')! },
        },
      }
    );

    const {
      data: { user },
    } = await supabaseClient.auth.getUser();

    if (!user) {
      return new Response(JSON.stringify({ error: 'Unauthorized' }), {
        status: 401,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      });
    }

    const { category_slug, details, slot_start, slot_end, location, latitude, longitude } = await req.json();

    // Get category ID from slug
    const { data: category, error: categoryError } = await supabaseClient
      .from('categories')
      .select('id')
      .eq('slug', category_slug)
      .single();

    if (categoryError || !category) {
      throw new Error('Invalid category');
    }

    // Create the job
    const { data: job, error: jobError } = await supabaseClient
      .from('jobs')
      .insert({
        user_id: user.id,
        category_id: category.id,
        details,
        slot_start,
        slot_end,
        latitude,
        longitude,
        status: 'DRAFT'
      })
      .select()
      .single();

    if (jobError) {
      console.error('Job creation error:', jobError);
      throw new Error('Failed to create service request');
    }

    console.log('Job created successfully:', job);

    return new Response(
      JSON.stringify({ success: true, job }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  } catch (error) {
    console.error('Error creating job:', error);
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    return new Response(
      JSON.stringify({ error: errorMessage }),
      {
        status: 500,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      }
    );
  }
});

```

### frontend/supabase/functions/transcribe-audio/index.ts

```typescript
import "https://deno.land/x/xhr@0.1.0/mod.ts"
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

// Process base64 in chunks to prevent memory issues
function processBase64Chunks(base64String: string, chunkSize = 32768) {
  const chunks: Uint8Array[] = [];
  let position = 0;
  
  while (position < base64String.length) {
    const chunk = base64String.slice(position, position + chunkSize);
    const binaryChunk = atob(chunk);
    const bytes = new Uint8Array(binaryChunk.length);
    
    for (let i = 0; i < binaryChunk.length; i++) {
      bytes[i] = binaryChunk.charCodeAt(i);
    }
    
    chunks.push(bytes);
    position += chunkSize;
  }

  const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;

  for (const chunk of chunks) {
    result.set(chunk, offset);
    offset += chunk.length;
  }

  return result;
}

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    const { audio } = await req.json()
    
    if (!audio) {
      throw new Error('No audio data provided')
    }

    console.log('Processing audio transcription request');

    // Process audio in chunks
    const binaryAudio = processBase64Chunks(audio)
    
    // Prepare form data
    const formData = new FormData()
    const blob = new Blob([binaryAudio], { type: 'audio/webm' })
    formData.append('file', blob, 'audio.webm')
    formData.append('model', 'whisper-1')

    // Send to OpenAI
    const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      },
      body: formData,
    })

    if (!response.ok) {
      const errorText = await response.text()
      console.error('OpenAI API error:', errorText)
      throw new Error(`OpenAI API error: ${errorText}`)
    }

    const result = await response.json()
    console.log('Transcription successful');

    return new Response(
      JSON.stringify({ text: result.text }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )

  } catch (error) {
    console.error('Transcription error:', error)
    const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
    return new Response(
      JSON.stringify({ error: errorMessage }),
      {
        status: 500,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      }
    )
  }
})
```

[115 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]