Spaces:
Sleeping
Sleeping
Bhishaj9 commited on
Commit ·
8bf695d
0
Parent(s):
Initial commit: Anvesha AI sovereign search engine
Browse files- .gitignore +34 -0
- backend/.env.template +16 -0
- backend/Dockerfile +20 -0
- backend/config.py +35 -0
- backend/main.py +255 -0
- backend/probe_instances.py +25 -0
- backend/requirements.txt +8 -0
- backend/sarvam_service.py +394 -0
- backend/test_sarvam.py +122 -0
- backend/test_search.py +205 -0
- docker-compose.prod.yml +63 -0
- docker-compose.yml +12 -0
- frontend/.gitignore +41 -0
- frontend/Dockerfile +42 -0
- frontend/README.md +36 -0
- frontend/eslint.config.mjs +18 -0
- frontend/next.config.ts +7 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +26 -0
- frontend/postcss.config.mjs +7 -0
- frontend/public/file.svg +1 -0
- frontend/public/globe.svg +1 -0
- frontend/public/next.svg +1 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/src/app/api/search/route.ts +40 -0
- frontend/src/app/api/text-to-voice/route.ts +32 -0
- frontend/src/app/api/voice-to-text/route.ts +47 -0
- frontend/src/app/favicon.ico +0 -0
- frontend/src/app/globals.css +58 -0
- frontend/src/app/layout.tsx +36 -0
- frontend/src/app/page.tsx +21 -0
- frontend/src/app/search/page.tsx +156 -0
- frontend/src/components/CTASection.tsx +26 -0
- frontend/src/components/FeaturesSection.tsx +55 -0
- frontend/src/components/Footer.tsx +35 -0
- frontend/src/components/Header.tsx +45 -0
- frontend/src/components/HeroSection.tsx +46 -0
- frontend/src/components/PhilosophySection.tsx +20 -0
- frontend/src/components/SearchBar.tsx +151 -0
- frontend/src/components/SearchSidebar.tsx +58 -0
- frontend/src/components/SuggestionPills.tsx +30 -0
- frontend/src/components/SutraCard.tsx +149 -0
- frontend/stitch/anvesha_ai_landing_page/code.html +190 -0
- frontend/stitch/anvesha_ai_search_interface/code.html +178 -0
- frontend/tsconfig.json +34 -0
- searxng/settings.yml +18 -0
.gitignore
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
dist/
|
| 4 |
+
build/
|
| 5 |
+
.next/
|
| 6 |
+
|
| 7 |
+
# Environment Variables
|
| 8 |
+
.env
|
| 9 |
+
.env.local
|
| 10 |
+
.env.development.local
|
| 11 |
+
.env.test.local
|
| 12 |
+
.env.production.local
|
| 13 |
+
backend/.env
|
| 14 |
+
frontend/.env
|
| 15 |
+
|
| 16 |
+
# Python
|
| 17 |
+
__pycache__/
|
| 18 |
+
*.py[cod]
|
| 19 |
+
*$py.class
|
| 20 |
+
venv/
|
| 21 |
+
.venv/
|
| 22 |
+
env/
|
| 23 |
+
.env/
|
| 24 |
+
|
| 25 |
+
# IDEs
|
| 26 |
+
.vscode/
|
| 27 |
+
.idea/
|
| 28 |
+
|
| 29 |
+
# OS files
|
| 30 |
+
.DS_Store
|
| 31 |
+
Thumbs.db
|
| 32 |
+
|
| 33 |
+
# Docker
|
| 34 |
+
.docker/
|
backend/.env.template
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anvesha AI - Environment Variables Template
|
| 2 |
+
# Copy this file to .env and fill in your values.
|
| 3 |
+
|
| 4 |
+
# ── Sarvam AI ──────────────────────────────────────────────────
|
| 5 |
+
SARVAM_API_KEY=your_sarvam_api_key_here
|
| 6 |
+
SARVAM_API_BASE=https://api.sarvam.ai/v1
|
| 7 |
+
SARVAM_ROUTER_MODEL=sarvam-30b
|
| 8 |
+
SARVAM_SYNTH_MODEL=sarvam-105b
|
| 9 |
+
|
| 10 |
+
# ── Local Search Orchestration ─────────────────────────────────
|
| 11 |
+
SEARXNG_BASE_URL=http://localhost:8080
|
| 12 |
+
SEARCH_REGION_DEFAULT=in-en
|
| 13 |
+
SEARCH_LANG_DEFAULT=en
|
| 14 |
+
|
| 15 |
+
# ── Frontend ───────────────────────────────────────────────────
|
| 16 |
+
FRONTEND_URL=http://localhost:3000
|
backend/Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.13-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install dependencies
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy application code
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
# Expose port
|
| 13 |
+
EXPOSE 8000
|
| 14 |
+
|
| 15 |
+
# Health check
|
| 16 |
+
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
| 17 |
+
CMD python -c "import httpx; httpx.get('http://localhost:8000/')" || exit 1
|
| 18 |
+
|
| 19 |
+
# Run with uvicorn
|
| 20 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
backend/config.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Settings(BaseSettings):
|
| 6 |
+
"""
|
| 7 |
+
Anvesha AI backend configuration.
|
| 8 |
+
All values are loaded from the .env file in the backend directory.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
# ── Sarvam AI ──────────────────────────────────────────────
|
| 12 |
+
SARVAM_API_KEY: str = ""
|
| 13 |
+
SARVAM_API_BASE: str = "https://api.sarvam.ai/v1"
|
| 14 |
+
SARVAM_ROUTER_MODEL: str = "sarvam-30b"
|
| 15 |
+
SARVAM_SYNTH_MODEL: str = "sarvam-105b"
|
| 16 |
+
|
| 17 |
+
# ── SearxNG ────────────────────────────────────────────────
|
| 18 |
+
SEARXNG_BASE_URL: str = "http://localhost:8080"
|
| 19 |
+
SEARCH_REGION_DEFAULT: str = "in-en"
|
| 20 |
+
SEARCH_LANG_DEFAULT: str = "en"
|
| 21 |
+
|
| 22 |
+
# ── Frontend ───────────────────────────────────────────────
|
| 23 |
+
FRONTEND_URL: str = "http://localhost:3000"
|
| 24 |
+
|
| 25 |
+
model_config = {
|
| 26 |
+
"env_file": ".env",
|
| 27 |
+
"env_file_encoding": "utf-8",
|
| 28 |
+
"extra": "ignore",
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@lru_cache()
|
| 33 |
+
def get_settings() -> Settings:
|
| 34 |
+
"""Cached singleton for app settings."""
|
| 35 |
+
return Settings()
|
backend/main.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
from fastapi import FastAPI, File, Query, UploadFile
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
import httpx
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
from config import get_settings
|
| 10 |
+
from sarvam_service import (
|
| 11 |
+
route_query,
|
| 12 |
+
synthesize_response,
|
| 13 |
+
speech_to_text,
|
| 14 |
+
text_to_speech,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# ── Logging ────────────────────────────────────────────────────
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger("anvesha.api")
|
| 20 |
+
|
| 21 |
+
# ── App ────────────────────────────────────────────────────────
|
| 22 |
+
settings = get_settings()
|
| 23 |
+
app = FastAPI(
|
| 24 |
+
title="Anvesha AI Backend",
|
| 25 |
+
description="Sovereign Indian search & intelligence API",
|
| 26 |
+
version="0.4.0",
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# CORS: allow frontend dev server
|
| 30 |
+
app.add_middleware(
|
| 31 |
+
CORSMiddleware,
|
| 32 |
+
allow_origins=[settings.FRONTEND_URL],
|
| 33 |
+
allow_credentials=True,
|
| 34 |
+
allow_methods=["*"],
|
| 35 |
+
allow_headers=["*"],
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ── Request / Response Models ──────────────────────────────────
|
| 40 |
+
|
| 41 |
+
class AskRequest(BaseModel):
|
| 42 |
+
query: str
|
| 43 |
+
region: str = "in-en"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class Citation(BaseModel):
|
| 47 |
+
index: int
|
| 48 |
+
title: str
|
| 49 |
+
url: str
|
| 50 |
+
is_gov: bool = False
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class SutraResponse(BaseModel):
|
| 54 |
+
summary: str
|
| 55 |
+
citations: list[Citation] = []
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class AskResponse(BaseModel):
|
| 59 |
+
sutra: SutraResponse
|
| 60 |
+
raw_results: list[dict] = []
|
| 61 |
+
routed_queries: list[str] = []
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class TTSRequest(BaseModel):
|
| 65 |
+
text: str
|
| 66 |
+
language: str = "en-IN"
|
| 67 |
+
speaker: str = "meera"
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class TTSResponse(BaseModel):
|
| 71 |
+
audio_base64: str
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class STTResponse(BaseModel):
|
| 75 |
+
text: str
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ── Helpers ────────────────────────────────────────────────────
|
| 79 |
+
|
| 80 |
+
async def _searxng_search(client: httpx.AsyncClient, query: str, region: str) -> list[dict]:
|
| 81 |
+
"""Execute a single SearxNG search and return results."""
|
| 82 |
+
try:
|
| 83 |
+
response = await client.get(
|
| 84 |
+
f"{settings.SEARXNG_BASE_URL}/search",
|
| 85 |
+
params={
|
| 86 |
+
"q": query,
|
| 87 |
+
"format": "json",
|
| 88 |
+
"pageno": 1,
|
| 89 |
+
"time_range": "",
|
| 90 |
+
"language": region.split("-")[1] if "-" in region else "en",
|
| 91 |
+
"safesearch": 0,
|
| 92 |
+
},
|
| 93 |
+
timeout=15.0,
|
| 94 |
+
)
|
| 95 |
+
response.raise_for_status()
|
| 96 |
+
data = response.json()
|
| 97 |
+
return data.get("results", [])
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.warning(f"SearxNG search failed for '{query}': {e}")
|
| 100 |
+
return []
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _deduplicate_results(results: list[dict]) -> list[dict]:
|
| 104 |
+
"""Remove duplicate results by URL, preserving order."""
|
| 105 |
+
seen_urls = set()
|
| 106 |
+
unique = []
|
| 107 |
+
for r in results:
|
| 108 |
+
url = r.get("url", "")
|
| 109 |
+
if url and url not in seen_urls:
|
| 110 |
+
seen_urls.add(url)
|
| 111 |
+
unique.append(r)
|
| 112 |
+
return unique
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ── Endpoints ──────────────────────────────────────────────────
|
| 116 |
+
|
| 117 |
+
@app.get("/")
|
| 118 |
+
def read_root():
|
| 119 |
+
return {"message": "Welcome to Anvesha AI Backend", "version": "0.4.0"}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@app.get("/search")
|
| 123 |
+
async def search(
|
| 124 |
+
q: str = Query(..., description="Search query"),
|
| 125 |
+
region: Optional[str] = Query("in-en", description="Search region"),
|
| 126 |
+
):
|
| 127 |
+
"""Direct SearxNG pass-through (Week 1 legacy endpoint)."""
|
| 128 |
+
async with httpx.AsyncClient() as client:
|
| 129 |
+
try:
|
| 130 |
+
response = await client.get(
|
| 131 |
+
f"{settings.SEARXNG_BASE_URL}/search",
|
| 132 |
+
params={
|
| 133 |
+
"q": q,
|
| 134 |
+
"format": "json",
|
| 135 |
+
"pageno": 1,
|
| 136 |
+
"time_range": "",
|
| 137 |
+
"language": region.split("-")[1] if "-" in region else "en",
|
| 138 |
+
"safesearch": 0,
|
| 139 |
+
},
|
| 140 |
+
timeout=15.0,
|
| 141 |
+
)
|
| 142 |
+
response.raise_for_status()
|
| 143 |
+
return response.json()
|
| 144 |
+
except Exception as e:
|
| 145 |
+
return {"error": str(e)}
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@app.post("/ask", response_model=AskResponse)
|
| 149 |
+
async def ask(request: AskRequest):
|
| 150 |
+
"""
|
| 151 |
+
🧠 The Intelligence Pipeline — Week 3
|
| 152 |
+
|
| 153 |
+
Full pipeline:
|
| 154 |
+
1. Router (Sarvam 30B) decomposes the user query into optimized searches
|
| 155 |
+
2. Fan-out SearxNG searches for each optimized query
|
| 156 |
+
3. Deduplicate results
|
| 157 |
+
4. Synthesizer (Sarvam 105B) generates a cited Sutra response
|
| 158 |
+
5. Return the Sutra + raw results
|
| 159 |
+
"""
|
| 160 |
+
logger.info(f"🔍 ASK request: '{request.query}'")
|
| 161 |
+
|
| 162 |
+
# Step 1: Route the query
|
| 163 |
+
routed_queries = await route_query(request.query)
|
| 164 |
+
logger.info(f"Routed queries: {routed_queries}")
|
| 165 |
+
|
| 166 |
+
# Step 2: Fan-out SearxNG searches
|
| 167 |
+
async with httpx.AsyncClient() as client:
|
| 168 |
+
tasks = [
|
| 169 |
+
_searxng_search(client, q, request.region)
|
| 170 |
+
for q in routed_queries
|
| 171 |
+
]
|
| 172 |
+
all_results = await asyncio.gather(*tasks)
|
| 173 |
+
|
| 174 |
+
merged = []
|
| 175 |
+
for result_list in all_results:
|
| 176 |
+
merged.extend(result_list)
|
| 177 |
+
|
| 178 |
+
# Step 3: Deduplicate
|
| 179 |
+
unique_results = _deduplicate_results(merged)
|
| 180 |
+
logger.info(f"{len(merged)} total → {len(unique_results)} unique results")
|
| 181 |
+
|
| 182 |
+
# Step 4: Synthesize
|
| 183 |
+
sutra_data = await synthesize_response(request.query, unique_results)
|
| 184 |
+
|
| 185 |
+
# Step 5: Return
|
| 186 |
+
sutra = SutraResponse(
|
| 187 |
+
summary=sutra_data.get("summary", ""),
|
| 188 |
+
citations=[
|
| 189 |
+
Citation(
|
| 190 |
+
index=c.get("index", i + 1),
|
| 191 |
+
title=c.get("title", ""),
|
| 192 |
+
url=c.get("url", ""),
|
| 193 |
+
is_gov=c.get("is_gov", False),
|
| 194 |
+
)
|
| 195 |
+
for i, c in enumerate(sutra_data.get("citations", []))
|
| 196 |
+
],
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
return AskResponse(
|
| 200 |
+
sutra=sutra,
|
| 201 |
+
raw_results=unique_results[:20],
|
| 202 |
+
routed_queries=routed_queries,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# ── Voice Endpoints — Week 4 ──────────────────────────────────
|
| 207 |
+
|
| 208 |
+
@app.post("/voice-to-text", response_model=STTResponse)
|
| 209 |
+
async def voice_to_text(
|
| 210 |
+
file: UploadFile = File(..., description="Audio file (WAV, MP3, WebM, OGG)"),
|
| 211 |
+
language: str = Query("en-IN", description="Language code (e.g. hi-IN, en-IN)"),
|
| 212 |
+
):
|
| 213 |
+
"""
|
| 214 |
+
🎙️ Speech-to-Text — Sarvam Saaras V3
|
| 215 |
+
|
| 216 |
+
Accepts an audio file upload and returns the transcribed text.
|
| 217 |
+
"""
|
| 218 |
+
logger.info(f"🎙️ STT request: {file.filename} ({language})")
|
| 219 |
+
|
| 220 |
+
audio_bytes = await file.read()
|
| 221 |
+
if not audio_bytes:
|
| 222 |
+
return STTResponse(text="")
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
transcript = await speech_to_text(audio_bytes, language_code=language)
|
| 226 |
+
logger.info(f"✅ STT complete: '{transcript[:60]}...'")
|
| 227 |
+
return STTResponse(text=transcript)
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logger.error(f"STT endpoint error: {e}")
|
| 230 |
+
return STTResponse(text=f"[Voice recognition error: {str(e)}]")
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@app.post("/text-to-voice", response_model=TTSResponse)
|
| 234 |
+
async def text_to_voice(request: TTSRequest):
|
| 235 |
+
"""
|
| 236 |
+
🔊 Text-to-Speech — Sarvam Bulbul V3
|
| 237 |
+
|
| 238 |
+
Accepts text and returns base64-encoded WAV audio.
|
| 239 |
+
"""
|
| 240 |
+
logger.info(f"🔊 TTS request: '{request.text[:60]}...' ({request.language})")
|
| 241 |
+
|
| 242 |
+
if not request.text.strip():
|
| 243 |
+
return TTSResponse(audio_base64="")
|
| 244 |
+
|
| 245 |
+
try:
|
| 246 |
+
audio_b64 = await text_to_speech(
|
| 247 |
+
text=request.text,
|
| 248 |
+
target_language_code=request.language,
|
| 249 |
+
speaker=request.speaker,
|
| 250 |
+
)
|
| 251 |
+
logger.info(f"✅ TTS complete: {len(audio_b64)} base64 chars")
|
| 252 |
+
return TTSResponse(audio_base64=audio_b64)
|
| 253 |
+
except Exception as e:
|
| 254 |
+
logger.error(f"TTS endpoint error: {e}")
|
| 255 |
+
return TTSResponse(audio_base64="")
|
backend/probe_instances.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
import json
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
headers = {
|
| 6 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
| 7 |
+
"Accept": "application/json, text/html",
|
| 8 |
+
}
|
| 9 |
+
params = {"q": "India government digital scheme", "format": "json"}
|
| 10 |
+
|
| 11 |
+
urls = [
|
| 12 |
+
"https://search.inetol.net/search",
|
| 13 |
+
"https://search.hbubli.cc/search",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
for u in urls:
|
| 17 |
+
print(f"\nTrying {u}...")
|
| 18 |
+
try:
|
| 19 |
+
r = httpx.get(u, params=params, headers=headers, timeout=15, follow_redirects=True)
|
| 20 |
+
print(f" Status: {r.status_code}")
|
| 21 |
+
ct = r.headers.get("content-type", "unknown")
|
| 22 |
+
print(f" Content-Type: {ct}")
|
| 23 |
+
print(f" Body (first 500 chars): {r.text[:500]}")
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f" Error: {e}")
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
httpx
|
| 4 |
+
pydantic
|
| 5 |
+
pydantic-settings
|
| 6 |
+
python-dotenv
|
| 7 |
+
openai
|
| 8 |
+
python-multipart
|
backend/sarvam_service.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
sarvam_service.py — Sarvam AI Integration for Anvesha AI
|
| 3 |
+
|
| 4 |
+
Router (Sarvam 30B): Decomposes a user query into optimized SearxNG searches.
|
| 5 |
+
Synthesizer (Sarvam 105B): Generates a cited "Sutra" response from search results.
|
| 6 |
+
STT (Saaras V3): Speech-to-text for voice input.
|
| 7 |
+
TTS (Bulbul V3): Text-to-speech for audio output.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import base64
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
import httpx
|
| 14 |
+
from openai import AsyncOpenAI
|
| 15 |
+
from config import get_settings
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger("anvesha.sarvam")
|
| 18 |
+
|
| 19 |
+
# ─────────────────────────────────────────────────────────────────
|
| 20 |
+
# Client factory
|
| 21 |
+
# ─────────────────────────────────────────────────────────────────
|
| 22 |
+
|
| 23 |
+
def _get_client() -> AsyncOpenAI:
|
| 24 |
+
"""Create an AsyncOpenAI client pointed at the Sarvam API."""
|
| 25 |
+
settings = get_settings()
|
| 26 |
+
if not settings.SARVAM_API_KEY:
|
| 27 |
+
raise ValueError(
|
| 28 |
+
"SARVAM_API_KEY is not set. "
|
| 29 |
+
"Please add it to your .env file (see .env.template)."
|
| 30 |
+
)
|
| 31 |
+
return AsyncOpenAI(
|
| 32 |
+
api_key=settings.SARVAM_API_KEY,
|
| 33 |
+
base_url=settings.SARVAM_API_BASE,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ─────────────────────────────────────────────────────────────────
|
| 38 |
+
# 1. THE ROUTER — Sarvam 30B
|
| 39 |
+
# ─────────────────────────────────────────────────────────────────
|
| 40 |
+
|
| 41 |
+
ROUTER_SYSTEM_PROMPT = """\
|
| 42 |
+
You are a search-query optimizer for an Indian sovereign search engine called Anvesha AI.
|
| 43 |
+
|
| 44 |
+
Given a user's natural-language question, produce 3 to 5 independent, \
|
| 45 |
+
optimized web-search queries that will retrieve the most relevant results. \
|
| 46 |
+
Focus on Indian context and government sources (.gov.in) where appropriate.
|
| 47 |
+
|
| 48 |
+
Rules:
|
| 49 |
+
1. Each query should target a different facet of the user's intent.
|
| 50 |
+
2. Include at least one query that specifically targets Indian government \
|
| 51 |
+
sources (add "site:gov.in" where it makes sense).
|
| 52 |
+
3. Keep queries concise (5-10 words each).
|
| 53 |
+
4. Return ONLY a JSON array of strings, no other text.
|
| 54 |
+
|
| 55 |
+
Example input: "Latest union budget highlights"
|
| 56 |
+
Example output: ["Union Budget 2025-26 key highlights", \
|
| 57 |
+
"site:gov.in union budget 2025 document", \
|
| 58 |
+
"India budget tax changes summary", \
|
| 59 |
+
"budget allocation education health India 2025"]
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
async def route_query(user_query: str) -> list[str]:
|
| 64 |
+
"""
|
| 65 |
+
Router — uses Sarvam 30B to decompose a natural-language query
|
| 66 |
+
into 3–5 optimized search queries for SearxNG.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
user_query: The raw user question, e.g. "Latest budget news"
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
A list of 3–5 optimized search query strings.
|
| 73 |
+
|
| 74 |
+
Raises:
|
| 75 |
+
ValueError: If the API key is missing.
|
| 76 |
+
Exception: On API or parsing errors (logged and re-raised).
|
| 77 |
+
"""
|
| 78 |
+
settings = get_settings()
|
| 79 |
+
client = _get_client()
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
response = await client.chat.completions.create(
|
| 83 |
+
model=settings.SARVAM_ROUTER_MODEL,
|
| 84 |
+
messages=[
|
| 85 |
+
{"role": "system", "content": ROUTER_SYSTEM_PROMPT},
|
| 86 |
+
{"role": "user", "content": user_query},
|
| 87 |
+
],
|
| 88 |
+
temperature=0.3,
|
| 89 |
+
max_tokens=512,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
raw = response.choices[0].message.content.strip()
|
| 93 |
+
logger.info(f"Router raw response: {raw}")
|
| 94 |
+
|
| 95 |
+
# Parse JSON array — handle markdown code fences if present
|
| 96 |
+
cleaned = raw
|
| 97 |
+
if cleaned.startswith("```"):
|
| 98 |
+
# Strip ```json ... ``` wrapping
|
| 99 |
+
lines = cleaned.split("\n")
|
| 100 |
+
cleaned = "\n".join(
|
| 101 |
+
line for line in lines
|
| 102 |
+
if not line.strip().startswith("```")
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
queries = json.loads(cleaned)
|
| 106 |
+
|
| 107 |
+
if not isinstance(queries, list) or not all(isinstance(q, str) for q in queries):
|
| 108 |
+
raise ValueError(f"Expected a JSON array of strings, got: {type(queries)}")
|
| 109 |
+
|
| 110 |
+
# Clamp to 3-5 queries
|
| 111 |
+
return queries[:5] if len(queries) > 5 else queries
|
| 112 |
+
|
| 113 |
+
except json.JSONDecodeError as e:
|
| 114 |
+
logger.error(f"Router JSON parse error: {e}. Raw: {raw}")
|
| 115 |
+
# Fallback: return the original query so search still works
|
| 116 |
+
return [user_query]
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"Router error: {e}")
|
| 119 |
+
# Graceful degradation — use original query
|
| 120 |
+
return [user_query]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# ─────────────────────────────────────────────────────────────────
|
| 124 |
+
# 2. THE SYNTHESIZER — Sarvam 105B
|
| 125 |
+
# ─────────────────��───────────────────────────────────────────────
|
| 126 |
+
|
| 127 |
+
SYNTH_SYSTEM_PROMPT = """\
|
| 128 |
+
You are a sovereign Indian intelligence engine called Anvesha AI. \
|
| 129 |
+
Your purpose is to synthesize search results into a clear, authoritative, \
|
| 130 |
+
and well-cited response following the "Sutra" format.
|
| 131 |
+
|
| 132 |
+
Rules:
|
| 133 |
+
1. Provide a comprehensive yet concise summary answering the user's query.
|
| 134 |
+
2. Use inline citations like [1], [2], etc. that correspond to the source numbers.
|
| 135 |
+
3. PRIORITIZE information from .gov.in sources — cite them first and prominently.
|
| 136 |
+
4. If a .gov.in source is available, always prefer it over other sources.
|
| 137 |
+
5. At the end, list all citations with their source URLs.
|
| 138 |
+
6. Maintain a professional, authoritative tone appropriate for policy research.
|
| 139 |
+
7. If the search results don't contain relevant information, say so honestly.
|
| 140 |
+
|
| 141 |
+
Return your response as JSON with this exact structure:
|
| 142 |
+
{
|
| 143 |
+
"summary": "Your synthesized response with [1], [2] inline citations...",
|
| 144 |
+
"citations": [
|
| 145 |
+
{"index": 1, "title": "Source title", "url": "https://...", "is_gov": true},
|
| 146 |
+
{"index": 2, "title": "Source title", "url": "https://...", "is_gov": false}
|
| 147 |
+
]
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
Return ONLY valid JSON, no other text.
|
| 151 |
+
"""
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _format_context(search_results: list[dict]) -> str:
|
| 155 |
+
"""Format search results into a numbered context block for the LLM."""
|
| 156 |
+
lines = []
|
| 157 |
+
for i, result in enumerate(search_results, 1):
|
| 158 |
+
title = result.get("title", "Untitled")
|
| 159 |
+
url = result.get("url", "")
|
| 160 |
+
content = result.get("content", "")
|
| 161 |
+
is_gov = ".gov.in" in url
|
| 162 |
+
gov_marker = " [GOV.IN SOURCE]" if is_gov else ""
|
| 163 |
+
|
| 164 |
+
lines.append(
|
| 165 |
+
f"[{i}]{gov_marker}\n"
|
| 166 |
+
f"Title: {title}\n"
|
| 167 |
+
f"URL: {url}\n"
|
| 168 |
+
f"Content: {content}\n"
|
| 169 |
+
)
|
| 170 |
+
return "\n---\n".join(lines)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
async def synthesize_response(
|
| 174 |
+
user_query: str,
|
| 175 |
+
search_results: list[dict],
|
| 176 |
+
) -> dict:
|
| 177 |
+
"""
|
| 178 |
+
Synthesizer — uses Sarvam 105B to generate a cited "Sutra" response
|
| 179 |
+
from search results, prioritizing .gov.in sources.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
user_query: The original user question.
|
| 183 |
+
search_results: List of dicts with keys: title, url, content.
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
A dict with "summary" (str with inline [N] citations) and
|
| 187 |
+
"citations" (list of citation dicts).
|
| 188 |
+
|
| 189 |
+
Raises:
|
| 190 |
+
ValueError: If the API key is missing.
|
| 191 |
+
Exception: On API or parsing errors (logged, returns fallback).
|
| 192 |
+
"""
|
| 193 |
+
if not search_results:
|
| 194 |
+
return {
|
| 195 |
+
"summary": "No search results were found for your query. "
|
| 196 |
+
"Please try rephrasing or broadening your search.",
|
| 197 |
+
"citations": [],
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
settings = get_settings()
|
| 201 |
+
client = _get_client()
|
| 202 |
+
|
| 203 |
+
# Sort results: .gov.in sources first
|
| 204 |
+
sorted_results = sorted(
|
| 205 |
+
search_results,
|
| 206 |
+
key=lambda r: 0 if ".gov.in" in r.get("url", "") else 1,
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
context = _format_context(sorted_results[:15]) # Limit to top 15
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
response = await client.chat.completions.create(
|
| 213 |
+
model=settings.SARVAM_SYNTH_MODEL,
|
| 214 |
+
messages=[
|
| 215 |
+
{"role": "system", "content": SYNTH_SYSTEM_PROMPT},
|
| 216 |
+
{
|
| 217 |
+
"role": "user",
|
| 218 |
+
"content": (
|
| 219 |
+
f"User Query: {user_query}\n\n"
|
| 220 |
+
f"Search Results:\n{context}"
|
| 221 |
+
),
|
| 222 |
+
},
|
| 223 |
+
],
|
| 224 |
+
temperature=0.4,
|
| 225 |
+
max_tokens=2048,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
raw = response.choices[0].message.content.strip()
|
| 229 |
+
logger.info(f"Synthesizer raw response length: {len(raw)} chars")
|
| 230 |
+
|
| 231 |
+
# Parse JSON — handle markdown code fences
|
| 232 |
+
cleaned = raw
|
| 233 |
+
if cleaned.startswith("```"):
|
| 234 |
+
lines = cleaned.split("\n")
|
| 235 |
+
cleaned = "\n".join(
|
| 236 |
+
line for line in lines
|
| 237 |
+
if not line.strip().startswith("```")
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
result = json.loads(cleaned)
|
| 241 |
+
|
| 242 |
+
# Validate structure
|
| 243 |
+
if "summary" not in result:
|
| 244 |
+
result["summary"] = raw # Fallback to raw text
|
| 245 |
+
if "citations" not in result:
|
| 246 |
+
result["citations"] = []
|
| 247 |
+
|
| 248 |
+
return result
|
| 249 |
+
|
| 250 |
+
except json.JSONDecodeError as e:
|
| 251 |
+
logger.error(f"Synthesizer JSON parse error: {e}")
|
| 252 |
+
# Return raw text as summary with basic citations
|
| 253 |
+
return {
|
| 254 |
+
"summary": raw if 'raw' in dir() else "Failed to generate response.",
|
| 255 |
+
"citations": [
|
| 256 |
+
{
|
| 257 |
+
"index": i + 1,
|
| 258 |
+
"title": r.get("title", ""),
|
| 259 |
+
"url": r.get("url", ""),
|
| 260 |
+
"is_gov": ".gov.in" in r.get("url", ""),
|
| 261 |
+
}
|
| 262 |
+
for i, r in enumerate(sorted_results[:10])
|
| 263 |
+
],
|
| 264 |
+
}
|
| 265 |
+
except Exception as e:
|
| 266 |
+
logger.error(f"Synthesizer error: {e}")
|
| 267 |
+
return {
|
| 268 |
+
"summary": f"An error occurred while generating the response: {str(e)}",
|
| 269 |
+
"citations": [],
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# ─────────────────────────────────────────────────────────────────
|
| 274 |
+
# 3. SPEECH-TO-TEXT — Saaras V3
|
| 275 |
+
# ─────────────────────────────────────────────────────────────────
|
| 276 |
+
|
| 277 |
+
SARVAM_STT_URL = "https://api.sarvam.ai/speech-to-text"
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
async def speech_to_text(
|
| 281 |
+
audio_bytes: bytes,
|
| 282 |
+
language_code: str = "hi-IN",
|
| 283 |
+
model: str = "saaras:v3",
|
| 284 |
+
) -> str:
|
| 285 |
+
"""
|
| 286 |
+
Convert speech audio to text using Sarvam Saaras V3.
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
audio_bytes: Raw audio file bytes (WAV, MP3, WebM, OGG).
|
| 290 |
+
language_code: BCP-47 language code (e.g. "hi-IN", "en-IN").
|
| 291 |
+
model: Sarvam STT model identifier.
|
| 292 |
+
|
| 293 |
+
Returns:
|
| 294 |
+
Transcribed text string.
|
| 295 |
+
"""
|
| 296 |
+
settings = get_settings()
|
| 297 |
+
if not settings.SARVAM_API_KEY:
|
| 298 |
+
raise ValueError("SARVAM_API_KEY is not set.")
|
| 299 |
+
|
| 300 |
+
try:
|
| 301 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 302 |
+
response = await client.post(
|
| 303 |
+
SARVAM_STT_URL,
|
| 304 |
+
headers={"api-subscription-key": settings.SARVAM_API_KEY},
|
| 305 |
+
data={
|
| 306 |
+
"language_code": language_code,
|
| 307 |
+
"model": model,
|
| 308 |
+
"with_timestamps": "false",
|
| 309 |
+
},
|
| 310 |
+
files={"file": ("audio.wav", audio_bytes, "audio/wav")},
|
| 311 |
+
)
|
| 312 |
+
response.raise_for_status()
|
| 313 |
+
data = response.json()
|
| 314 |
+
transcript = data.get("transcript", "")
|
| 315 |
+
logger.info(f"STT result ({language_code}): '{transcript[:80]}...'")
|
| 316 |
+
return transcript
|
| 317 |
+
|
| 318 |
+
except Exception as e:
|
| 319 |
+
logger.error(f"STT error: {e}")
|
| 320 |
+
raise
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
# ─────────────────────────────────────────────────────────────────
|
| 324 |
+
# 4. TEXT-TO-SPEECH — Bulbul V3
|
| 325 |
+
# ─────────────────────────────────────────────────────────────────
|
| 326 |
+
|
| 327 |
+
SARVAM_TTS_URL = "https://api.sarvam.ai/text-to-speech"
|
| 328 |
+
|
| 329 |
+
# Available Bulbul V3 speakers
|
| 330 |
+
DEFAULT_SPEAKER = "meera" # Natural female Indian voice
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
async def text_to_speech(
|
| 334 |
+
text: str,
|
| 335 |
+
target_language_code: str = "hi-IN",
|
| 336 |
+
speaker: str = DEFAULT_SPEAKER,
|
| 337 |
+
model: str = "bulbul:v3",
|
| 338 |
+
pace: float = 1.0,
|
| 339 |
+
) -> str:
|
| 340 |
+
"""
|
| 341 |
+
Convert text to speech using Sarvam Bulbul V3.
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
text: The text to convert (max 2500 chars).
|
| 345 |
+
target_language_code: BCP-47 language code for output audio.
|
| 346 |
+
speaker: Voice name from Bulbul V3 library.
|
| 347 |
+
model: Sarvam TTS model identifier.
|
| 348 |
+
pace: Speech speed multiplier (0.5–2.0).
|
| 349 |
+
|
| 350 |
+
Returns:
|
| 351 |
+
Base64-encoded WAV audio string.
|
| 352 |
+
"""
|
| 353 |
+
settings = get_settings()
|
| 354 |
+
if not settings.SARVAM_API_KEY:
|
| 355 |
+
raise ValueError("SARVAM_API_KEY is not set.")
|
| 356 |
+
|
| 357 |
+
# Truncate to Bulbul V3 limit
|
| 358 |
+
if len(text) > 2500:
|
| 359 |
+
text = text[:2497] + "..."
|
| 360 |
+
|
| 361 |
+
try:
|
| 362 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 363 |
+
response = await client.post(
|
| 364 |
+
SARVAM_TTS_URL,
|
| 365 |
+
headers={
|
| 366 |
+
"api-subscription-key": settings.SARVAM_API_KEY,
|
| 367 |
+
"Content-Type": "application/json",
|
| 368 |
+
},
|
| 369 |
+
json={
|
| 370 |
+
"inputs": [text],
|
| 371 |
+
"target_language_code": target_language_code,
|
| 372 |
+
"speaker": speaker,
|
| 373 |
+
"model": model,
|
| 374 |
+
"pace": pace,
|
| 375 |
+
"enable_preprocessing": True,
|
| 376 |
+
},
|
| 377 |
+
)
|
| 378 |
+
response.raise_for_status()
|
| 379 |
+
data = response.json()
|
| 380 |
+
audios = data.get("audios", [])
|
| 381 |
+
|
| 382 |
+
if not audios:
|
| 383 |
+
raise ValueError("No audio returned from TTS API")
|
| 384 |
+
|
| 385 |
+
audio_base64 = audios[0]
|
| 386 |
+
logger.info(
|
| 387 |
+
f"TTS result ({target_language_code}, {speaker}): "
|
| 388 |
+
f"{len(audio_base64)} base64 chars"
|
| 389 |
+
)
|
| 390 |
+
return audio_base64
|
| 391 |
+
|
| 392 |
+
except Exception as e:
|
| 393 |
+
logger.error(f"TTS error: {e}")
|
| 394 |
+
raise
|
backend/test_sarvam.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
test_sarvam.py — Quick tests for the Sarvam AI service layer.
|
| 3 |
+
|
| 4 |
+
Run: python test_sarvam.py
|
| 5 |
+
Requires: SARVAM_API_KEY set in .env
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import json
|
| 10 |
+
import sys
|
| 11 |
+
from config import get_settings
|
| 12 |
+
from sarvam_service import route_query, synthesize_response
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def print_section(title: str):
|
| 16 |
+
print(f"\n{'='*60}")
|
| 17 |
+
print(f" {title}")
|
| 18 |
+
print(f"{'='*60}\n")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
async def test_router():
|
| 22 |
+
"""Test the Router (Sarvam 30B) query decomposition."""
|
| 23 |
+
print_section("TEST 1: Router — Query Decomposition (Sarvam 30B)")
|
| 24 |
+
|
| 25 |
+
test_queries = [
|
| 26 |
+
"Latest budget news",
|
| 27 |
+
"RTI Act rules and amendments",
|
| 28 |
+
"Digital India initiative progress 2025",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
for query in test_queries:
|
| 32 |
+
print(f"Input: \"{query}\"")
|
| 33 |
+
try:
|
| 34 |
+
result = await route_query(query)
|
| 35 |
+
print(f"Output: {json.dumps(result, indent=2)}")
|
| 36 |
+
print(f"Count: {len(result)} queries")
|
| 37 |
+
assert isinstance(result, list), "Expected list"
|
| 38 |
+
assert len(result) >= 1, "Expected at least 1 query"
|
| 39 |
+
print("Status: ✅ PASS\n")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"Status: ❌ FAIL — {e}\n")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
async def test_synthesizer():
|
| 45 |
+
"""Test the Synthesizer (Sarvam 105B) with mock search results."""
|
| 46 |
+
print_section("TEST 2: Synthesizer — Sutra Generation (Sarvam 105B)")
|
| 47 |
+
|
| 48 |
+
mock_results = [
|
| 49 |
+
{
|
| 50 |
+
"title": "Union Budget 2025-26 Highlights",
|
| 51 |
+
"url": "https://www.indiabudget.gov.in/highlights",
|
| 52 |
+
"content": "The Union Budget 2025-26 proposes significant changes to income tax slabs, "
|
| 53 |
+
"with the new regime offering zero tax up to ₹12 lakh. Infrastructure spending "
|
| 54 |
+
"increased to ₹11.2 lakh crore.",
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"title": "Budget 2025: Key Takeaways for Indian Economy",
|
| 58 |
+
"url": "https://economictimes.indiatimes.com/budget-2025",
|
| 59 |
+
"content": "Finance Minister announced major agricultural reforms and a new urban housing scheme. "
|
| 60 |
+
"The fiscal deficit target is set at 4.4% of GDP.",
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"title": "Digital India Budget Allocation 2025",
|
| 64 |
+
"url": "https://www.digitalindia.gov.in/budget-2025",
|
| 65 |
+
"content": "Digital India received ₹15,000 crore allocation for AI research and semiconductor "
|
| 66 |
+
"manufacturing. New centres of excellence to be established across 10 states.",
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"title": "Budget Analysis by Financial Experts",
|
| 70 |
+
"url": "https://www.moneycontrol.com/budget-analysis",
|
| 71 |
+
"content": "Market analysts view the budget positively, expecting GDP growth of 7% in FY26. "
|
| 72 |
+
"FDI reforms could attract $100 billion in investments.",
|
| 73 |
+
},
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
query = "What are the highlights of the 2025 Union Budget?"
|
| 77 |
+
print(f"Query: \"{query}\"")
|
| 78 |
+
print(f"Context: {len(mock_results)} search results ({sum(1 for r in mock_results if '.gov.in' in r['url'])} .gov.in)\n")
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
result = await synthesize_response(query, mock_results)
|
| 82 |
+
print(f"Summary:\n{result.get('summary', 'N/A')}\n")
|
| 83 |
+
print(f"Citations ({len(result.get('citations', []))}):")
|
| 84 |
+
for c in result.get("citations", []):
|
| 85 |
+
gov_tag = " 🏛️ GOV.IN" if c.get("is_gov") else ""
|
| 86 |
+
print(f" [{c.get('index')}] {c.get('title', 'N/A')} — {c.get('url', 'N/A')}{gov_tag}")
|
| 87 |
+
print("\nStatus: ✅ PASS")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f"Status: ❌ FAIL — {e}")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
async def test_synthesizer_empty():
|
| 93 |
+
"""Test the Synthesizer with no results."""
|
| 94 |
+
print_section("TEST 3: Synthesizer — Empty Results")
|
| 95 |
+
|
| 96 |
+
result = await synthesize_response("test query", [])
|
| 97 |
+
print(f"Summary: {result.get('summary', 'N/A')}")
|
| 98 |
+
assert "no search results" in result["summary"].lower() or "No search results" in result["summary"]
|
| 99 |
+
print("Status: ✅ PASS")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
async def main():
|
| 103 |
+
settings = get_settings()
|
| 104 |
+
print(f"Sarvam API Base: {settings.SARVAM_API_BASE}")
|
| 105 |
+
print(f"Router Model: {settings.SARVAM_ROUTER_MODEL}")
|
| 106 |
+
print(f"Synth Model: {settings.SARVAM_SYNTH_MODEL}")
|
| 107 |
+
print(f"API Key: {'***' + settings.SARVAM_API_KEY[-4:] if settings.SARVAM_API_KEY else 'NOT SET'}")
|
| 108 |
+
|
| 109 |
+
if not settings.SARVAM_API_KEY or settings.SARVAM_API_KEY == "your_sarvam_api_key_here":
|
| 110 |
+
print("\n⚠️ SARVAM_API_KEY is not configured.")
|
| 111 |
+
print(" Set it in backend/.env to run live tests.")
|
| 112 |
+
print(" Running offline tests only...\n")
|
| 113 |
+
await test_synthesizer_empty()
|
| 114 |
+
return
|
| 115 |
+
|
| 116 |
+
await test_router()
|
| 117 |
+
await test_synthesizer()
|
| 118 |
+
await test_synthesizer_empty()
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
if __name__ == "__main__":
|
| 122 |
+
asyncio.run(main())
|
backend/test_search.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Anvesha AI - Week 1 Heartbeat Test
|
| 3 |
+
===================================
|
| 4 |
+
Tests our SearxNG integration and validates Indian source retrieval.
|
| 5 |
+
"""
|
| 6 |
+
import httpx
|
| 7 |
+
import json
|
| 8 |
+
import re
|
| 9 |
+
import sys
|
| 10 |
+
from html import unescape
|
| 11 |
+
|
| 12 |
+
def test_local_searxng():
|
| 13 |
+
"""Test local SearxNG Docker instance."""
|
| 14 |
+
url = "http://localhost:8080/search"
|
| 15 |
+
params = {"q": "India government digital scheme", "format": "json"}
|
| 16 |
+
|
| 17 |
+
print("=" * 60)
|
| 18 |
+
print("STEP 1: Testing Local SearxNG Docker Instance")
|
| 19 |
+
print("=" * 60)
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
response = httpx.get(url, params=params, timeout=15.0)
|
| 23 |
+
response.raise_for_status()
|
| 24 |
+
data = response.json()
|
| 25 |
+
results = data.get("results", [])
|
| 26 |
+
unresponsive = data.get("unresponsive_engines", [])
|
| 27 |
+
|
| 28 |
+
if results:
|
| 29 |
+
print(f"SUCCESS: Local SearxNG returned {len(results)} results!")
|
| 30 |
+
return data
|
| 31 |
+
else:
|
| 32 |
+
print(f"Local SearxNG returned 0 results.")
|
| 33 |
+
if unresponsive:
|
| 34 |
+
print(f" Unresponsive engines: {[e[0] for e in unresponsive]}")
|
| 35 |
+
print(f" Docker container cannot reach external search engines.")
|
| 36 |
+
print(f" Fix: Restart Docker Desktop / reset WSL2 network.")
|
| 37 |
+
return None
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"Local SearxNG unreachable: {e}")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_host_search():
|
| 44 |
+
"""Direct search from host machine as fallback proof-of-concept."""
|
| 45 |
+
print("\n" + "=" * 60)
|
| 46 |
+
print("STEP 2: Direct Host Search (Proof-of-Concept Fallback)")
|
| 47 |
+
print("=" * 60)
|
| 48 |
+
print("Since Docker NAT is broken, searching from host directly...")
|
| 49 |
+
|
| 50 |
+
url = "https://html.duckduckgo.com/html/"
|
| 51 |
+
params = {"q": "India government digital scheme site:gov.in OR site:nic.in"}
|
| 52 |
+
headers = {
|
| 53 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
response = httpx.post(url, data=params, headers=headers, timeout=15.0, follow_redirects=True)
|
| 58 |
+
response.raise_for_status()
|
| 59 |
+
html = response.text
|
| 60 |
+
|
| 61 |
+
# Parse results from DDG HTML
|
| 62 |
+
results = []
|
| 63 |
+
# Extract result blocks
|
| 64 |
+
result_blocks = re.findall(
|
| 65 |
+
r'<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)</a>.*?'
|
| 66 |
+
r'(?:<a[^>]+class="result__snippet"[^>]*>(.*?)</a>)?',
|
| 67 |
+
html, re.DOTALL
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
if not result_blocks:
|
| 71 |
+
# Try alternate pattern
|
| 72 |
+
links = re.findall(r'<a[^>]+rel="nofollow"[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)</a>', html, re.DOTALL)
|
| 73 |
+
snippets = re.findall(r'class="result__snippet"[^>]*>(.*?)</(?:a|span)>', html, re.DOTALL)
|
| 74 |
+
|
| 75 |
+
for i, (link, title) in enumerate(links):
|
| 76 |
+
snippet = unescape(re.sub(r'<[^>]+>', '', snippets[i])).strip() if i < len(snippets) else ""
|
| 77 |
+
clean_title = unescape(re.sub(r'<[^>]+>', '', title)).strip()
|
| 78 |
+
results.append({
|
| 79 |
+
"title": clean_title,
|
| 80 |
+
"url": link,
|
| 81 |
+
"content": snippet,
|
| 82 |
+
"engine": "duckduckgo_html"
|
| 83 |
+
})
|
| 84 |
+
else:
|
| 85 |
+
for link, title, snippet in result_blocks:
|
| 86 |
+
clean_title = unescape(re.sub(r'<[^>]+>', '', title)).strip()
|
| 87 |
+
clean_snippet = unescape(re.sub(r'<[^>]+>', '', snippet)).strip() if snippet else ""
|
| 88 |
+
results.append({
|
| 89 |
+
"title": clean_title,
|
| 90 |
+
"url": link,
|
| 91 |
+
"content": clean_snippet,
|
| 92 |
+
"engine": "duckduckgo_html"
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
if results:
|
| 96 |
+
print(f"SUCCESS: Found {len(results)} results from host search!")
|
| 97 |
+
return {"results": results, "query": params["q"]}
|
| 98 |
+
else:
|
| 99 |
+
print("Could not parse HTML results. Trying alternate extraction...")
|
| 100 |
+
|
| 101 |
+
# Last resort: extract any gov.in links
|
| 102 |
+
gov_links = re.findall(r'href="(https?://[^"]*\.gov\.in[^"]*)"', html)
|
| 103 |
+
nic_links = re.findall(r'href="(https?://[^"]*\.nic\.in[^"]*)"', html)
|
| 104 |
+
all_links = gov_links + nic_links
|
| 105 |
+
|
| 106 |
+
if all_links:
|
| 107 |
+
results = [{"title": f"Indian Government Source", "url": link, "content": "", "engine": "duckduckgo_html"} for link in all_links[:10]]
|
| 108 |
+
print(f"Found {len(results)} .gov.in/.nic.in links!")
|
| 109 |
+
return {"results": results, "query": params["q"]}
|
| 110 |
+
|
| 111 |
+
# Save raw HTML for debugging
|
| 112 |
+
with open("debug_ddg_response.html", "w", encoding="utf-8") as f:
|
| 113 |
+
f.write(html)
|
| 114 |
+
print("Saved raw HTML to debug_ddg_response.html for inspection.")
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"Error: {e}")
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def analyze_and_print(data):
|
| 123 |
+
"""Print raw JSON and analyze for Indian sources."""
|
| 124 |
+
results = data.get("results", [])
|
| 125 |
+
|
| 126 |
+
print(f"\n{'='*60}")
|
| 127 |
+
print(f"RAW JSON OUTPUT ({len(results)} results)")
|
| 128 |
+
print(f"{'='*60}")
|
| 129 |
+
print(json.dumps(results[:5], indent=2, ensure_ascii=False))
|
| 130 |
+
|
| 131 |
+
# Analyze for Indian sources
|
| 132 |
+
indian_gov = []
|
| 133 |
+
indian_content = []
|
| 134 |
+
|
| 135 |
+
for r in results:
|
| 136 |
+
url_str = r.get("url", "")
|
| 137 |
+
title_str = (r.get("title") or "").lower()
|
| 138 |
+
content_str = (r.get("content") or "").lower()
|
| 139 |
+
|
| 140 |
+
if any(d in url_str for d in [".gov.in", ".nic.in", "india.gov"]):
|
| 141 |
+
indian_gov.append(r)
|
| 142 |
+
elif any(kw in title_str or kw in content_str for kw in
|
| 143 |
+
["india", "bharat", "digital india", "modi", "aadhaar", "upi"]):
|
| 144 |
+
indian_content.append(r)
|
| 145 |
+
|
| 146 |
+
print(f"\n{'='*60}")
|
| 147 |
+
print(f"INDIAN SOURCE ANALYSIS")
|
| 148 |
+
print(f"{'='*60}")
|
| 149 |
+
|
| 150 |
+
if indian_gov:
|
| 151 |
+
print(f"\n Government Sources (.gov.in / .nic.in): {len(indian_gov)}")
|
| 152 |
+
for s in indian_gov:
|
| 153 |
+
print(f" [GOV.IN] {s.get('title')}")
|
| 154 |
+
print(f" URL: {s.get('url')}")
|
| 155 |
+
if s.get("content"):
|
| 156 |
+
print(f" Snippet: {s['content'][:150]}...")
|
| 157 |
+
print()
|
| 158 |
+
|
| 159 |
+
if indian_content:
|
| 160 |
+
print(f"\n India-Related Content: {len(indian_content)}")
|
| 161 |
+
for s in indian_content[:5]:
|
| 162 |
+
print(f" [INDIA] {s.get('title')}")
|
| 163 |
+
print(f" URL: {s.get('url')}")
|
| 164 |
+
if s.get("content"):
|
| 165 |
+
print(f" Snippet: {s['content'][:150]}...")
|
| 166 |
+
print()
|
| 167 |
+
|
| 168 |
+
total = len(indian_gov) + len(indian_content)
|
| 169 |
+
|
| 170 |
+
print(f"\n{'='*60}")
|
| 171 |
+
print(f"WEEK 1 HEARTBEAT RESULT")
|
| 172 |
+
print(f"{'='*60}")
|
| 173 |
+
if indian_gov:
|
| 174 |
+
print(f" [PASS] {len(indian_gov)} Indian Government source(s) (.gov.in) found!")
|
| 175 |
+
print(f" [PASS] Anvesha AI can pull Indian .gov.in sources from the web.")
|
| 176 |
+
print(f" [PASS] The 'Sutra of Information' pipeline is VALIDATED.")
|
| 177 |
+
elif total > 0:
|
| 178 |
+
print(f" [PASS] {total} India-related source(s) found!")
|
| 179 |
+
print(f" [PASS] Pipeline works. .gov.in sources will surface once")
|
| 180 |
+
print(f" local SearxNG (with default_region: in-en) is online.")
|
| 181 |
+
else:
|
| 182 |
+
print(f" [PARTIAL] Search pipeline works but no .gov.in in top results.")
|
| 183 |
+
|
| 184 |
+
return total > 0
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
print()
|
| 189 |
+
print(" ANVESHA AI - Week 1 Heartbeat Test")
|
| 190 |
+
print(" Sovereign Indian Search Engine")
|
| 191 |
+
print()
|
| 192 |
+
|
| 193 |
+
# Try local SearxNG first
|
| 194 |
+
data = test_local_searxng()
|
| 195 |
+
|
| 196 |
+
# Fallback to direct host search
|
| 197 |
+
if not data:
|
| 198 |
+
data = test_host_search()
|
| 199 |
+
|
| 200 |
+
if data:
|
| 201 |
+
success = analyze_and_print(data)
|
| 202 |
+
sys.exit(0)
|
| 203 |
+
else:
|
| 204 |
+
print("\nAll search methods failed.")
|
| 205 |
+
sys.exit(1)
|
docker-compose.prod.yml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anvesha AI — Production Docker Compose
|
| 2 |
+
# Usage: docker compose -f docker-compose.prod.yml up --build -d
|
| 3 |
+
|
| 4 |
+
services:
|
| 5 |
+
# ── SearxNG Search Engine ────────────────────────────────────
|
| 6 |
+
searxng:
|
| 7 |
+
container_name: anvesha-searxng
|
| 8 |
+
image: searxng/searxng:latest
|
| 9 |
+
ports:
|
| 10 |
+
- "8080:8080"
|
| 11 |
+
volumes:
|
| 12 |
+
- ./searxng:/etc/searxng
|
| 13 |
+
environment:
|
| 14 |
+
- SEARXNG_BASE_URL=http://localhost:8080/
|
| 15 |
+
- SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY:-anvesha_prod_secret}
|
| 16 |
+
restart: unless-stopped
|
| 17 |
+
networks:
|
| 18 |
+
- anvesha-net
|
| 19 |
+
healthcheck:
|
| 20 |
+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/"]
|
| 21 |
+
interval: 30s
|
| 22 |
+
timeout: 10s
|
| 23 |
+
retries: 3
|
| 24 |
+
|
| 25 |
+
# ── FastAPI Backend ──────────────────────────────────────────
|
| 26 |
+
backend:
|
| 27 |
+
container_name: anvesha-backend
|
| 28 |
+
build:
|
| 29 |
+
context: ./backend
|
| 30 |
+
dockerfile: Dockerfile
|
| 31 |
+
ports:
|
| 32 |
+
- "8000:8000"
|
| 33 |
+
env_file:
|
| 34 |
+
- ./backend/.env
|
| 35 |
+
environment:
|
| 36 |
+
- SEARXNG_BASE_URL=http://searxng:8080
|
| 37 |
+
- FRONTEND_URL=http://frontend:3000
|
| 38 |
+
depends_on:
|
| 39 |
+
searxng:
|
| 40 |
+
condition: service_healthy
|
| 41 |
+
restart: unless-stopped
|
| 42 |
+
networks:
|
| 43 |
+
- anvesha-net
|
| 44 |
+
|
| 45 |
+
# ── Next.js Frontend ─────────────────────────────────────────
|
| 46 |
+
frontend:
|
| 47 |
+
container_name: anvesha-frontend
|
| 48 |
+
build:
|
| 49 |
+
context: ./frontend
|
| 50 |
+
dockerfile: Dockerfile
|
| 51 |
+
ports:
|
| 52 |
+
- "3000:3000"
|
| 53 |
+
environment:
|
| 54 |
+
- BACKEND_URL=http://backend:8000
|
| 55 |
+
depends_on:
|
| 56 |
+
- backend
|
| 57 |
+
restart: unless-stopped
|
| 58 |
+
networks:
|
| 59 |
+
- anvesha-net
|
| 60 |
+
|
| 61 |
+
networks:
|
| 62 |
+
anvesha-net:
|
| 63 |
+
driver: bridge
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
searxng:
|
| 3 |
+
container_name: anvesha-searxng
|
| 4 |
+
image: searxng/searxng:latest
|
| 5 |
+
ports:
|
| 6 |
+
- "8080:8080"
|
| 7 |
+
volumes:
|
| 8 |
+
- ./searxng:/etc/searxng
|
| 9 |
+
environment:
|
| 10 |
+
- SEARXNG_BASE_URL=http://localhost:8080/
|
| 11 |
+
- SEARXNG_SECRET_KEY=anvesha_secret_key_12345
|
| 12 |
+
restart: unless-stopped
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
frontend/Dockerfile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ── Stage 1: Dependencies ──────────────────────────────────────
|
| 2 |
+
FROM node:20-alpine AS deps
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
COPY package.json package-lock.json ./
|
| 5 |
+
RUN npm ci --production=false
|
| 6 |
+
|
| 7 |
+
# ── Stage 2: Build ─────────────────────────────────────────────
|
| 8 |
+
FROM node:20-alpine AS builder
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 11 |
+
COPY . .
|
| 12 |
+
|
| 13 |
+
# Set build-time env vars
|
| 14 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 15 |
+
ENV BACKEND_URL=http://backend:8000
|
| 16 |
+
|
| 17 |
+
RUN npm run build
|
| 18 |
+
|
| 19 |
+
# ── Stage 3: Production ───────────────────────────────────────
|
| 20 |
+
FROM node:20-alpine AS runner
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
ENV NODE_ENV=production
|
| 24 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 25 |
+
ENV BACKEND_URL=http://backend:8000
|
| 26 |
+
|
| 27 |
+
# Create non-root user
|
| 28 |
+
RUN addgroup --system --gid 1001 nodejs && \
|
| 29 |
+
adduser --system --uid 1001 nextjs
|
| 30 |
+
|
| 31 |
+
# Copy built assets
|
| 32 |
+
COPY --from=builder /app/public ./public
|
| 33 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
| 34 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
| 35 |
+
|
| 36 |
+
USER nextjs
|
| 37 |
+
EXPOSE 3000
|
| 38 |
+
|
| 39 |
+
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
| 40 |
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
|
| 41 |
+
|
| 42 |
+
CMD ["node", "server.js"]
|
frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
frontend/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
frontend/next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
/* config options here */
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"next": "16.1.6",
|
| 13 |
+
"react": "19.2.3",
|
| 14 |
+
"react-dom": "19.2.3"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@tailwindcss/postcss": "^4",
|
| 18 |
+
"@types/node": "^20",
|
| 19 |
+
"@types/react": "^19",
|
| 20 |
+
"@types/react-dom": "^19",
|
| 21 |
+
"eslint": "^9",
|
| 22 |
+
"eslint-config-next": "16.1.6",
|
| 23 |
+
"tailwindcss": "^4",
|
| 24 |
+
"typescript": "^5"
|
| 25 |
+
}
|
| 26 |
+
}
|
frontend/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
frontend/public/file.svg
ADDED
|
|
frontend/public/globe.svg
ADDED
|
|
frontend/public/next.svg
ADDED
|
|
frontend/public/vercel.svg
ADDED
|
|
frontend/public/window.svg
ADDED
|
|
frontend/src/app/api/search/route.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
|
| 4 |
+
|
| 5 |
+
export async function GET(request: NextRequest) {
|
| 6 |
+
const { searchParams } = new URL(request.url);
|
| 7 |
+
const q = searchParams.get("q");
|
| 8 |
+
const region = searchParams.get("region") || "in-en";
|
| 9 |
+
|
| 10 |
+
if (!q) {
|
| 11 |
+
return NextResponse.json(
|
| 12 |
+
{ error: "Query parameter 'q' is required" },
|
| 13 |
+
{ status: 400 }
|
| 14 |
+
);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
const backendUrl = `${BACKEND_URL}/search?q=${encodeURIComponent(q)}®ion=${encodeURIComponent(region)}`;
|
| 19 |
+
const response = await fetch(backendUrl, {
|
| 20 |
+
headers: { "Content-Type": "application/json" },
|
| 21 |
+
signal: AbortSignal.timeout(15000),
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
if (!response.ok) {
|
| 25 |
+
return NextResponse.json(
|
| 26 |
+
{ error: `Backend returned ${response.status}` },
|
| 27 |
+
{ status: response.status }
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const data = await response.json();
|
| 32 |
+
return NextResponse.json(data);
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error("Search proxy error:", error);
|
| 35 |
+
return NextResponse.json(
|
| 36 |
+
{ error: "Failed to reach the backend search service. Is it running?" },
|
| 37 |
+
{ status: 502 }
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
}
|
frontend/src/app/api/text-to-voice/route.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
|
| 4 |
+
|
| 5 |
+
export async function POST(request: NextRequest) {
|
| 6 |
+
try {
|
| 7 |
+
const body = await request.json();
|
| 8 |
+
|
| 9 |
+
const response = await fetch(`${BACKEND_URL}/text-to-voice`, {
|
| 10 |
+
method: "POST",
|
| 11 |
+
headers: { "Content-Type": "application/json" },
|
| 12 |
+
body: JSON.stringify(body),
|
| 13 |
+
signal: AbortSignal.timeout(30000),
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
if (!response.ok) {
|
| 17 |
+
return NextResponse.json(
|
| 18 |
+
{ error: `Backend returned ${response.status}` },
|
| 19 |
+
{ status: response.status }
|
| 20 |
+
);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const data = await response.json();
|
| 24 |
+
return NextResponse.json(data);
|
| 25 |
+
} catch (error) {
|
| 26 |
+
console.error("Text-to-voice proxy error:", error);
|
| 27 |
+
return NextResponse.json(
|
| 28 |
+
{ error: "Failed to reach the voice service." },
|
| 29 |
+
{ status: 502 }
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
}
|
frontend/src/app/api/voice-to-text/route.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
|
| 4 |
+
|
| 5 |
+
export async function POST(request: NextRequest) {
|
| 6 |
+
try {
|
| 7 |
+
const formData = await request.formData();
|
| 8 |
+
const file = formData.get("file") as File | null;
|
| 9 |
+
const language = formData.get("language") as string || "en-IN";
|
| 10 |
+
|
| 11 |
+
if (!file) {
|
| 12 |
+
return NextResponse.json(
|
| 13 |
+
{ error: "No audio file provided" },
|
| 14 |
+
{ status: 400 }
|
| 15 |
+
);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Forward as multipart to backend
|
| 19 |
+
const backendForm = new FormData();
|
| 20 |
+
backendForm.append("file", file);
|
| 21 |
+
|
| 22 |
+
const response = await fetch(
|
| 23 |
+
`${BACKEND_URL}/voice-to-text?language=${encodeURIComponent(language)}`,
|
| 24 |
+
{
|
| 25 |
+
method: "POST",
|
| 26 |
+
body: backendForm,
|
| 27 |
+
signal: AbortSignal.timeout(30000),
|
| 28 |
+
}
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
if (!response.ok) {
|
| 32 |
+
return NextResponse.json(
|
| 33 |
+
{ error: `Backend returned ${response.status}` },
|
| 34 |
+
{ status: response.status }
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const data = await response.json();
|
| 39 |
+
return NextResponse.json(data);
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error("Voice-to-text proxy error:", error);
|
| 42 |
+
return NextResponse.json(
|
| 43 |
+
{ error: "Failed to reach the voice service." },
|
| 44 |
+
{ status: 502 }
|
| 45 |
+
);
|
| 46 |
+
}
|
| 47 |
+
}
|
frontend/src/app/favicon.ico
ADDED
|
|
frontend/src/app/globals.css
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
@theme inline {
|
| 4 |
+
--color-primary: #D4AF37;
|
| 5 |
+
--color-charcoal: #36454F;
|
| 6 |
+
--color-background-light: #f8f7f6;
|
| 7 |
+
--color-background-dark: #201d12;
|
| 8 |
+
--font-display: 'Inter', sans-serif;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
:root {
|
| 12 |
+
--background: #f8f7f6;
|
| 13 |
+
--foreground: #36454F;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
@media (prefers-color-scheme: dark) {
|
| 17 |
+
:root {
|
| 18 |
+
--background: #201d12;
|
| 19 |
+
--foreground: #f1f5f9;
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
background: var(--background);
|
| 25 |
+
color: var(--foreground);
|
| 26 |
+
font-family: 'Inter', sans-serif;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Sutra Citation line gradient */
|
| 30 |
+
.sutra-line {
|
| 31 |
+
height: 1px;
|
| 32 |
+
background: linear-gradient(90deg, #D4AF37 0%, rgba(212, 175, 55, 0) 100%);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Material Symbols config */
|
| 36 |
+
.material-symbols-outlined {
|
| 37 |
+
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Smooth scrolling */
|
| 41 |
+
html {
|
| 42 |
+
scroll-behavior: smooth;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Custom scrollbar */
|
| 46 |
+
::-webkit-scrollbar {
|
| 47 |
+
width: 6px;
|
| 48 |
+
}
|
| 49 |
+
::-webkit-scrollbar-track {
|
| 50 |
+
background: transparent;
|
| 51 |
+
}
|
| 52 |
+
::-webkit-scrollbar-thumb {
|
| 53 |
+
background: rgba(212, 175, 55, 0.3);
|
| 54 |
+
border-radius: 3px;
|
| 55 |
+
}
|
| 56 |
+
::-webkit-scrollbar-thumb:hover {
|
| 57 |
+
background: rgba(212, 175, 55, 0.5);
|
| 58 |
+
}
|
frontend/src/app/layout.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Inter } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const inter = Inter({
|
| 6 |
+
variable: "--font-inter",
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
weight: ["300", "400", "500", "600", "700", "800", "900"],
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
export const metadata: Metadata = {
|
| 12 |
+
title: "Anvesha AI | The Sutra of Information",
|
| 13 |
+
description:
|
| 14 |
+
"Experience sovereign Indian LLMs designed for professional insight, deep reasoning, and cultural precision. Priority access to .gov.in sources and verified citations.",
|
| 15 |
+
keywords: ["Anvesha AI", "Indian search engine", "sovereign LLM", "gov.in citations", "Indian intelligence"],
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
export default function RootLayout({
|
| 19 |
+
children,
|
| 20 |
+
}: Readonly<{
|
| 21 |
+
children: React.ReactNode;
|
| 22 |
+
}>) {
|
| 23 |
+
return (
|
| 24 |
+
<html lang="en">
|
| 25 |
+
<head>
|
| 26 |
+
<link
|
| 27 |
+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
| 28 |
+
rel="stylesheet"
|
| 29 |
+
/>
|
| 30 |
+
</head>
|
| 31 |
+
<body className={`${inter.variable} antialiased font-[family-name:var(--font-inter)]`}>
|
| 32 |
+
{children}
|
| 33 |
+
</body>
|
| 34 |
+
</html>
|
| 35 |
+
);
|
| 36 |
+
}
|
frontend/src/app/page.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Header from "@/components/Header";
|
| 2 |
+
import HeroSection from "@/components/HeroSection";
|
| 3 |
+
import PhilosophySection from "@/components/PhilosophySection";
|
| 4 |
+
import FeaturesSection from "@/components/FeaturesSection";
|
| 5 |
+
import CTASection from "@/components/CTASection";
|
| 6 |
+
import Footer from "@/components/Footer";
|
| 7 |
+
|
| 8 |
+
export default function Home() {
|
| 9 |
+
return (
|
| 10 |
+
<div className="relative flex min-h-screen flex-col overflow-x-hidden bg-background-light text-charcoal">
|
| 11 |
+
<Header />
|
| 12 |
+
<main className="flex-1">
|
| 13 |
+
<HeroSection />
|
| 14 |
+
<PhilosophySection />
|
| 15 |
+
<FeaturesSection />
|
| 16 |
+
<CTASection />
|
| 17 |
+
</main>
|
| 18 |
+
<Footer />
|
| 19 |
+
</div>
|
| 20 |
+
);
|
| 21 |
+
}
|
frontend/src/app/search/page.tsx
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useCallback } from "react";
|
| 4 |
+
import SearchSidebar from "@/components/SearchSidebar";
|
| 5 |
+
import SearchBar from "@/components/SearchBar";
|
| 6 |
+
import SutraCard from "@/components/SutraCard";
|
| 7 |
+
import SuggestionPills from "@/components/SuggestionPills";
|
| 8 |
+
|
| 9 |
+
interface SearchResult {
|
| 10 |
+
title: string;
|
| 11 |
+
content: string;
|
| 12 |
+
url: string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
interface SearchResponse {
|
| 16 |
+
results?: SearchResult[];
|
| 17 |
+
error?: string;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default function SearchPage() {
|
| 21 |
+
const [results, setResults] = useState<SearchResult[]>([]);
|
| 22 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 23 |
+
const [error, setError] = useState<string | null>(null);
|
| 24 |
+
const [hasSearched, setHasSearched] = useState(false);
|
| 25 |
+
const [resultCount, setResultCount] = useState(0);
|
| 26 |
+
|
| 27 |
+
const handleSearch = useCallback(async (query: string) => {
|
| 28 |
+
setIsLoading(true);
|
| 29 |
+
setError(null);
|
| 30 |
+
setHasSearched(true);
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
| 34 |
+
const data: SearchResponse = await res.json();
|
| 35 |
+
|
| 36 |
+
if (data.error) {
|
| 37 |
+
setError(data.error);
|
| 38 |
+
setResults([]);
|
| 39 |
+
setResultCount(0);
|
| 40 |
+
} else {
|
| 41 |
+
const searchResults = data.results || [];
|
| 42 |
+
setResults(searchResults);
|
| 43 |
+
setResultCount(searchResults.length);
|
| 44 |
+
}
|
| 45 |
+
} catch (err) {
|
| 46 |
+
setError("Failed to connect to the search service. Please try again.");
|
| 47 |
+
setResults([]);
|
| 48 |
+
setResultCount(0);
|
| 49 |
+
console.error("Search error:", err);
|
| 50 |
+
} finally {
|
| 51 |
+
setIsLoading(false);
|
| 52 |
+
}
|
| 53 |
+
}, []);
|
| 54 |
+
|
| 55 |
+
const govInCount = results.filter((r) => r.url?.includes(".gov.in")).length;
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<div className="flex h-screen overflow-hidden bg-background-light text-charcoal">
|
| 59 |
+
<SearchSidebar />
|
| 60 |
+
|
| 61 |
+
<main className="flex-1 flex flex-col min-w-0 relative overflow-y-auto">
|
| 62 |
+
{/* Search Header */}
|
| 63 |
+
<header className="sticky top-0 z-10 p-6 md:px-12 flex flex-col items-center bg-background-light/80 backdrop-blur-md">
|
| 64 |
+
<SearchBar onSearch={handleSearch} isLoading={isLoading} />
|
| 65 |
+
</header>
|
| 66 |
+
|
| 67 |
+
{/* Results Section */}
|
| 68 |
+
<section className="max-w-4xl mx-auto w-full p-6 md:p-12 space-y-8">
|
| 69 |
+
{hasSearched && !isLoading && !error && (
|
| 70 |
+
<div className="flex items-center justify-between mb-2">
|
| 71 |
+
<h2 className="text-sm font-semibold uppercase tracking-widest text-primary/70">
|
| 72 |
+
Top Insights
|
| 73 |
+
</h2>
|
| 74 |
+
<span className="text-xs text-charcoal/40">
|
| 75 |
+
Found {resultCount} relevant citation{resultCount !== 1 ? "s" : ""}
|
| 76 |
+
{govInCount > 0 && (
|
| 77 |
+
<span className="text-primary ml-1">
|
| 78 |
+
({govInCount} .gov.in)
|
| 79 |
+
</span>
|
| 80 |
+
)}
|
| 81 |
+
</span>
|
| 82 |
+
</div>
|
| 83 |
+
)}
|
| 84 |
+
|
| 85 |
+
{/* Loading State */}
|
| 86 |
+
{isLoading && (
|
| 87 |
+
<div className="flex flex-col items-center gap-6 py-20">
|
| 88 |
+
<div className="h-12 w-12 rounded-full border-4 border-primary/20 border-t-primary animate-spin" />
|
| 89 |
+
<p className="text-charcoal/50 text-sm font-medium">
|
| 90 |
+
Searching the sutras...
|
| 91 |
+
</p>
|
| 92 |
+
</div>
|
| 93 |
+
)}
|
| 94 |
+
|
| 95 |
+
{/* Error State */}
|
| 96 |
+
{error && (
|
| 97 |
+
<div className="bg-red-50 border border-red-200 text-red-700 rounded-2xl p-8 text-center">
|
| 98 |
+
<span className="material-symbols-outlined text-3xl mb-2 block">error</span>
|
| 99 |
+
<p className="font-medium">{error}</p>
|
| 100 |
+
<p className="text-sm mt-2 text-red-500">
|
| 101 |
+
Please ensure Docker and the backend are running.
|
| 102 |
+
</p>
|
| 103 |
+
</div>
|
| 104 |
+
)}
|
| 105 |
+
|
| 106 |
+
{/* Empty State */}
|
| 107 |
+
{!isLoading && !error && !hasSearched && (
|
| 108 |
+
<div className="flex flex-col items-center gap-6 py-20 text-center">
|
| 109 |
+
<div className="h-20 w-20 rounded-full bg-primary/10 flex items-center justify-center">
|
| 110 |
+
<span className="material-symbols-outlined text-4xl text-primary">
|
| 111 |
+
auto_awesome
|
| 112 |
+
</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div>
|
| 115 |
+
<h3 className="text-2xl font-bold text-charcoal mb-2">
|
| 116 |
+
Begin your inquiry
|
| 117 |
+
</h3>
|
| 118 |
+
<p className="text-charcoal/50 max-w-md">
|
| 119 |
+
Ask any question to receive sovereign intelligence with
|
| 120 |
+
verified .gov.in citations and deep reasoning.
|
| 121 |
+
</p>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
)}
|
| 125 |
+
|
| 126 |
+
{/* No Results */}
|
| 127 |
+
{!isLoading && !error && hasSearched && results.length === 0 && (
|
| 128 |
+
<div className="flex flex-col items-center gap-4 py-16 text-center">
|
| 129 |
+
<span className="material-symbols-outlined text-5xl text-charcoal/20">
|
| 130 |
+
search_off
|
| 131 |
+
</span>
|
| 132 |
+
<p className="text-charcoal/50">No results found. Try a different query.</p>
|
| 133 |
+
</div>
|
| 134 |
+
)}
|
| 135 |
+
|
| 136 |
+
{/* Results */}
|
| 137 |
+
{!isLoading &&
|
| 138 |
+
results.map((result, index) => (
|
| 139 |
+
<SutraCard
|
| 140 |
+
key={`${result.url}-${index}`}
|
| 141 |
+
title={result.title}
|
| 142 |
+
content={result.content}
|
| 143 |
+
url={result.url}
|
| 144 |
+
citationIndex={index + 1}
|
| 145 |
+
/>
|
| 146 |
+
))}
|
| 147 |
+
</section>
|
| 148 |
+
|
| 149 |
+
{/* Suggestion Pills */}
|
| 150 |
+
{!isLoading && (
|
| 151 |
+
<SuggestionPills onSuggestionClick={handleSearch} />
|
| 152 |
+
)}
|
| 153 |
+
</main>
|
| 154 |
+
</div>
|
| 155 |
+
);
|
| 156 |
+
}
|
frontend/src/components/CTASection.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
|
| 3 |
+
export default function CTASection() {
|
| 4 |
+
return (
|
| 5 |
+
<section className="px-6 py-20 lg:px-12">
|
| 6 |
+
<div className="mx-auto max-w-5xl overflow-hidden rounded-3xl bg-charcoal p-12 lg:p-20 text-center relative">
|
| 7 |
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-transparent pointer-events-none" />
|
| 8 |
+
<div className="relative z-10 flex flex-col items-center gap-8">
|
| 9 |
+
<h2 className="text-3xl font-black text-white md:text-5xl">
|
| 10 |
+
Ready to unlock sovereign intelligence?
|
| 11 |
+
</h2>
|
| 12 |
+
<p className="max-w-2xl text-lg text-slate-300">
|
| 13 |
+
Join the leading organizations leveraging India's most advanced
|
| 14 |
+
reasoning engine for a smarter, more secure future.
|
| 15 |
+
</p>
|
| 16 |
+
<Link
|
| 17 |
+
href="/search"
|
| 18 |
+
className="rounded-lg bg-primary px-10 py-4 text-lg font-bold text-background-dark transition-all hover:scale-105 active:scale-95"
|
| 19 |
+
>
|
| 20 |
+
Explore Now
|
| 21 |
+
</Link>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
</section>
|
| 25 |
+
);
|
| 26 |
+
}
|
frontend/src/components/FeaturesSection.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const features = [
|
| 2 |
+
{
|
| 3 |
+
icon: "shield_person",
|
| 4 |
+
title: "Sovereign Intelligence",
|
| 5 |
+
description:
|
| 6 |
+
"Built on Sarvam 105B for high-performance localized reasoning that respects data sovereignty and cultural context.",
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
icon: "verified",
|
| 10 |
+
title: "Citations First",
|
| 11 |
+
description:
|
| 12 |
+
"Priority access to .gov.in sources and primary legal documents for verified, trustworthy, and hallucination-free information.",
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
icon: "psychology_alt",
|
| 16 |
+
title: "Multimodal Freedom",
|
| 17 |
+
description:
|
| 18 |
+
"Seamlessly interact through Saaras Voice and Sarvam Vision, enabling natural language communication across Indian dialects.",
|
| 19 |
+
},
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
export default function FeaturesSection() {
|
| 23 |
+
return (
|
| 24 |
+
<section id="features" className="px-6 py-24 lg:px-12">
|
| 25 |
+
<div className="mx-auto max-w-7xl">
|
| 26 |
+
<div className="grid gap-8 md:grid-cols-3">
|
| 27 |
+
{features.map((feature) => (
|
| 28 |
+
<div
|
| 29 |
+
key={feature.title}
|
| 30 |
+
className="group relative flex flex-col gap-6 rounded-2xl border border-primary/10 bg-white p-8 transition-all hover:-translate-y-2 hover:shadow-xl"
|
| 31 |
+
>
|
| 32 |
+
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary text-background-dark">
|
| 33 |
+
<span className="material-symbols-outlined text-3xl">
|
| 34 |
+
{feature.icon}
|
| 35 |
+
</span>
|
| 36 |
+
</div>
|
| 37 |
+
<div className="flex flex-col gap-3">
|
| 38 |
+
<h4 className="text-xl font-bold text-charcoal">
|
| 39 |
+
{feature.title}
|
| 40 |
+
</h4>
|
| 41 |
+
<p className="text-charcoal/70">{feature.description}</p>
|
| 42 |
+
</div>
|
| 43 |
+
<div className="mt-auto pt-4 text-primary font-bold text-sm inline-flex items-center gap-2 cursor-pointer">
|
| 44 |
+
Learn more{" "}
|
| 45 |
+
<span className="material-symbols-outlined text-sm">
|
| 46 |
+
arrow_forward
|
| 47 |
+
</span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
))}
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</section>
|
| 54 |
+
);
|
| 55 |
+
}
|
frontend/src/components/Footer.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function Footer() {
|
| 2 |
+
return (
|
| 3 |
+
<footer className="border-t border-primary/10 bg-background-light px-6 py-12 lg:px-12">
|
| 4 |
+
<div className="mx-auto max-w-7xl">
|
| 5 |
+
<div className="flex flex-col items-center justify-between gap-8 md:flex-row">
|
| 6 |
+
<div className="flex items-center gap-3">
|
| 7 |
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
|
| 8 |
+
<span className="text-sm font-black text-background-dark">A</span>
|
| 9 |
+
</div>
|
| 10 |
+
<span className="text-lg font-bold tracking-tight text-charcoal">
|
| 11 |
+
Anvesha AI
|
| 12 |
+
</span>
|
| 13 |
+
</div>
|
| 14 |
+
<div className="flex flex-wrap justify-center gap-8 text-sm font-medium text-charcoal/60">
|
| 15 |
+
<a className="hover:text-primary transition-colors" href="#">
|
| 16 |
+
Privacy Policy
|
| 17 |
+
</a>
|
| 18 |
+
<a className="hover:text-primary transition-colors" href="#">
|
| 19 |
+
Terms of Service
|
| 20 |
+
</a>
|
| 21 |
+
<a className="hover:text-primary transition-colors" href="#">
|
| 22 |
+
Documentation
|
| 23 |
+
</a>
|
| 24 |
+
<a className="hover:text-primary transition-colors" href="#">
|
| 25 |
+
Contact
|
| 26 |
+
</a>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
<div className="mt-12 text-center text-sm text-charcoal/40">
|
| 30 |
+
© 2024 Anvesha AI. Sovereign intelligence for a new era.
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</footer>
|
| 34 |
+
);
|
| 35 |
+
}
|
frontend/src/components/Header.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
|
| 3 |
+
export default function Header() {
|
| 4 |
+
return (
|
| 5 |
+
<header className="sticky top-0 z-50 w-full border-b border-primary/10 bg-background-light/80 backdrop-blur-md">
|
| 6 |
+
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 lg:px-12">
|
| 7 |
+
<Link href="/" className="flex items-center gap-3">
|
| 8 |
+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary">
|
| 9 |
+
<span className="text-xl font-black text-background-dark">A</span>
|
| 10 |
+
</div>
|
| 11 |
+
<span className="text-xl font-bold tracking-tight text-charcoal">
|
| 12 |
+
Anvesha AI
|
| 13 |
+
</span>
|
| 14 |
+
</Link>
|
| 15 |
+
|
| 16 |
+
<nav className="hidden md:flex items-center gap-8">
|
| 17 |
+
<a className="text-sm font-medium hover:text-primary transition-colors" href="#philosophy">
|
| 18 |
+
Philosophy
|
| 19 |
+
</a>
|
| 20 |
+
<a className="text-sm font-medium hover:text-primary transition-colors" href="#features">
|
| 21 |
+
Intelligence
|
| 22 |
+
</a>
|
| 23 |
+
<a className="text-sm font-medium hover:text-primary transition-colors" href="#features">
|
| 24 |
+
Citations
|
| 25 |
+
</a>
|
| 26 |
+
<a className="text-sm font-medium hover:text-primary transition-colors" href="#features">
|
| 27 |
+
Multimodal
|
| 28 |
+
</a>
|
| 29 |
+
</nav>
|
| 30 |
+
|
| 31 |
+
<div className="flex items-center gap-4">
|
| 32 |
+
<Link
|
| 33 |
+
href="/search"
|
| 34 |
+
className="hidden sm:flex items-center justify-center rounded-lg bg-primary px-6 py-2.5 text-sm font-bold text-background-dark transition-all hover:brightness-110 active:scale-95"
|
| 35 |
+
>
|
| 36 |
+
Explore Now
|
| 37 |
+
</Link>
|
| 38 |
+
<button className="md:hidden p-2 text-charcoal" aria-label="Menu">
|
| 39 |
+
<span className="material-symbols-outlined">menu</span>
|
| 40 |
+
</button>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</header>
|
| 44 |
+
);
|
| 45 |
+
}
|
frontend/src/components/HeroSection.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
|
| 3 |
+
export default function HeroSection() {
|
| 4 |
+
return (
|
| 5 |
+
<section className="relative overflow-hidden px-6 py-20 lg:px-12 lg:py-32">
|
| 6 |
+
<div className="mx-auto max-w-7xl">
|
| 7 |
+
<div className="grid items-center gap-12 lg:grid-cols-2">
|
| 8 |
+
<div className="flex flex-col gap-8">
|
| 9 |
+
<div className="inline-flex w-fit items-center rounded-full bg-primary/10 px-4 py-1 text-xs font-bold uppercase tracking-widest text-primary">
|
| 10 |
+
Sovereign Indian LLM
|
| 11 |
+
</div>
|
| 12 |
+
<h1 className="text-5xl font-black leading-[1.1] tracking-tight text-charcoal md:text-6xl lg:text-7xl">
|
| 13 |
+
The Sutra of{" "}
|
| 14 |
+
<span className="text-primary">Information</span>
|
| 15 |
+
</h1>
|
| 16 |
+
<p className="max-w-xl text-lg leading-relaxed text-charcoal/80 md:text-xl">
|
| 17 |
+
Experience sovereign Indian LLMs designed for professional
|
| 18 |
+
insight, deep reasoning, and cultural precision.
|
| 19 |
+
</p>
|
| 20 |
+
<div className="flex flex-col gap-4 sm:flex-row">
|
| 21 |
+
<Link
|
| 22 |
+
href="/search"
|
| 23 |
+
className="flex items-center justify-center rounded-lg bg-primary px-8 py-4 text-base font-bold text-background-dark transition-all hover:shadow-lg hover:shadow-primary/20 active:scale-95"
|
| 24 |
+
>
|
| 25 |
+
Explore Now
|
| 26 |
+
</Link>
|
| 27 |
+
<button className="flex items-center justify-center rounded-lg border-2 border-charcoal/10 bg-transparent px-8 py-4 text-base font-bold text-charcoal hover:bg-charcoal/5 transition-all">
|
| 28 |
+
Watch Demo
|
| 29 |
+
</button>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div className="relative aspect-square lg:aspect-video rounded-2xl overflow-hidden shadow-2xl border border-primary/20">
|
| 34 |
+
<div className="absolute inset-0 bg-gradient-to-tr from-primary/20 to-transparent mix-blend-overlay z-10" />
|
| 35 |
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
| 36 |
+
<img
|
| 37 |
+
alt="Abstract geometric patterns representing neural networks and sovereign Indian AI"
|
| 38 |
+
className="h-full w-full object-cover"
|
| 39 |
+
src="https://lh3.googleusercontent.com/aida-public/AB6AXuA_6OvMz2fD13HUs1okW8JEeIMt8E-GMbTNmvfYbMco_7Zc07J3UvDNI5_KlQ23hvpim5SnIP_KxBsssF9lKdBIG91e-IldHIU_XSXHvD4_1BacJ97hkgXIU-8h0laSeE4rmIzT60GmuOhxfwoplZWcJ5FHqM1yp0rc-o1j7Ha8lkC8fYG_rUTJH2hsByJpRLwWXcL3gRAVRr9azRRgpLJp6C6t0XyTyq4ajH6CoPWhu6jR0DY8M28VgZuWNQemnx-Tpn7F0kBaNVGp"
|
| 40 |
+
/>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</section>
|
| 45 |
+
);
|
| 46 |
+
}
|
frontend/src/components/PhilosophySection.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function PhilosophySection() {
|
| 2 |
+
return (
|
| 3 |
+
<section id="philosophy" className="bg-white/50 py-24">
|
| 4 |
+
<div className="mx-auto max-w-4xl px-6 text-center lg:px-12">
|
| 5 |
+
<h2 className="mb-6 text-sm font-bold uppercase tracking-[0.3em] text-primary">
|
| 6 |
+
Philosophy
|
| 7 |
+
</h2>
|
| 8 |
+
<h3 className="mb-8 text-4xl font-black text-charcoal md:text-5xl">
|
| 9 |
+
Prajna (Insight)
|
| 10 |
+
</h3>
|
| 11 |
+
<p className="text-xl leading-relaxed text-charcoal/70">
|
| 12 |
+
Prajna is our core philosophy of context-driven deep reasoning.
|
| 13 |
+
Powered by sovereign Indian LLMs, we provide unparalleled accuracy
|
| 14 |
+
and local relevance that understands the nuanced tapestry of Indian
|
| 15 |
+
information ecosystems.
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
</section>
|
| 19 |
+
);
|
| 20 |
+
}
|
frontend/src/components/SearchBar.tsx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useRef, FormEvent, KeyboardEvent } from "react";
|
| 4 |
+
|
| 5 |
+
interface SearchBarProps {
|
| 6 |
+
onSearch: (query: string) => void;
|
| 7 |
+
isLoading?: boolean;
|
| 8 |
+
initialQuery?: string;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function SearchBar({ onSearch, isLoading, initialQuery = "" }: SearchBarProps) {
|
| 12 |
+
const [query, setQuery] = useState(initialQuery);
|
| 13 |
+
const [isRecording, setIsRecording] = useState(false);
|
| 14 |
+
const [isTranscribing, setIsTranscribing] = useState(false);
|
| 15 |
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
| 16 |
+
const chunksRef = useRef<Blob[]>([]);
|
| 17 |
+
|
| 18 |
+
const handleSubmit = (e?: FormEvent) => {
|
| 19 |
+
e?.preventDefault();
|
| 20 |
+
const trimmed = query.trim();
|
| 21 |
+
if (trimmed) {
|
| 22 |
+
onSearch(trimmed);
|
| 23 |
+
}
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
| 27 |
+
if (e.key === "Enter") {
|
| 28 |
+
handleSubmit();
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const startRecording = async () => {
|
| 33 |
+
try {
|
| 34 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 35 |
+
const mediaRecorder = new MediaRecorder(stream, {
|
| 36 |
+
mimeType: MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
|
| 37 |
+
? "audio/webm;codecs=opus"
|
| 38 |
+
: "audio/webm",
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
chunksRef.current = [];
|
| 42 |
+
mediaRecorderRef.current = mediaRecorder;
|
| 43 |
+
|
| 44 |
+
mediaRecorder.ondataavailable = (e) => {
|
| 45 |
+
if (e.data.size > 0) {
|
| 46 |
+
chunksRef.current.push(e.data);
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
mediaRecorder.onstop = async () => {
|
| 51 |
+
// Stop all tracks
|
| 52 |
+
stream.getTracks().forEach((track) => track.stop());
|
| 53 |
+
|
| 54 |
+
const audioBlob = new Blob(chunksRef.current, { type: "audio/webm" });
|
| 55 |
+
if (audioBlob.size === 0) return;
|
| 56 |
+
|
| 57 |
+
setIsTranscribing(true);
|
| 58 |
+
try {
|
| 59 |
+
const formData = new FormData();
|
| 60 |
+
formData.append("file", audioBlob, "recording.webm");
|
| 61 |
+
formData.append("language", "en-IN");
|
| 62 |
+
|
| 63 |
+
const res = await fetch("/api/voice-to-text", {
|
| 64 |
+
method: "POST",
|
| 65 |
+
body: formData,
|
| 66 |
+
});
|
| 67 |
+
const data = await res.json();
|
| 68 |
+
|
| 69 |
+
if (data.text && !data.text.startsWith("[Voice recognition error")) {
|
| 70 |
+
setQuery(data.text);
|
| 71 |
+
// Auto-trigger search
|
| 72 |
+
onSearch(data.text);
|
| 73 |
+
}
|
| 74 |
+
} catch (err) {
|
| 75 |
+
console.error("Voice transcription error:", err);
|
| 76 |
+
} finally {
|
| 77 |
+
setIsTranscribing(false);
|
| 78 |
+
}
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
mediaRecorder.start();
|
| 82 |
+
setIsRecording(true);
|
| 83 |
+
} catch (err) {
|
| 84 |
+
console.error("Microphone access denied:", err);
|
| 85 |
+
}
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
const stopRecording = () => {
|
| 89 |
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
|
| 90 |
+
mediaRecorderRef.current.stop();
|
| 91 |
+
setIsRecording(false);
|
| 92 |
+
}
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
const toggleRecording = () => {
|
| 96 |
+
if (isRecording) {
|
| 97 |
+
stopRecording();
|
| 98 |
+
} else {
|
| 99 |
+
startRecording();
|
| 100 |
+
}
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<div className="w-full max-w-3xl relative">
|
| 105 |
+
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none text-primary">
|
| 106 |
+
<span className="material-symbols-outlined">search</span>
|
| 107 |
+
</div>
|
| 108 |
+
<input
|
| 109 |
+
id="search-input"
|
| 110 |
+
type="text"
|
| 111 |
+
value={query}
|
| 112 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 113 |
+
onKeyDown={handleKeyDown}
|
| 114 |
+
className="w-full pl-12 pr-28 py-4 bg-white border-none rounded-2xl shadow-xl shadow-primary/5 focus:ring-2 focus:ring-primary/50 text-charcoal placeholder-charcoal/40"
|
| 115 |
+
placeholder={
|
| 116 |
+
isTranscribing
|
| 117 |
+
? "Transcribing your voice..."
|
| 118 |
+
: isRecording
|
| 119 |
+
? "Listening... click mic to stop"
|
| 120 |
+
: "Search the wisdom of the sutras..."
|
| 121 |
+
}
|
| 122 |
+
disabled={isLoading || isTranscribing}
|
| 123 |
+
/>
|
| 124 |
+
<div className="absolute inset-y-0 right-4 flex items-center gap-2">
|
| 125 |
+
<button
|
| 126 |
+
onClick={toggleRecording}
|
| 127 |
+
disabled={isLoading || isTranscribing}
|
| 128 |
+
className={`p-2 transition-all rounded-full ${
|
| 129 |
+
isRecording
|
| 130 |
+
? "text-red-500 bg-red-50 animate-pulse"
|
| 131 |
+
: isTranscribing
|
| 132 |
+
? "text-primary/50 cursor-wait"
|
| 133 |
+
: "text-charcoal/40 hover:text-primary hover:bg-primary/5"
|
| 134 |
+
}`}
|
| 135 |
+
aria-label={isRecording ? "Stop recording" : "Voice search"}
|
| 136 |
+
title={isRecording ? "Stop recording" : "Voice search"}
|
| 137 |
+
>
|
| 138 |
+
<span className="material-symbols-outlined">
|
| 139 |
+
{isRecording ? "stop_circle" : isTranscribing ? "hourglass_top" : "mic"}
|
| 140 |
+
</span>
|
| 141 |
+
</button>
|
| 142 |
+
<button
|
| 143 |
+
className="p-2 text-charcoal/40 hover:text-primary transition-colors"
|
| 144 |
+
aria-label="Upload file"
|
| 145 |
+
>
|
| 146 |
+
<span className="material-symbols-outlined">upload_file</span>
|
| 147 |
+
</button>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
);
|
| 151 |
+
}
|
frontend/src/components/SearchSidebar.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
|
| 3 |
+
export default function SearchSidebar() {
|
| 4 |
+
return (
|
| 5 |
+
<aside className="w-16 lg:w-64 flex-shrink-0 border-r border-primary/10 bg-white flex flex-col h-full">
|
| 6 |
+
<div className="p-4 lg:p-6 flex items-center gap-3">
|
| 7 |
+
<Link href="/" className="flex items-center gap-3">
|
| 8 |
+
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center shrink-0">
|
| 9 |
+
<span className="text-xl font-black text-background-dark">A</span>
|
| 10 |
+
</div>
|
| 11 |
+
<div className="hidden lg:block overflow-hidden">
|
| 12 |
+
<h1 className="text-charcoal font-bold text-lg leading-tight">
|
| 13 |
+
Anvesha AI
|
| 14 |
+
</h1>
|
| 15 |
+
<p className="text-primary text-xs font-medium uppercase tracking-widest">
|
| 16 |
+
Sovereign
|
| 17 |
+
</p>
|
| 18 |
+
</div>
|
| 19 |
+
</Link>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<nav className="flex-1 px-2 lg:px-4 space-y-2 mt-4">
|
| 23 |
+
<a
|
| 24 |
+
className="flex items-center gap-4 px-3 py-3 rounded-xl bg-primary/10 text-primary"
|
| 25 |
+
href="#"
|
| 26 |
+
>
|
| 27 |
+
<span className="material-symbols-outlined">history</span>
|
| 28 |
+
<span className="hidden lg:block font-medium">Search History</span>
|
| 29 |
+
</a>
|
| 30 |
+
<a
|
| 31 |
+
className="flex items-center gap-4 px-3 py-3 rounded-xl text-charcoal/60 hover:bg-charcoal/5 transition-colors"
|
| 32 |
+
href="#"
|
| 33 |
+
>
|
| 34 |
+
<span className="material-symbols-outlined">bookmarks</span>
|
| 35 |
+
<span className="hidden lg:block font-medium">Saved Sutras</span>
|
| 36 |
+
</a>
|
| 37 |
+
<a
|
| 38 |
+
className="flex items-center gap-4 px-3 py-3 rounded-xl text-charcoal/60 hover:bg-charcoal/5 transition-colors"
|
| 39 |
+
href="#"
|
| 40 |
+
>
|
| 41 |
+
<span className="material-symbols-outlined">description</span>
|
| 42 |
+
<span className="hidden lg:block font-medium">Document Intelligence</span>
|
| 43 |
+
</a>
|
| 44 |
+
</nav>
|
| 45 |
+
|
| 46 |
+
<div className="p-4 border-t border-primary/10">
|
| 47 |
+
<div className="flex items-center gap-3 p-2">
|
| 48 |
+
<div className="h-8 w-8 rounded-full bg-charcoal/10 flex items-center justify-center">
|
| 49 |
+
<span className="material-symbols-outlined text-charcoal/50 text-sm">person</span>
|
| 50 |
+
</div>
|
| 51 |
+
<div className="hidden lg:block">
|
| 52 |
+
<p className="text-sm font-semibold text-charcoal">Dharma Practitioner</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</aside>
|
| 57 |
+
);
|
| 58 |
+
}
|
frontend/src/components/SuggestionPills.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
interface SuggestionPillsProps {
|
| 4 |
+
onSuggestionClick: (query: string) => void;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
const suggestions = [
|
| 8 |
+
"Interpret Ahimsa in Policy",
|
| 9 |
+
"Arthashastra Tax Code",
|
| 10 |
+
"Justice Systems Comparison",
|
| 11 |
+
"Ethics of Intelligence",
|
| 12 |
+
"Digital India Initiatives",
|
| 13 |
+
"RTI Act Overview",
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
export default function SuggestionPills({ onSuggestionClick }: SuggestionPillsProps) {
|
| 17 |
+
return (
|
| 18 |
+
<div className="max-w-4xl mx-auto w-full px-6 pb-12 flex flex-wrap gap-2 justify-center">
|
| 19 |
+
{suggestions.map((suggestion) => (
|
| 20 |
+
<button
|
| 21 |
+
key={suggestion}
|
| 22 |
+
onClick={() => onSuggestionClick(suggestion)}
|
| 23 |
+
className="px-4 py-2 rounded-full border border-primary/20 text-xs font-medium text-charcoal/50 hover:bg-primary/10 hover:text-primary transition-all"
|
| 24 |
+
>
|
| 25 |
+
{suggestion}
|
| 26 |
+
</button>
|
| 27 |
+
))}
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
}
|
frontend/src/components/SutraCard.tsx
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useRef } from "react";
|
| 4 |
+
|
| 5 |
+
interface SutraCardProps {
|
| 6 |
+
title: string;
|
| 7 |
+
content: string;
|
| 8 |
+
url: string;
|
| 9 |
+
citationIndex: number;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function SutraCard({
|
| 13 |
+
title,
|
| 14 |
+
content,
|
| 15 |
+
url,
|
| 16 |
+
citationIndex,
|
| 17 |
+
}: SutraCardProps) {
|
| 18 |
+
const [isPlaying, setIsPlaying] = useState(false);
|
| 19 |
+
const [isLoadingAudio, setIsLoadingAudio] = useState(false);
|
| 20 |
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
| 21 |
+
|
| 22 |
+
const isGovIn = url.includes(".gov.in");
|
| 23 |
+
const domain = (() => {
|
| 24 |
+
try {
|
| 25 |
+
return new URL(url).hostname;
|
| 26 |
+
} catch {
|
| 27 |
+
return url;
|
| 28 |
+
}
|
| 29 |
+
})();
|
| 30 |
+
|
| 31 |
+
const handleListen = async () => {
|
| 32 |
+
// If already playing, stop
|
| 33 |
+
if (isPlaying && audioRef.current) {
|
| 34 |
+
audioRef.current.pause();
|
| 35 |
+
audioRef.current.currentTime = 0;
|
| 36 |
+
setIsPlaying(false);
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
setIsLoadingAudio(true);
|
| 41 |
+
try {
|
| 42 |
+
const textToRead = `${title}. ${content}`;
|
| 43 |
+
const res = await fetch("/api/text-to-voice", {
|
| 44 |
+
method: "POST",
|
| 45 |
+
headers: { "Content-Type": "application/json" },
|
| 46 |
+
body: JSON.stringify({
|
| 47 |
+
text: textToRead,
|
| 48 |
+
language: "en-IN",
|
| 49 |
+
speaker: "meera",
|
| 50 |
+
}),
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
const data = await res.json();
|
| 54 |
+
|
| 55 |
+
if (data.audio_base64) {
|
| 56 |
+
// Create audio from base64
|
| 57 |
+
const audioSrc = `data:audio/wav;base64,${data.audio_base64}`;
|
| 58 |
+
const audio = new Audio(audioSrc);
|
| 59 |
+
audioRef.current = audio;
|
| 60 |
+
|
| 61 |
+
audio.onended = () => setIsPlaying(false);
|
| 62 |
+
audio.onerror = () => {
|
| 63 |
+
setIsPlaying(false);
|
| 64 |
+
console.error("Audio playback error");
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
await audio.play();
|
| 68 |
+
setIsPlaying(true);
|
| 69 |
+
}
|
| 70 |
+
} catch (err) {
|
| 71 |
+
console.error("TTS error:", err);
|
| 72 |
+
} finally {
|
| 73 |
+
setIsLoadingAudio(false);
|
| 74 |
+
}
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
<article className="group relative bg-white p-8 rounded-2xl border border-transparent hover:border-primary/20 transition-all shadow-sm hover:shadow-xl">
|
| 79 |
+
<div className="flex flex-col gap-4">
|
| 80 |
+
<h3 className="text-2xl font-bold text-charcoal leading-tight">
|
| 81 |
+
{title}
|
| 82 |
+
</h3>
|
| 83 |
+
<p className="text-charcoal/60 line-clamp-2 leading-relaxed">
|
| 84 |
+
{content}
|
| 85 |
+
</p>
|
| 86 |
+
|
| 87 |
+
<div className="flex items-center gap-4 mt-2">
|
| 88 |
+
<div className="sutra-line flex-1" />
|
| 89 |
+
<div
|
| 90 |
+
className={`flex items-center gap-2 px-3 py-1 rounded-full border ${
|
| 91 |
+
isGovIn
|
| 92 |
+
? "bg-primary/10 border-primary/20"
|
| 93 |
+
: "bg-charcoal/5 border-charcoal/10"
|
| 94 |
+
}`}
|
| 95 |
+
>
|
| 96 |
+
<span
|
| 97 |
+
className={`text-[10px] font-bold uppercase ${
|
| 98 |
+
isGovIn ? "text-primary" : "text-charcoal/50"
|
| 99 |
+
}`}
|
| 100 |
+
>
|
| 101 |
+
{isGovIn ? "Sutra Citation" : "Source"}
|
| 102 |
+
</span>
|
| 103 |
+
<span
|
| 104 |
+
className={`text-xs font-mono font-bold ${
|
| 105 |
+
isGovIn ? "text-primary" : "text-charcoal/50"
|
| 106 |
+
}`}
|
| 107 |
+
>
|
| 108 |
+
[{citationIndex}] {domain}
|
| 109 |
+
</span>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div className="flex justify-between items-center pt-4">
|
| 114 |
+
{/* Listen Button */}
|
| 115 |
+
<button
|
| 116 |
+
onClick={handleListen}
|
| 117 |
+
disabled={isLoadingAudio}
|
| 118 |
+
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-semibold transition-all ${
|
| 119 |
+
isPlaying
|
| 120 |
+
? "bg-primary text-background-dark"
|
| 121 |
+
: isLoadingAudio
|
| 122 |
+
? "bg-charcoal/5 text-charcoal/30 cursor-wait"
|
| 123 |
+
: "bg-primary/10 text-primary hover:bg-primary/20"
|
| 124 |
+
}`}
|
| 125 |
+
title={isPlaying ? "Stop listening" : "Listen to this Sutra"}
|
| 126 |
+
>
|
| 127 |
+
<span className="material-symbols-outlined text-sm">
|
| 128 |
+
{isPlaying ? "stop" : isLoadingAudio ? "hourglass_top" : "volume_up"}
|
| 129 |
+
</span>
|
| 130 |
+
{isPlaying ? "Stop" : isLoadingAudio ? "Loading..." : "Listen"}
|
| 131 |
+
</button>
|
| 132 |
+
|
| 133 |
+
{/* Deep Dive Link */}
|
| 134 |
+
<a
|
| 135 |
+
href={url}
|
| 136 |
+
target="_blank"
|
| 137 |
+
rel="noopener noreferrer"
|
| 138 |
+
className="text-primary font-semibold text-sm flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
| 139 |
+
>
|
| 140 |
+
Deep Dive{" "}
|
| 141 |
+
<span className="material-symbols-outlined text-sm">
|
| 142 |
+
arrow_forward
|
| 143 |
+
</span>
|
| 144 |
+
</a>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</article>
|
| 148 |
+
);
|
| 149 |
+
}
|
frontend/stitch/anvesha_ai_landing_page/code.html
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
|
| 3 |
+
<html lang="en"><head>
|
| 4 |
+
<meta charset="utf-8"/>
|
| 5 |
+
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
| 6 |
+
<title>Anvesha AI | The Sutra of Information</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"/>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 11 |
+
<script id="tailwind-config">
|
| 12 |
+
tailwind.config = {
|
| 13 |
+
darkMode: "class",
|
| 14 |
+
theme: {
|
| 15 |
+
extend: {
|
| 16 |
+
colors: {
|
| 17 |
+
"primary": "#d4af35",
|
| 18 |
+
"charcoal": "#36454f",
|
| 19 |
+
"background-light": "#f8f7f6",
|
| 20 |
+
"background-dark": "#201d12",
|
| 21 |
+
},
|
| 22 |
+
fontFamily: {
|
| 23 |
+
"display": ["Inter", "sans-serif"]
|
| 24 |
+
},
|
| 25 |
+
borderRadius: {
|
| 26 |
+
"DEFAULT": "0.25rem",
|
| 27 |
+
"lg": "0.5rem",
|
| 28 |
+
"xl": "0.75rem",
|
| 29 |
+
"full": "9999px"
|
| 30 |
+
},
|
| 31 |
+
},
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
</script>
|
| 35 |
+
<style>
|
| 36 |
+
body {
|
| 37 |
+
font-family: 'Inter', sans-serif;
|
| 38 |
+
}
|
| 39 |
+
.material-symbols-outlined {
|
| 40 |
+
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
| 41 |
+
}
|
| 42 |
+
</style>
|
| 43 |
+
</head>
|
| 44 |
+
<body class="bg-background-light dark:bg-background-dark text-charcoal dark:text-slate-100 font-display">
|
| 45 |
+
<div class="relative flex min-h-screen flex-col overflow-x-hidden">
|
| 46 |
+
<header class="sticky top-0 z-50 w-full border-b border-primary/10 bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md">
|
| 47 |
+
<div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 lg:px-12">
|
| 48 |
+
<div class="flex items-center gap-3">
|
| 49 |
+
<img alt="Anvesha AI Logo" class="h-10 w-10" data-alt="Anvesha AI stylized logo symbol" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCj_Ov9celq7zkiikEhF4t3euNN9aygAlyXbG8qIC7jF5OKJrrriEp4xV4VjyH8zEu-QSRK6lxZP11SuOw1I6-eDS4_o0_SAR4ZNj2zr-HPvwyVH3gYyymw8-R33DoTYLBRIrBwhNwyghbjwxuoPDHtWYnxGOJ2uAv0-96EEAJORoZ2T2plPaY0rSiGK92G4arfwFsanOJPf-YhbQ0ZGTOz-uf3NYG-SuI24vIPRTcTwhSwnDjFng9snBdy3TW2yV4mY7y4_cVsUMo_"/>
|
| 50 |
+
<span class="text-xl font-bold tracking-tight text-charcoal dark:text-white">Anvesha AI</span>
|
| 51 |
+
</div>
|
| 52 |
+
<nav class="hidden md:flex items-center gap-8">
|
| 53 |
+
<a class="text-sm font-medium hover:text-primary transition-colors" href="#">Philosophy</a>
|
| 54 |
+
<a class="text-sm font-medium hover:text-primary transition-colors" href="#">Intelligence</a>
|
| 55 |
+
<a class="text-sm font-medium hover:text-primary transition-colors" href="#">Citations</a>
|
| 56 |
+
<a class="text-sm font-medium hover:text-primary transition-colors" href="#">Multimodal</a>
|
| 57 |
+
</nav>
|
| 58 |
+
<div class="flex items-center gap-4">
|
| 59 |
+
<button class="hidden sm:flex items-center justify-center rounded-lg bg-primary px-6 py-2.5 text-sm font-bold text-background-dark transition-all hover:brightness-110 active:scale-95">
|
| 60 |
+
Explore Now
|
| 61 |
+
</button>
|
| 62 |
+
<button class="md:hidden p-2 text-charcoal dark:text-white">
|
| 63 |
+
<span class="material-symbols-outlined">menu</span>
|
| 64 |
+
</button>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</header>
|
| 68 |
+
<main class="flex-1">
|
| 69 |
+
<section class="relative overflow-hidden px-6 py-20 lg:px-12 lg:py-32">
|
| 70 |
+
<div class="mx-auto max-w-7xl">
|
| 71 |
+
<div class="grid items-center gap-12 lg:grid-cols-2">
|
| 72 |
+
<div class="flex flex-col gap-8">
|
| 73 |
+
<div class="inline-flex w-fit items-center rounded-full bg-primary/10 px-4 py-1 text-xs font-bold uppercase tracking-widest text-primary">
|
| 74 |
+
Sovereign Indian LLM
|
| 75 |
+
</div>
|
| 76 |
+
<h1 class="text-5xl font-black leading-[1.1] tracking-tight text-charcoal dark:text-white md:text-6xl lg:text-7xl">
|
| 77 |
+
The Sutra of <span class="text-primary">Information</span>
|
| 78 |
+
</h1>
|
| 79 |
+
<p class="max-w-xl text-lg leading-relaxed text-charcoal/80 dark:text-slate-300 md:text-xl">
|
| 80 |
+
Experience sovereign Indian LLMs designed for professional insight, deep reasoning, and cultural precision.
|
| 81 |
+
</p>
|
| 82 |
+
<div class="flex flex-col gap-4 sm:flex-row">
|
| 83 |
+
<button class="flex items-center justify-center rounded-lg bg-primary px-8 py-4 text-base font-bold text-background-dark transition-all hover:shadow-lg hover:shadow-primary/20 active:scale-95">
|
| 84 |
+
Explore Now
|
| 85 |
+
</button>
|
| 86 |
+
<button class="flex items-center justify-center rounded-lg border-2 border-charcoal/10 bg-transparent px-8 py-4 text-base font-bold text-charcoal dark:border-white/10 dark:text-white hover:bg-charcoal/5 dark:hover:bg-white/5 transition-all">
|
| 87 |
+
Watch Demo
|
| 88 |
+
</button>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="relative aspect-square lg:aspect-video rounded-2xl overflow-hidden shadow-2xl border border-primary/20">
|
| 92 |
+
<div class="absolute inset-0 bg-gradient-to-tr from-primary/20 to-transparent mix-blend-overlay"></div>
|
| 93 |
+
<img alt="Hero Image" class="h-full w-full object-cover" data-alt="Abstract geometric patterns representing neural networks" src="https://lh3.googleusercontent.com/aida-public/AB6AXuA_6OvMz2fD13HUs1okW8JEeIMt8E-GMbTNmvfYbMco_7Zc07J3UvDNI5_KlQ23hvpim5SnIP_KxBsssF9lKdBIG91e-IldHIU_XSXHvD4_1BacJ97hkgXIU-8h0laSeE4rmIzT60GmuOhxfwoplZWcJ5FHqM1yp0rc-o1j7Ha8lkC8fYG_rUTJH2hsByJpRLwWXcL3gRAVRr9azRRgpLJp6C6t0XyTyq4ajH6CoPWhu6jR0DY8M28VgZuWNQemnx-Tpn7F0kBaNVGp"/>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</section>
|
| 98 |
+
<section class="bg-white/50 dark:bg-white/5 py-24">
|
| 99 |
+
<div class="mx-auto max-w-4xl px-6 text-center lg:px-12">
|
| 100 |
+
<h2 class="mb-6 text-sm font-bold uppercase tracking-[0.3em] text-primary">Philosophy</h2>
|
| 101 |
+
<h3 class="mb-8 text-4xl font-black text-charcoal dark:text-white md:text-5xl">Prajna (Insight)</h3>
|
| 102 |
+
<p class="text-xl leading-relaxed text-charcoal/70 dark:text-slate-300">
|
| 103 |
+
Prajna is our core philosophy of context-driven deep reasoning. Powered by sovereign Indian LLMs, we provide unparalleled accuracy and local relevance that understands the nuanced tapestry of Indian information ecosystems.
|
| 104 |
+
</p>
|
| 105 |
+
</div>
|
| 106 |
+
</section>
|
| 107 |
+
<section class="px-6 py-24 lg:px-12">
|
| 108 |
+
<div class="mx-auto max-w-7xl">
|
| 109 |
+
<div class="grid gap-8 md:grid-cols-3">
|
| 110 |
+
<div class="group relative flex flex-col gap-6 rounded-2xl border border-primary/10 bg-white p-8 transition-all hover:-translate-y-2 hover:shadow-xl dark:bg-background-dark/50">
|
| 111 |
+
<div class="flex h-14 w-14 items-center justify-center rounded-xl bg-primary text-background-dark">
|
| 112 |
+
<span class="material-symbols-outlined text-3xl">shield_person</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="flex flex-col gap-3">
|
| 115 |
+
<h4 class="text-xl font-bold text-charcoal dark:text-white">Sovereign Intelligence</h4>
|
| 116 |
+
<p class="text-charcoal/70 dark:text-slate-400">
|
| 117 |
+
Built on Sarvam 105B for high-performance localized reasoning that respects data sovereignty and cultural context.
|
| 118 |
+
</p>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="mt-auto pt-4 text-primary font-bold text-sm inline-flex items-center gap-2 cursor-pointer">
|
| 121 |
+
Learn more <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="group relative flex flex-col gap-6 rounded-2xl border border-primary/10 bg-white p-8 transition-all hover:-translate-y-2 hover:shadow-xl dark:bg-background-dark/50">
|
| 125 |
+
<div class="flex h-14 w-14 items-center justify-center rounded-xl bg-primary text-background-dark">
|
| 126 |
+
<span class="material-symbols-outlined text-3xl">verified</span>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="flex flex-col gap-3">
|
| 129 |
+
<h4 class="text-xl font-bold text-charcoal dark:text-white">Citations First</h4>
|
| 130 |
+
<p class="text-charcoal/70 dark:text-slate-400">
|
| 131 |
+
Priority access to .gov.in sources and primary legal documents for verified, trustworthy, and hallucination-free information.
|
| 132 |
+
</p>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="mt-auto pt-4 text-primary font-bold text-sm inline-flex items-center gap-2 cursor-pointer">
|
| 135 |
+
Learn more <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
<div class="group relative flex flex-col gap-6 rounded-2xl border border-primary/10 bg-white p-8 transition-all hover:-translate-y-2 hover:shadow-xl dark:bg-background-dark/50">
|
| 139 |
+
<div class="flex h-14 w-14 items-center justify-center rounded-xl bg-primary text-background-dark">
|
| 140 |
+
<span class="material-symbols-outlined text-3xl">psychology_alt</span>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="flex flex-col gap-3">
|
| 143 |
+
<h4 class="text-xl font-bold text-charcoal dark:text-white">Multimodal Freedom</h4>
|
| 144 |
+
<p class="text-charcoal/70 dark:text-slate-400">
|
| 145 |
+
Seamlessly interact through Saaras Voice and Sarvam Vision, enabling natural language communication across Indian dialects.
|
| 146 |
+
</p>
|
| 147 |
+
</div>
|
| 148 |
+
<div class="mt-auto pt-4 text-primary font-bold text-sm inline-flex items-center gap-2 cursor-pointer">
|
| 149 |
+
Learn more <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</section>
|
| 155 |
+
<section class="px-6 py-20 lg:px-12">
|
| 156 |
+
<div class="mx-auto max-w-5xl overflow-hidden rounded-3xl bg-charcoal dark:bg-primary/5 p-12 lg:p-20 text-center relative">
|
| 157 |
+
<div class="absolute inset-0 bg-gradient-to-br from-primary/20 to-transparent pointer-events-none"></div>
|
| 158 |
+
<div class="relative z-10 flex flex-col items-center gap-8">
|
| 159 |
+
<h2 class="text-3xl font-black text-white dark:text-white md:text-5xl">Ready to unlock sovereign intelligence?</h2>
|
| 160 |
+
<p class="max-w-2xl text-lg text-slate-300 dark:text-slate-300">
|
| 161 |
+
Join the leading organizations leveraging India's most advanced reasoning engine for a smarter, more secure future.
|
| 162 |
+
</p>
|
| 163 |
+
<button class="rounded-lg bg-primary px-10 py-4 text-lg font-bold text-background-dark transition-all hover:scale-105 active:scale-95">
|
| 164 |
+
Explore Now
|
| 165 |
+
</button>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
</section>
|
| 169 |
+
</main>
|
| 170 |
+
<footer class="border-t border-primary/10 bg-background-light dark:bg-background-dark px-6 py-12 lg:px-12">
|
| 171 |
+
<div class="mx-auto max-w-7xl">
|
| 172 |
+
<div class="flex flex-col items-center justify-between gap-8 md:flex-row">
|
| 173 |
+
<div class="flex items-center gap-3">
|
| 174 |
+
<img alt="Anvesha AI Footer Logo" class="h-8 w-8" data-alt="Small version of Anvesha AI logo" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDu1QQyodyCn0aOjAn4ET0hlEvp2gFV0kzelT7kpBa4F6xAvsm-6Etr1-qCORPY3LLHJheabz2VPd6IGvjSpDAL4r_kJi09l3gMZTe_VahmNBXHKCALY_Rgpf7pHVBDLzG1KSGhkAxXuzpgwysEygl_Mou6GrHLXfivwIzTE32Uq_DrHLQIvc0aKzBAgarmd9Bo-sDC4OzJVV9QCff7ozOgAkwwvFmvNYERYbZrybl99xX40M7f8qBevJnalO-W0vQtEwctXlTMbuxt"/>
|
| 175 |
+
<span class="text-lg font-bold tracking-tight text-charcoal dark:text-white">Anvesha AI</span>
|
| 176 |
+
</div>
|
| 177 |
+
<div class="flex flex-wrap justify-center gap-8 text-sm font-medium text-charcoal/60 dark:text-slate-400">
|
| 178 |
+
<a class="hover:text-primary" href="#">Privacy Policy</a>
|
| 179 |
+
<a class="hover:text-primary" href="#">Terms of Service</a>
|
| 180 |
+
<a class="hover:text-primary" href="#">Documentation</a>
|
| 181 |
+
<a class="hover:text-primary" href="#">Contact</a>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
<div class="mt-12 text-center text-sm text-charcoal/40 dark:text-slate-500">
|
| 185 |
+
© 2024 Anvesha AI. Sovereign intelligence for a new era.
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
</footer>
|
| 189 |
+
</div>
|
| 190 |
+
</body></html>
|
frontend/stitch/anvesha_ai_search_interface/code.html
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
|
| 3 |
+
<html class="light" lang="en"><head>
|
| 4 |
+
<meta charset="utf-8"/>
|
| 5 |
+
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
| 6 |
+
<title>Anvesha AI - Sovereign Intelligence</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 11 |
+
<script id="tailwind-config">
|
| 12 |
+
tailwind.config = {
|
| 13 |
+
darkMode: "class",
|
| 14 |
+
theme: {
|
| 15 |
+
extend: {
|
| 16 |
+
colors: {
|
| 17 |
+
"primary": "#d4af35",
|
| 18 |
+
"background-light": "#f8f7f6",
|
| 19 |
+
"background-dark": "#201d12",
|
| 20 |
+
},
|
| 21 |
+
fontFamily: {
|
| 22 |
+
"display": ["Inter", "sans-serif"]
|
| 23 |
+
},
|
| 24 |
+
borderRadius: {
|
| 25 |
+
"DEFAULT": "0.25rem",
|
| 26 |
+
"lg": "0.5rem",
|
| 27 |
+
"xl": "0.75rem",
|
| 28 |
+
"full": "9999px"
|
| 29 |
+
},
|
| 30 |
+
},
|
| 31 |
+
},
|
| 32 |
+
}
|
| 33 |
+
</script>
|
| 34 |
+
<style>
|
| 35 |
+
body {
|
| 36 |
+
font-family: 'Inter', sans-serif;
|
| 37 |
+
}
|
| 38 |
+
.sutra-line {
|
| 39 |
+
height: 1px;
|
| 40 |
+
background: linear-gradient(90deg, #d4af35 0%, rgba(212, 175, 53, 0) 100%);
|
| 41 |
+
}
|
| 42 |
+
</style>
|
| 43 |
+
</head>
|
| 44 |
+
<body class="bg-background-light dark:bg-background-dark text-slate-900 dark:text-slate-100 font-display transition-colors duration-300">
|
| 45 |
+
<div class="flex h-screen overflow-hidden">
|
| 46 |
+
<!-- Collapsible Left Sidebar -->
|
| 47 |
+
<aside class="w-20 lg:w-64 flex-shrink-0 border-r border-primary/10 bg-white dark:bg-background-dark/50 flex flex-col h-full">
|
| 48 |
+
<div class="p-6 flex items-center gap-3">
|
| 49 |
+
<div class="h-10 w-10 rounded-full bg-primary flex items-center justify-center overflow-hidden shrink-0">
|
| 50 |
+
<img class="h-full w-full object-cover" data-alt="Anvesha AI logo gold emblem" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAoEBnUZO1_cQCkwH56i2klXr1IROsBdHYHnb8Wb69SrSc4kAiNgl9tdX80SFbbEH62zQVsVr7mTGs2CGJQkA97QT1Tjo9BD1Ms_q0FXbHXFZHVV6Mc4I8F6RcOhEXCo156DZBOYavKVg_XjvpPKIYpUymDlSJeI5HUbvWH-ShRCRcsk-5LUsHcuQF4fnAdNGczT4aAnaUA4DXuQayySgl_-QlcAZE5VEHRYRQvU-gKJlYY3_QpxcReR9wHure2_9V7IIUEf60ZweUW"/>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="hidden lg:block overflow-hidden">
|
| 53 |
+
<h1 class="text-slate-900 dark:text-slate-100 font-bold text-lg leading-tight">Anvesha AI</h1>
|
| 54 |
+
<p class="text-primary text-xs font-medium uppercase tracking-widest">Sovereign</p>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
<nav class="flex-1 px-4 space-y-2 mt-4">
|
| 58 |
+
<a class="flex items-center gap-4 px-3 py-3 rounded-xl bg-primary/10 text-primary" href="#">
|
| 59 |
+
<span class="material-symbols-outlined">history</span>
|
| 60 |
+
<span class="hidden lg:block font-medium">Search History</span>
|
| 61 |
+
</a>
|
| 62 |
+
<a class="flex items-center gap-4 px-3 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-white/5 transition-colors" href="#">
|
| 63 |
+
<span class="material-symbols-outlined">bookmarks</span>
|
| 64 |
+
<span class="hidden lg:block font-medium">Saved Sutras</span>
|
| 65 |
+
</a>
|
| 66 |
+
<a class="flex items-center gap-4 px-3 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-white/5 transition-colors" href="#">
|
| 67 |
+
<span class="material-symbols-outlined">description</span>
|
| 68 |
+
<span class="hidden lg:block font-medium">Document Intelligence</span>
|
| 69 |
+
</a>
|
| 70 |
+
</nav>
|
| 71 |
+
<div class="p-4 border-t border-primary/10">
|
| 72 |
+
<div class="flex items-center gap-3 p-2">
|
| 73 |
+
<div class="h-8 w-8 rounded-full bg-slate-200 dark:bg-white/10" data-alt="User profile placeholder" style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDaF6Rf3fBrFRx4sly7VGKK4WMEpnqdBsA-krFVL-L2uAIglGenuPYkHZVVGHiP1o6iStcyiQurC47ZqeKtIhj9eQoO-D-zwXpAIZwY4rnAe7pFBDhl31z00LsYSFLbq73nicevxvFsbRK4ZZolDaGNNBRVo1GVpndPsUX0r6dHEFwvb-ltETc620oSvmuhg14jNdv7Jce9JNvu98WDfiSIoI83f-yq8Vy-0YGql0txPkKoi3cmpEE6JZCPKC_tPzakG4TXv8Gvx4WO');"></div>
|
| 74 |
+
<div class="hidden lg:block">
|
| 75 |
+
<p class="text-sm font-semibold">Dharma Practitioner</p>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</aside>
|
| 80 |
+
<!-- Main Content Area -->
|
| 81 |
+
<main class="flex-1 flex flex-col min-w-0 bg-background-light dark:bg-background-dark relative overflow-y-auto">
|
| 82 |
+
<!-- Top Search Header -->
|
| 83 |
+
<header class="sticky top-0 z-10 p-6 md:px-12 flex flex-col items-center bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md">
|
| 84 |
+
<div class="w-full max-w-3xl relative">
|
| 85 |
+
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none text-primary">
|
| 86 |
+
<span class="material-symbols-outlined">search</span>
|
| 87 |
+
</div>
|
| 88 |
+
<input class="w-full pl-12 pr-28 py-4 bg-white dark:bg-white/5 border-none rounded-2xl shadow-xl shadow-primary/5 focus:ring-2 focus:ring-primary/50 text-slate-900 dark:text-slate-100 placeholder-slate-400" placeholder="Search the wisdom of the sutras..." type="text"/>
|
| 89 |
+
<div class="absolute inset-y-0 right-4 flex items-center gap-2">
|
| 90 |
+
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
| 91 |
+
<span class="material-symbols-outlined">mic</span>
|
| 92 |
+
</button>
|
| 93 |
+
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
| 94 |
+
<span class="material-symbols-outlined">upload_file</span>
|
| 95 |
+
</button>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</header>
|
| 99 |
+
<!-- Results Section -->
|
| 100 |
+
<section class="max-w-4xl mx-auto w-full p-6 md:p-12 space-y-8">
|
| 101 |
+
<div class="flex items-center justify-between mb-2">
|
| 102 |
+
<h2 class="text-sm font-semibold uppercase tracking-widest text-primary/70">Top Insights</h2>
|
| 103 |
+
<span class="text-xs text-slate-400">Found 12 relevant citations</span>
|
| 104 |
+
</div>
|
| 105 |
+
<!-- Result Card 1 -->
|
| 106 |
+
<article class="group relative bg-white dark:bg-white/5 p-8 rounded-2xl border border-transparent hover:border-primary/20 transition-all shadow-sm hover:shadow-xl">
|
| 107 |
+
<div class="flex flex-col gap-4">
|
| 108 |
+
<h3 class="text-2xl font-bold text-slate-800 dark:text-slate-100 leading-tight">Fundamental Principles of Governance</h3>
|
| 109 |
+
<p class="text-slate-600 dark:text-slate-400 line-clamp-2 leading-relaxed">
|
| 110 |
+
A comprehensive analysis of administrative frameworks and ethical leadership as derived from classical texts, focusing on the decentralization of authority.
|
| 111 |
+
</p>
|
| 112 |
+
<div class="flex items-center gap-4 mt-2">
|
| 113 |
+
<div class="sutra-line flex-1"></div>
|
| 114 |
+
<div class="flex items-center gap-2 px-3 py-1 bg-primary/10 rounded-full border border-primary/20">
|
| 115 |
+
<span class="text-[10px] font-bold text-primary uppercase">Sutra Citation</span>
|
| 116 |
+
<span class="text-xs font-mono text-primary font-bold">[1] .gov.in</span>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
<div class="flex justify-end pt-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 120 |
+
<button class="text-primary font-semibold text-sm flex items-center gap-2">
|
| 121 |
+
Deep Dive <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 122 |
+
</button>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
</article>
|
| 126 |
+
<!-- Result Card 2 -->
|
| 127 |
+
<article class="group relative bg-white dark:bg-white/5 p-8 rounded-2xl border border-transparent hover:border-primary/20 transition-all shadow-sm hover:shadow-xl">
|
| 128 |
+
<div class="flex flex-col gap-4">
|
| 129 |
+
<h3 class="text-2xl font-bold text-slate-800 dark:text-slate-100 leading-tight">Economic Policy and Social Welfare</h3>
|
| 130 |
+
<p class="text-slate-600 dark:text-slate-400 line-clamp-2 leading-relaxed">
|
| 131 |
+
Exploring the intersection of fiscal responsibility and social equity through the lens of traditional intelligence, specifically addressing poverty alleviation.
|
| 132 |
+
</p>
|
| 133 |
+
<div class="flex items-center gap-4 mt-2">
|
| 134 |
+
<div class="sutra-line flex-1"></div>
|
| 135 |
+
<div class="flex items-center gap-2 px-3 py-1 bg-primary/10 rounded-full border border-primary/20">
|
| 136 |
+
<span class="text-[10px] font-bold text-primary uppercase">Sutra Citation</span>
|
| 137 |
+
<span class="text-xs font-mono text-primary font-bold">[2] .gov.in</span>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
<div class="flex justify-end pt-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 141 |
+
<button class="text-primary font-semibold text-sm flex items-center gap-2">
|
| 142 |
+
Deep Dive <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 143 |
+
</button>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</article>
|
| 147 |
+
<!-- Result Card 3 -->
|
| 148 |
+
<article class="group relative bg-white dark:bg-white/5 p-8 rounded-2xl border border-transparent hover:border-primary/20 transition-all shadow-sm hover:shadow-xl">
|
| 149 |
+
<div class="flex flex-col gap-4">
|
| 150 |
+
<h3 class="text-2xl font-bold text-slate-800 dark:text-slate-100 leading-tight">Digital Sovereignty in the AI Age</h3>
|
| 151 |
+
<p class="text-slate-600 dark:text-slate-400 line-clamp-2 leading-relaxed">
|
| 152 |
+
How traditional concepts of 'Swaraj' apply to data protection and algorithmic governance in a rapidly globalizing technology landscape.
|
| 153 |
+
</p>
|
| 154 |
+
<div class="flex items-center gap-4 mt-2">
|
| 155 |
+
<div class="sutra-line flex-1"></div>
|
| 156 |
+
<div class="flex items-center gap-2 px-3 py-1 bg-primary/10 rounded-full border border-primary/20">
|
| 157 |
+
<span class="text-[10px] font-bold text-primary uppercase">Sutra Citation</span>
|
| 158 |
+
<span class="text-xs font-mono text-primary font-bold">[3] .gov.in</span>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
<div class="flex justify-end pt-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 162 |
+
<button class="text-primary font-semibold text-sm flex items-center gap-2">
|
| 163 |
+
Deep Dive <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 164 |
+
</button>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</article>
|
| 168 |
+
</section>
|
| 169 |
+
<!-- Quick Suggestion Pills -->
|
| 170 |
+
<div class="max-w-4xl mx-auto w-full px-6 pb-12 flex flex-wrap gap-2 justify-center">
|
| 171 |
+
<button class="px-4 py-2 rounded-full border border-primary/20 text-xs font-medium text-slate-500 hover:bg-primary/10 hover:text-primary transition-all">Interpret Ahimsa in Policy</button>
|
| 172 |
+
<button class="px-4 py-2 rounded-full border border-primary/20 text-xs font-medium text-slate-500 hover:bg-primary/10 hover:text-primary transition-all">Arthashastra Tax Code</button>
|
| 173 |
+
<button class="px-4 py-2 rounded-full border border-primary/20 text-xs font-medium text-slate-500 hover:bg-primary/10 hover:text-primary transition-all">Justice Systems Comparison</button>
|
| 174 |
+
<button class="px-4 py-2 rounded-full border border-primary/20 text-xs font-medium text-slate-500 hover:bg-primary/10 hover:text-primary transition-all">Ethics of Intelligence</button>
|
| 175 |
+
</div>
|
| 176 |
+
</main>
|
| 177 |
+
</div>
|
| 178 |
+
</body></html>
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "react-jsx",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./src/*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": [
|
| 26 |
+
"next-env.d.ts",
|
| 27 |
+
"**/*.ts",
|
| 28 |
+
"**/*.tsx",
|
| 29 |
+
".next/types/**/*.ts",
|
| 30 |
+
".next/dev/types/**/*.ts",
|
| 31 |
+
"**/*.mts"
|
| 32 |
+
],
|
| 33 |
+
"exclude": ["node_modules"]
|
| 34 |
+
}
|
searxng/settings.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use_default_settings: true
|
| 2 |
+
general:
|
| 3 |
+
debug: false
|
| 4 |
+
instance_name: "Anvesha AI Search Node"
|
| 5 |
+
|
| 6 |
+
search:
|
| 7 |
+
formats:
|
| 8 |
+
- html
|
| 9 |
+
- json
|
| 10 |
+
safe_search: 0
|
| 11 |
+
autocomplete: "duckduckgo"
|
| 12 |
+
default_region: "in-en"
|
| 13 |
+
|
| 14 |
+
server:
|
| 15 |
+
secret_key: "anvesha_secret_key_12345"
|
| 16 |
+
|
| 17 |
+
outgoing:
|
| 18 |
+
request_timeout: 10.0
|