Bhishaj9 commited on
Commit
8bf695d
·
0 Parent(s):

Initial commit: Anvesha AI sovereign search engine

Browse files
Files changed (47) hide show
  1. .gitignore +34 -0
  2. backend/.env.template +16 -0
  3. backend/Dockerfile +20 -0
  4. backend/config.py +35 -0
  5. backend/main.py +255 -0
  6. backend/probe_instances.py +25 -0
  7. backend/requirements.txt +8 -0
  8. backend/sarvam_service.py +394 -0
  9. backend/test_sarvam.py +122 -0
  10. backend/test_search.py +205 -0
  11. docker-compose.prod.yml +63 -0
  12. docker-compose.yml +12 -0
  13. frontend/.gitignore +41 -0
  14. frontend/Dockerfile +42 -0
  15. frontend/README.md +36 -0
  16. frontend/eslint.config.mjs +18 -0
  17. frontend/next.config.ts +7 -0
  18. frontend/package-lock.json +0 -0
  19. frontend/package.json +26 -0
  20. frontend/postcss.config.mjs +7 -0
  21. frontend/public/file.svg +1 -0
  22. frontend/public/globe.svg +1 -0
  23. frontend/public/next.svg +1 -0
  24. frontend/public/vercel.svg +1 -0
  25. frontend/public/window.svg +1 -0
  26. frontend/src/app/api/search/route.ts +40 -0
  27. frontend/src/app/api/text-to-voice/route.ts +32 -0
  28. frontend/src/app/api/voice-to-text/route.ts +47 -0
  29. frontend/src/app/favicon.ico +0 -0
  30. frontend/src/app/globals.css +58 -0
  31. frontend/src/app/layout.tsx +36 -0
  32. frontend/src/app/page.tsx +21 -0
  33. frontend/src/app/search/page.tsx +156 -0
  34. frontend/src/components/CTASection.tsx +26 -0
  35. frontend/src/components/FeaturesSection.tsx +55 -0
  36. frontend/src/components/Footer.tsx +35 -0
  37. frontend/src/components/Header.tsx +45 -0
  38. frontend/src/components/HeroSection.tsx +46 -0
  39. frontend/src/components/PhilosophySection.tsx +20 -0
  40. frontend/src/components/SearchBar.tsx +151 -0
  41. frontend/src/components/SearchSidebar.tsx +58 -0
  42. frontend/src/components/SuggestionPills.tsx +30 -0
  43. frontend/src/components/SutraCard.tsx +149 -0
  44. frontend/stitch/anvesha_ai_landing_page/code.html +190 -0
  45. frontend/stitch/anvesha_ai_search_interface/code.html +178 -0
  46. frontend/tsconfig.json +34 -0
  47. 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)}&region=${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&apos;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&amp;display=swap" rel="stylesheet"/>
9
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
10
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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&amp;display=swap" rel="stylesheet"/>
9
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@100..700,0..1&amp;display=swap" rel="stylesheet"/>
10
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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