umer6016 commited on
Commit
ad3d8b0
·
1 Parent(s): 04f25f0

Prepare for deployment: Update dependencies and add all new components

Browse files
.gitignore CHANGED
@@ -1,12 +1,12 @@
1
- node_modules
2
- dist
3
- build
4
- *.log
5
- __pycache__
6
  *.pyc
7
  .env
 
 
 
 
 
 
 
8
  .DS_Store
9
- venv
10
- env
11
- .vscode
12
- .idea
 
1
+ .venv/
2
+ __pycache__/
 
 
 
3
  *.pyc
4
  .env
5
+ .env.local
6
+ knowledge_files/
7
+ backend/knowledge_files/
8
+ frontend/node_modules/
9
+ frontend/dist/
10
+ metrics_logs/
11
+ .vscode/
12
  .DS_Store
 
 
 
 
AGENTS.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Codex Guidance
2
+
3
+ ## Conventions
4
+ - Keep existing FastAPI endpoints and React flows stable; add new functionality behind flags/config toggles.
5
+ - Prefer small, composable helpers in `backend/app/services/` for pipeline changes.
6
+ - Do not commit secrets (`.env`), caches (`knowledge_files/`), build artifacts (`frontend/dist/`), or logs (`metrics_logs/`).
7
+
8
+ ## Metrics
9
+ - New chatbot metrics live in `backend/app/services/scrape_pipeline.py`.
10
+ - Add supporting helpers in `backend/app/services/metrics_logger.py`.
11
+ - Guard all metrics code with the `ENABLE_METRICS_LOGGING` environment variable (default `false`).
12
+ - Surface new metrics via API stats objects without breaking existing fields.
13
+
14
+ ## Commands to run after changes
15
+ - Backend: `pip install -r requirements.txt` (if deps change), then `pytest`.
16
+ - Frontend: `cd frontend && npm install` (if deps change), then `npm run build`.
17
+
18
+ ## Feature flags
19
+ - All new features/metrics must be optional and off by default:
20
+ - `ENABLE_METRICS_LOGGING` controls metrics/telemetry.
21
+ - Wrap new logic in `if os.getenv("ENABLE_METRICS_LOGGING", "false").lower() == "true": ...`.
22
+
23
+ ## Testing expectations
24
+ - Add/maintain unit tests for new logic; prefer fast, isolated tests with mocks over network calls.
25
+ - If modifying the pipeline, ensure cache behavior and new stats remain backward compatible.
README.md CHANGED
@@ -80,6 +80,9 @@ npm install
80
  npm run dev # opens on http://localhost:5173
81
  ```
82
 
 
 
 
83
  ### Usage
84
  - Sign up (first/last/email/password) → OTP → auto-login.
85
  - Generate chatbot: paste URL, optional Force refresh → Run. A brief summary (pages scraped, web searches) shows, then the chatbot appears.
 
80
  npm run dev # opens on http://localhost:5173
81
  ```
82
 
83
+ ### Optional metrics (feature-flagged)
84
+ - Set `ENABLE_METRICS_LOGGING=true` in your environment to capture Time-to-Chatbot-Ready (TCR), cache hit flags, and chat Q/A JSONL logs (`metrics_logs/chat_answers.jsonl`). Disabled by default to avoid any impact on existing flows.
85
+
86
  ### Usage
87
  - Sign up (first/last/email/password) → OTP → auto-login.
88
  - Generate chatbot: paste URL, optional Force refresh → Run. A brief summary (pages scraped, web searches) shows, then the chatbot appears.
backend/app/api/chat.py CHANGED
@@ -2,6 +2,11 @@ from fastapi import APIRouter, HTTPException
2
  from openai import OpenAI
3
 
4
  from ..models.chat import ChatRequest, ChatResponse, ChatMessage
 
 
 
 
 
5
 
6
  router = APIRouter()
7
 
@@ -22,6 +27,28 @@ async def chat(req: ChatRequest):
22
  messages=messages,
23
  )
24
  answer = resp.choices[0].message.content or ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  return ChatResponse(message=ChatMessage(role="assistant", content=answer))
26
  except Exception as exc:
27
  raise HTTPException(status_code=500, detail=str(exc))
 
2
  from openai import OpenAI
3
 
4
  from ..models.chat import ChatRequest, ChatResponse, ChatMessage
5
+ from ..services.metrics_logger import (
6
+ log_chat_answer,
7
+ save_chat_answer_to_supabase,
8
+ metrics_enabled,
9
+ )
10
 
11
  router = APIRouter()
12
 
 
27
  messages=messages,
28
  )
29
  answer = resp.choices[0].message.content or ""
30
+
31
+ if metrics_enabled():
32
+ provenance = "primary_plus_secondary" if "SECONDARY SOURCE" in req.system_prompt else "primary_only"
33
+ try:
34
+ # Best-effort file log
35
+ log_chat_answer(
36
+ question=req.messages[-1].content if req.messages else "",
37
+ answer=answer,
38
+ provenance=provenance,
39
+ user=None,
40
+ )
41
+ # Best-effort Supabase log
42
+ save_chat_answer_to_supabase(
43
+ question=req.messages[-1].content if req.messages else "",
44
+ answer=answer,
45
+ system_prompt=req.system_prompt,
46
+ user_id=None,
47
+ url=None,
48
+ )
49
+ except Exception as log_exc:
50
+ print(f"⚠️ Metrics logging skipped: {log_exc}")
51
+
52
  return ChatResponse(message=ChatMessage(role="assistant", content=answer))
53
  except Exception as exc:
54
  raise HTTPException(status_code=500, detail=str(exc))
backend/app/api/jobs.py CHANGED
@@ -4,6 +4,8 @@ import asyncio
4
 
5
  from ..models.jobs import JobCreate, JobStatus
6
  from ..services import scrape_pipeline
 
 
7
 
8
  router = APIRouter()
9
 
@@ -26,7 +28,19 @@ async def run_job(body: JobCreate) -> JobStatus:
26
  "searches_run": stats.get("searches_run", 0),
27
  "pages_scraped": stats.get("pages_scraped", 0),
28
  "gaps_found": stats.get("gaps_found", 0),
 
 
29
  }
 
 
 
 
 
 
 
 
 
 
30
  return JobStatus(
31
  job_id="dev-inline",
32
  status="completed",
 
4
 
5
  from ..models.jobs import JobCreate, JobStatus
6
  from ..services import scrape_pipeline
7
+ from ..services.metrics_logger import save_job_metrics_to_supabase
8
+ from ..services.scrape_pipeline import ENABLE_METRICS
9
 
10
  router = APIRouter()
11
 
 
28
  "searches_run": stats.get("searches_run", 0),
29
  "pages_scraped": stats.get("pages_scraped", 0),
30
  "gaps_found": stats.get("gaps_found", 0),
31
+ "tcr_seconds": stats.get("tcr_seconds", 0.0),
32
+ "cache_hit": bool(stats.get("cache_hit", False)),
33
  }
34
+ if ENABLE_METRICS:
35
+ try:
36
+ save_job_metrics_to_supabase(
37
+ url=str(body.url),
38
+ stats=stats,
39
+ user_id=None, # user id not available in this dev endpoint
40
+ )
41
+ except Exception as exc:
42
+ print(f"⚠️ Metrics logging skipped: {exc}")
43
+
44
  return JobStatus(
45
  job_id="dev-inline",
46
  status="completed",
backend/app/main.py CHANGED
@@ -3,11 +3,6 @@ from fastapi.middleware.cors import CORSMiddleware
3
  from .api.router import api_router
4
 
5
 
6
- from fastapi.staticfiles import StaticFiles
7
- from fastapi.responses import FileResponse
8
- import os
9
-
10
-
11
  def get_application() -> FastAPI:
12
  app = FastAPI(
13
  title="ChatSMITH Backend",
@@ -29,27 +24,6 @@ def get_application() -> FastAPI:
29
  )
30
 
31
  app.include_router(api_router, prefix="/api")
32
-
33
- # Serve static files from the frontend build directory
34
- # The Dockerfile copies frontend/dist to /app/frontend/dist
35
- # In local dev, we might verify this path or stick to running frontend separately.
36
- # We use a relative path assuming we run from /app root in Docker.
37
- static_dir = os.path.join(os.path.dirname(__file__), "../../frontend/dist")
38
-
39
- if os.path.isdir(static_dir):
40
- app.mount("/assets", StaticFiles(directory=os.path.join(static_dir, "assets")), name="assets")
41
-
42
- # Catch-all route for SPA client-side routing
43
- @app.get("/{full_path:path}")
44
- async def serve_app(full_path: str):
45
- # Check if file exists in static dir (e.g. favicon.ico)
46
- file_path = os.path.join(static_dir, full_path)
47
- if os.path.isfile(file_path):
48
- return FileResponse(file_path)
49
-
50
- # Otherwise return index.html
51
- return FileResponse(os.path.join(static_dir, "index.html"))
52
-
53
  return app
54
 
55
 
 
3
  from .api.router import api_router
4
 
5
 
 
 
 
 
 
6
  def get_application() -> FastAPI:
7
  app = FastAPI(
8
  title="ChatSMITH Backend",
 
24
  )
25
 
26
  app.include_router(api_router, prefix="/api")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  return app
28
 
29
 
backend/app/services/metrics_logger.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from supabase import Client, create_client
8
+
9
+ # Default path can be overridden via METRICS_LOG_PATH; caller should guard with ENABLE_METRICS_LOGGING
10
+ LOG_PATH = Path(os.getenv("METRICS_LOG_PATH", "metrics_logs/chat_answers.jsonl"))
11
+
12
+ # Read Supabase settings (support common alt casing to avoid env mismatches)
13
+ SUPABASE_URL = (os.getenv("SUPABASE_URL") or os.getenv("supabase_url") or "").strip() or None
14
+ # Prefer service role key; fall back to anon for dev. Accept lowercase variants too.
15
+ SUPABASE_KEY = (
16
+ os.getenv("SUPABASE_SERVICE_ROLE_KEY")
17
+ or os.getenv("supabase_service_role_key")
18
+ or os.getenv("SUPABASE_ANON_KEY")
19
+ or os.getenv("supabase_anon_key")
20
+ or ""
21
+ ).strip() or None
22
+ _supabase_client: Optional[Client] = None
23
+ _warned_no_supabase = False
24
+
25
+
26
+ def _metrics_enabled() -> bool:
27
+ return (os.getenv("ENABLE_METRICS_LOGGING", "false") or "").strip().lower() == "true"
28
+
29
+
30
+ def metrics_enabled() -> bool:
31
+ """Public helper to check if metrics are enabled."""
32
+ return _metrics_enabled()
33
+
34
+
35
+ def get_supabase_client() -> Optional[Client]:
36
+ """
37
+ Return a Supabase client or None if not configured/initialization fails.
38
+ """
39
+ global _supabase_client
40
+ global _warned_no_supabase
41
+ if _supabase_client:
42
+ return _supabase_client
43
+ if not SUPABASE_URL or not SUPABASE_KEY:
44
+ if _metrics_enabled() and not _warned_no_supabase:
45
+ print("⚠️ Metrics Supabase not configured: missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY/ANON.")
46
+ _warned_no_supabase = True
47
+ return None
48
+ try:
49
+ _supabase_client = create_client(SUPABASE_URL, SUPABASE_KEY)
50
+ except Exception as exc:
51
+ print(f"⚠️ Metrics Supabase init failed: {exc}")
52
+ _supabase_client = None
53
+ return _supabase_client
54
+
55
+
56
+ def log_chat_answer(
57
+ question: str,
58
+ answer: str,
59
+ provenance: str,
60
+ user: Optional[str] = None,
61
+ log_path: Path = LOG_PATH,
62
+ ) -> None:
63
+ """
64
+ Append a single chat Q/A record as JSONL for downstream accuracy sampling.
65
+ """
66
+ try:
67
+ log_path = Path(log_path)
68
+ log_path.parent.mkdir(parents=True, exist_ok=True)
69
+ record = {
70
+ "timestamp": datetime.now(timezone.utc).isoformat(),
71
+ "question": question,
72
+ "answer": answer,
73
+ "provenance": provenance,
74
+ "user": user,
75
+ }
76
+ with log_path.open("a", encoding="utf-8") as f:
77
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
78
+ except Exception as exc:
79
+ # Best-effort logging; never break the request path
80
+ print(f"⚠️ Metrics logging failed: {exc}")
81
+
82
+
83
+ def save_job_metrics_to_supabase(url: str, stats: dict, user_id: Optional[str] = None) -> None:
84
+ """
85
+ Insert a row into metrics_job_runs. Best-effort; quietly return on failure.
86
+ """
87
+ client = get_supabase_client()
88
+ if not client:
89
+ return
90
+ try:
91
+ payload = {
92
+ "url": url,
93
+ "cache_hit": bool(stats.get("cache_hit", False)),
94
+ "tcr_seconds": float(stats.get("tcr_seconds", 0.0) or 0.0),
95
+ "searches_run": int(stats.get("searches_run", 0) or 0),
96
+ "pages_scraped": int(stats.get("pages_scraped", 0) or 0),
97
+ "gaps_found": int(stats.get("gaps_found", 0) or 0),
98
+ "user_id": user_id,
99
+ }
100
+ client.table("metrics_job_runs").insert(payload).execute()
101
+ except Exception as exc:
102
+ print(f"⚠️ Supabase job metrics insert failed: {exc}")
103
+
104
+
105
+ def save_chat_answer_to_supabase(
106
+ question: str,
107
+ answer: str,
108
+ system_prompt: str,
109
+ user_id: Optional[str] = None,
110
+ url: Optional[str] = None,
111
+ ) -> None:
112
+ """
113
+ Insert a chat answer row into metrics_chat_answers. Best-effort.
114
+ """
115
+ client = get_supabase_client()
116
+ if not client:
117
+ return
118
+ try:
119
+ provenance = "primary_plus_secondary" if "SECONDARY SOURCE" in (system_prompt or "") else "primary_only"
120
+ payload = {
121
+ "user_id": user_id,
122
+ "url": url,
123
+ "question": question,
124
+ "answer": answer,
125
+ "provenance": provenance,
126
+ }
127
+ client.table("metrics_chat_answers").insert(payload).execute()
128
+ except Exception as exc:
129
+ print(f"⚠️ Supabase chat metrics insert failed: {exc}")
backend/app/services/scrape_pipeline.py CHANGED
@@ -4,6 +4,8 @@ import json
4
  import hashlib
5
  import re
6
  import ssl
 
 
7
  from datetime import datetime
8
  from typing import List, Dict, Tuple
9
  from urllib.parse import urljoin, urlparse
@@ -18,17 +20,24 @@ from pydantic import BaseModel, Field
18
  from supabase import Client, create_client
19
  from agents import Agent, WebSearchTool, Runner
20
  from agents.model_settings import ModelSettings
 
 
 
 
21
 
22
  # Initialize
 
 
 
 
 
23
  load_dotenv(override=True)
24
  client = OpenAI()
 
25
 
26
  # Create SSL context with certifi certificates (matches notebook behavior)
27
  SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
28
 
29
- # Create knowledge_files directory if it doesn't exist
30
- os.makedirs("knowledge_files", exist_ok=True)
31
-
32
  print("✅ Imports loaded")
33
 
34
  # Supabase auth setup
@@ -410,6 +419,15 @@ async def scrape_website(url: str) -> Dict:
410
  # Step 1: Fetch homepage with retry
411
  print(" 📄 Fetching homepage...")
412
  _, homepage_html, homepage_error = await fetch_page_with_retry(session, url)
 
 
 
 
 
 
 
 
 
413
 
414
  if not homepage_html:
415
  error_msg = f"Failed to fetch homepage: {homepage_error}"
@@ -719,27 +737,34 @@ def get_cache_path(url: str) -> str:
719
  """Get the cache file path for a given URL."""
720
  url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
721
  domain = urlparse(url).netloc.replace("www.", "").replace(".", "_")
722
- return f"knowledge_files/{domain}_{url_hash}.json"
723
 
724
 
725
  def is_cached(url: str) -> bool:
726
  """Check if knowledge for a URL is already cached."""
727
- cache_path = get_cache_path(url)
728
- return os.path.exists(cache_path)
 
 
 
 
729
 
730
 
731
  def get_cached_knowledge(url: str) -> Dict | None:
732
  """Load cached knowledge if available. Returns None if not cached."""
733
- cache_path = get_cache_path(url)
734
- if os.path.exists(cache_path):
735
- try:
736
- with open(cache_path, 'r', encoding='utf-8') as f:
737
- knowledge = json.load(f)
738
- print(f"📂 Loaded from cache: {cache_path}")
739
- return knowledge
740
- except Exception as e:
741
- print(f"⚠️ Cache read error: {e}")
742
- return None
 
 
 
743
  return None
744
 
745
 
@@ -778,13 +803,14 @@ def create_knowledge_json(url: str, scraped_data: Dict, web_search_results: List
778
 
779
  def save_knowledge_json(knowledge: Dict, url: str) -> str:
780
  """Save knowledge JSON to file. Returns filepath."""
781
- filepath = get_cache_path(url)
 
782
 
783
  with open(filepath, 'w', encoding='utf-8') as f:
784
  json.dump(knowledge, f, indent=2, ensure_ascii=False)
785
 
786
  print(f"💾 Knowledge saved to: {filepath}")
787
- return filepath
788
 
789
 
790
  def load_knowledge_json(filepath: str) -> Dict:
@@ -1275,7 +1301,8 @@ async def run_full_research_new(url: str, force_refresh: bool = False, progress=
1275
  NEW workflow: Scrape first, then fill gaps with targeted searches.
1276
  With caching support and improved error handling (Phase 3).
1277
  """
1278
- stats = {"pages_scraped": 0, "searches_run": 0, "gaps_found": 0}
 
1279
  errors = [] # Track errors for UI feedback
1280
 
1281
  # ===== Check Cache First =====
@@ -1284,6 +1311,7 @@ async def run_full_research_new(url: str, force_refresh: bool = False, progress=
1284
 
1285
  cached_knowledge = get_cached_knowledge(url)
1286
  if cached_knowledge:
 
1287
  progress(0.9, desc="Preparing chatbot from cache...")
1288
 
1289
  # Extract name from cached data
@@ -1308,6 +1336,8 @@ RULES:
1308
 
1309
  === END ===
1310
  """
 
 
1311
  progress(1.0, desc="Done (from cache)!")
1312
  status_text = build_status_new(100, current_step=4, selected_name=raw_name,
1313
  finished=True, stats=stats, from_cache=True)
@@ -1315,7 +1345,7 @@ RULES:
1315
  msg_update = gr.update(interactive=True, placeholder="Ask anything about the website...")
1316
  send_btn_update = gr.update(interactive=True)
1317
 
1318
- return status_text, system_prompt, raw_name, [], msg_update, send_btn_update
1319
 
1320
  # ===== Step 1: Scrape Website (PRIMARY SOURCE) =====
1321
  progress(0.05, desc="Scraping website...")
@@ -1454,6 +1484,8 @@ RULES:
1454
  """
1455
 
1456
  progress(1.0, desc="Done!")
 
 
1457
  status_text = build_status_new(100, current_step=4, selected_name=raw_name,
1458
  finished=True, stats=stats, errors=errors)
1459
 
@@ -1535,6 +1567,30 @@ def chat_fn(message, history, system_prompt, name, user=None):
1535
  print(f"❌ Chat error: {e}")
1536
  answer = f"⚠️ Sorry, there was an error generating a response. Please try again.\n\nError: {str(e)[:100]}"
1537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1538
  # Return in Gradio 6.x format
1539
  return "", history + [
1540
  {"role": "user", "content": message},
 
4
  import hashlib
5
  import re
6
  import ssl
7
+ import time
8
+ from pathlib import Path
9
  from datetime import datetime
10
  from typing import List, Dict, Tuple
11
  from urllib.parse import urljoin, urlparse
 
20
  from supabase import Client, create_client
21
  from agents import Agent, WebSearchTool, Runner
22
  from agents.model_settings import ModelSettings
23
+ from .metrics_logger import (
24
+ log_chat_answer,
25
+ save_chat_answer_to_supabase,
26
+ )
27
 
28
  # Initialize
29
+ # Knowledge base directory (consistent absolute path to avoid cwd issues)
30
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
31
+ KNOWLEDGE_DIR = PROJECT_ROOT / "knowledge_files"
32
+ KNOWLEDGE_DIR.mkdir(parents=True, exist_ok=True)
33
+
34
  load_dotenv(override=True)
35
  client = OpenAI()
36
+ ENABLE_METRICS = (os.getenv("ENABLE_METRICS_LOGGING", "false") or "").strip().lower() == "true"
37
 
38
  # Create SSL context with certifi certificates (matches notebook behavior)
39
  SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
40
 
 
 
 
41
  print("✅ Imports loaded")
42
 
43
  # Supabase auth setup
 
419
  # Step 1: Fetch homepage with retry
420
  print(" 📄 Fetching homepage...")
421
  _, homepage_html, homepage_error = await fetch_page_with_retry(session, url)
422
+
423
+ # Fallback: if HTTPS failed, try HTTP (some sites block/redirect HTTPS)
424
+ if not homepage_html and original_url.startswith("https://"):
425
+ fallback_url = "http://" + original_url[len("https://"):]
426
+ print(f" 🔁 HTTPS fetch failed, retrying with HTTP: {fallback_url}")
427
+ _, homepage_html, homepage_error = await fetch_page_with_retry(session, fallback_url)
428
+ if homepage_html:
429
+ url = fallback_url.rstrip('/')
430
+ results["source_url"] = url
431
 
432
  if not homepage_html:
433
  error_msg = f"Failed to fetch homepage: {homepage_error}"
 
737
  """Get the cache file path for a given URL."""
738
  url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
739
  domain = urlparse(url).netloc.replace("www.", "").replace(".", "_")
740
+ return str(KNOWLEDGE_DIR / f"{domain}_{url_hash}.json")
741
 
742
 
743
  def is_cached(url: str) -> bool:
744
  """Check if knowledge for a URL is already cached."""
745
+ cache_path = Path(get_cache_path(url))
746
+ if cache_path.exists():
747
+ return True
748
+ # Backward compatibility: check legacy relative path if different
749
+ legacy = Path("knowledge_files") / cache_path.name
750
+ return legacy.exists()
751
 
752
 
753
  def get_cached_knowledge(url: str) -> Dict | None:
754
  """Load cached knowledge if available. Returns None if not cached."""
755
+ paths = [Path(get_cache_path(url))]
756
+ # Add legacy relative path as fallback
757
+ paths.append(Path("knowledge_files") / paths[0].name)
758
+ for cache_path in paths:
759
+ if cache_path.exists():
760
+ try:
761
+ with open(cache_path, 'r', encoding='utf-8') as f:
762
+ knowledge = json.load(f)
763
+ print(f"📂 Loaded from cache: {cache_path}")
764
+ return knowledge
765
+ except Exception as e:
766
+ print(f"⚠️ Cache read error ({cache_path}): {e}")
767
+ continue
768
  return None
769
 
770
 
 
803
 
804
  def save_knowledge_json(knowledge: Dict, url: str) -> str:
805
  """Save knowledge JSON to file. Returns filepath."""
806
+ filepath = Path(get_cache_path(url))
807
+ filepath.parent.mkdir(parents=True, exist_ok=True)
808
 
809
  with open(filepath, 'w', encoding='utf-8') as f:
810
  json.dump(knowledge, f, indent=2, ensure_ascii=False)
811
 
812
  print(f"💾 Knowledge saved to: {filepath}")
813
+ return str(filepath)
814
 
815
 
816
  def load_knowledge_json(filepath: str) -> Dict:
 
1301
  NEW workflow: Scrape first, then fill gaps with targeted searches.
1302
  With caching support and improved error handling (Phase 3).
1303
  """
1304
+ stats = {"pages_scraped": 0, "searches_run": 0, "gaps_found": 0, "cache_hit": False}
1305
+ start_time = time.time() if ENABLE_METRICS else None
1306
  errors = [] # Track errors for UI feedback
1307
 
1308
  # ===== Check Cache First =====
 
1311
 
1312
  cached_knowledge = get_cached_knowledge(url)
1313
  if cached_knowledge:
1314
+ stats["cache_hit"] = True
1315
  progress(0.9, desc="Preparing chatbot from cache...")
1316
 
1317
  # Extract name from cached data
 
1336
 
1337
  === END ===
1338
  """
1339
+ if start_time is not None:
1340
+ stats["tcr_seconds"] = time.time() - start_time
1341
  progress(1.0, desc="Done (from cache)!")
1342
  status_text = build_status_new(100, current_step=4, selected_name=raw_name,
1343
  finished=True, stats=stats, from_cache=True)
 
1345
  msg_update = gr.update(interactive=True, placeholder="Ask anything about the website...")
1346
  send_btn_update = gr.update(interactive=True)
1347
 
1348
+ return status_text, system_prompt, raw_name, [], msg_update, send_btn_update, stats
1349
 
1350
  # ===== Step 1: Scrape Website (PRIMARY SOURCE) =====
1351
  progress(0.05, desc="Scraping website...")
 
1484
  """
1485
 
1486
  progress(1.0, desc="Done!")
1487
+ if start_time is not None:
1488
+ stats["tcr_seconds"] = time.time() - start_time
1489
  status_text = build_status_new(100, current_step=4, selected_name=raw_name,
1490
  finished=True, stats=stats, errors=errors)
1491
 
 
1567
  print(f"❌ Chat error: {e}")
1568
  answer = f"⚠️ Sorry, there was an error generating a response. Please try again.\n\nError: {str(e)[:100]}"
1569
 
1570
+ if ENABLE_METRICS:
1571
+ provenance = "primary_plus_secondary" if system_prompt and "SECONDARY SOURCE" in system_prompt else "primary_only"
1572
+ user_email = None
1573
+ if isinstance(user, dict):
1574
+ user_email = user.get("email")
1575
+ else:
1576
+ user_email = getattr(user, "email", None)
1577
+ log_chat_answer(
1578
+ question=message,
1579
+ answer=answer,
1580
+ provenance=provenance,
1581
+ user=user_email,
1582
+ )
1583
+ try:
1584
+ save_chat_answer_to_supabase(
1585
+ question=message,
1586
+ answer=answer,
1587
+ system_prompt=system_prompt,
1588
+ user_id=user_email,
1589
+ url=None, # URL not available in this handler
1590
+ )
1591
+ except Exception as exc:
1592
+ print(f"⚠️ Supabase chat metrics skipped: {exc}")
1593
+
1594
  # Return in Gradio 6.x format
1595
  return "", history + [
1596
  {"role": "user", "content": message},
deployment_guide.md CHANGED
@@ -31,9 +31,12 @@ You can upload files via the web interface or use Git. Since you have the code l
31
  #### Option A: Drag and Drop (Easiest for one-off)
32
  1. Go to the **Files** tab of your Space.
33
  2. Click **Add file** -> **Upload files**.
34
- 3. Drag and drop **ALL** files and folders from your `chatsmith-main` folder (backend, frontend, Dockerfile, requirements.txt, etc.).
35
- - *Note: You can skip `node_modules`, `venv`, `.git` folders.*
36
- 4. Commit the changes.
 
 
 
37
 
38
  #### Option B: Git (Recommended)
39
  1. In your local terminal, initialize git if not already:
@@ -50,6 +53,12 @@ You can upload files via the web interface or use Git. Since you have the code l
50
  ```bash
51
  git push --force space master:main
52
  ```
 
 
 
 
 
 
53
 
54
  ### 4. Wait for Build
55
  - Once files are uploaded, Hugging Face will automatically detect the `Dockerfile` and start building.
 
31
  #### Option A: Drag and Drop (Easiest for one-off)
32
  1. Go to the **Files** tab of your Space.
33
  2. Click **Add file** -> **Upload files**.
34
+ 3. Open your local `chatsmith-main` folder.
35
+ 4. Select **all files inside** (backend, frontend, Dockerfile, etc.).
36
+ 5. Drag and drop them into the web interface.
37
+ - **Important**: Do NOT drag the `chatsmith-main` folder itself. Drag the *contents*.
38
+ - *Tip: You can skip the `node_modules` folder if it exists, as it will be recreated by Docker.*
39
+ 6. Commit the changes.
40
 
41
  #### Option B: Git (Recommended)
42
  1. In your local terminal, initialize git if not already:
 
53
  ```bash
54
  git push --force space master:main
55
  ```
56
+ *Note: If asked for a password, you must use a **Hugging Face Access Token** with `write` permissions (Settings -> Access Tokens), NOT your account password.*
57
+
58
+ **Authentication Helper (if needed):**
59
+ ```bash
60
+ git config --global credential.helper store
61
+ ```
62
 
63
  ### 4. Wait for Build
64
  - Once files are uploaded, Hugging Face will automatically detect the `Dockerfile` and start building.
frontend/package-lock.json CHANGED
@@ -8,15 +8,15 @@
8
  "name": "chatsmith-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
- "@supabase/supabase-js": "^2.45.3",
12
- "react": "^18.3.1",
13
- "react-dom": "^18.3.1"
14
  },
15
  "devDependencies": {
16
- "@types/react": "^18.3.11",
17
- "@types/react-dom": "^18.3.0",
18
- "@vitejs/plugin-react": "^4.3.3",
19
- "vite": "^5.4.8"
20
  }
21
  },
22
  "node_modules/@babel/code-frame": {
@@ -302,9 +302,9 @@
302
  }
303
  },
304
  "node_modules/@esbuild/aix-ppc64": {
305
- "version": "0.21.5",
306
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
307
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
308
  "cpu": [
309
  "ppc64"
310
  ],
@@ -315,13 +315,13 @@
315
  "aix"
316
  ],
317
  "engines": {
318
- "node": ">=12"
319
  }
320
  },
321
  "node_modules/@esbuild/android-arm": {
322
- "version": "0.21.5",
323
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
324
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
325
  "cpu": [
326
  "arm"
327
  ],
@@ -332,13 +332,13 @@
332
  "android"
333
  ],
334
  "engines": {
335
- "node": ">=12"
336
  }
337
  },
338
  "node_modules/@esbuild/android-arm64": {
339
- "version": "0.21.5",
340
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
341
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
342
  "cpu": [
343
  "arm64"
344
  ],
@@ -349,13 +349,13 @@
349
  "android"
350
  ],
351
  "engines": {
352
- "node": ">=12"
353
  }
354
  },
355
  "node_modules/@esbuild/android-x64": {
356
- "version": "0.21.5",
357
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
358
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
359
  "cpu": [
360
  "x64"
361
  ],
@@ -366,13 +366,13 @@
366
  "android"
367
  ],
368
  "engines": {
369
- "node": ">=12"
370
  }
371
  },
372
  "node_modules/@esbuild/darwin-arm64": {
373
- "version": "0.21.5",
374
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
375
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
376
  "cpu": [
377
  "arm64"
378
  ],
@@ -383,13 +383,13 @@
383
  "darwin"
384
  ],
385
  "engines": {
386
- "node": ">=12"
387
  }
388
  },
389
  "node_modules/@esbuild/darwin-x64": {
390
- "version": "0.21.5",
391
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
392
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
393
  "cpu": [
394
  "x64"
395
  ],
@@ -400,13 +400,13 @@
400
  "darwin"
401
  ],
402
  "engines": {
403
- "node": ">=12"
404
  }
405
  },
406
  "node_modules/@esbuild/freebsd-arm64": {
407
- "version": "0.21.5",
408
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
409
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
410
  "cpu": [
411
  "arm64"
412
  ],
@@ -417,13 +417,13 @@
417
  "freebsd"
418
  ],
419
  "engines": {
420
- "node": ">=12"
421
  }
422
  },
423
  "node_modules/@esbuild/freebsd-x64": {
424
- "version": "0.21.5",
425
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
426
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
427
  "cpu": [
428
  "x64"
429
  ],
@@ -434,13 +434,13 @@
434
  "freebsd"
435
  ],
436
  "engines": {
437
- "node": ">=12"
438
  }
439
  },
440
  "node_modules/@esbuild/linux-arm": {
441
- "version": "0.21.5",
442
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
443
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
444
  "cpu": [
445
  "arm"
446
  ],
@@ -451,13 +451,13 @@
451
  "linux"
452
  ],
453
  "engines": {
454
- "node": ">=12"
455
  }
456
  },
457
  "node_modules/@esbuild/linux-arm64": {
458
- "version": "0.21.5",
459
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
460
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
461
  "cpu": [
462
  "arm64"
463
  ],
@@ -468,13 +468,13 @@
468
  "linux"
469
  ],
470
  "engines": {
471
- "node": ">=12"
472
  }
473
  },
474
  "node_modules/@esbuild/linux-ia32": {
475
- "version": "0.21.5",
476
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
477
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
478
  "cpu": [
479
  "ia32"
480
  ],
@@ -485,13 +485,13 @@
485
  "linux"
486
  ],
487
  "engines": {
488
- "node": ">=12"
489
  }
490
  },
491
  "node_modules/@esbuild/linux-loong64": {
492
- "version": "0.21.5",
493
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
494
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
495
  "cpu": [
496
  "loong64"
497
  ],
@@ -502,13 +502,13 @@
502
  "linux"
503
  ],
504
  "engines": {
505
- "node": ">=12"
506
  }
507
  },
508
  "node_modules/@esbuild/linux-mips64el": {
509
- "version": "0.21.5",
510
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
511
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
512
  "cpu": [
513
  "mips64el"
514
  ],
@@ -519,13 +519,13 @@
519
  "linux"
520
  ],
521
  "engines": {
522
- "node": ">=12"
523
  }
524
  },
525
  "node_modules/@esbuild/linux-ppc64": {
526
- "version": "0.21.5",
527
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
528
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
529
  "cpu": [
530
  "ppc64"
531
  ],
@@ -536,13 +536,13 @@
536
  "linux"
537
  ],
538
  "engines": {
539
- "node": ">=12"
540
  }
541
  },
542
  "node_modules/@esbuild/linux-riscv64": {
543
- "version": "0.21.5",
544
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
545
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
546
  "cpu": [
547
  "riscv64"
548
  ],
@@ -553,13 +553,13 @@
553
  "linux"
554
  ],
555
  "engines": {
556
- "node": ">=12"
557
  }
558
  },
559
  "node_modules/@esbuild/linux-s390x": {
560
- "version": "0.21.5",
561
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
562
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
563
  "cpu": [
564
  "s390x"
565
  ],
@@ -570,13 +570,13 @@
570
  "linux"
571
  ],
572
  "engines": {
573
- "node": ">=12"
574
  }
575
  },
576
  "node_modules/@esbuild/linux-x64": {
577
- "version": "0.21.5",
578
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
579
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
580
  "cpu": [
581
  "x64"
582
  ],
@@ -587,13 +587,30 @@
587
  "linux"
588
  ],
589
  "engines": {
590
- "node": ">=12"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  }
592
  },
593
  "node_modules/@esbuild/netbsd-x64": {
594
- "version": "0.21.5",
595
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
596
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
597
  "cpu": [
598
  "x64"
599
  ],
@@ -604,13 +621,30 @@
604
  "netbsd"
605
  ],
606
  "engines": {
607
- "node": ">=12"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  }
609
  },
610
  "node_modules/@esbuild/openbsd-x64": {
611
- "version": "0.21.5",
612
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
613
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
614
  "cpu": [
615
  "x64"
616
  ],
@@ -621,13 +655,30 @@
621
  "openbsd"
622
  ],
623
  "engines": {
624
- "node": ">=12"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  }
626
  },
627
  "node_modules/@esbuild/sunos-x64": {
628
- "version": "0.21.5",
629
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
630
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
631
  "cpu": [
632
  "x64"
633
  ],
@@ -638,13 +689,13 @@
638
  "sunos"
639
  ],
640
  "engines": {
641
- "node": ">=12"
642
  }
643
  },
644
  "node_modules/@esbuild/win32-arm64": {
645
- "version": "0.21.5",
646
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
647
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
648
  "cpu": [
649
  "arm64"
650
  ],
@@ -655,13 +706,13 @@
655
  "win32"
656
  ],
657
  "engines": {
658
- "node": ">=12"
659
  }
660
  },
661
  "node_modules/@esbuild/win32-ia32": {
662
- "version": "0.21.5",
663
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
664
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
665
  "cpu": [
666
  "ia32"
667
  ],
@@ -672,13 +723,13 @@
672
  "win32"
673
  ],
674
  "engines": {
675
- "node": ">=12"
676
  }
677
  },
678
  "node_modules/@esbuild/win32-x64": {
679
- "version": "0.21.5",
680
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
681
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
682
  "cpu": [
683
  "x64"
684
  ],
@@ -689,7 +740,7 @@
689
  "win32"
690
  ],
691
  "engines": {
692
- "node": ">=12"
693
  }
694
  },
695
  "node_modules/@jridgewell/gen-mapping": {
@@ -743,9 +794,9 @@
743
  }
744
  },
745
  "node_modules/@rolldown/pluginutils": {
746
- "version": "1.0.0-beta.27",
747
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
748
- "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
749
  "dev": true,
750
  "license": "MIT"
751
  },
@@ -1204,32 +1255,24 @@
1204
  "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
1205
  "license": "MIT"
1206
  },
1207
- "node_modules/@types/prop-types": {
1208
- "version": "15.7.15",
1209
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1210
- "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1211
- "dev": true,
1212
- "license": "MIT"
1213
- },
1214
  "node_modules/@types/react": {
1215
- "version": "18.3.27",
1216
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
1217
- "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
1218
  "dev": true,
1219
  "license": "MIT",
1220
  "dependencies": {
1221
- "@types/prop-types": "*",
1222
  "csstype": "^3.2.2"
1223
  }
1224
  },
1225
  "node_modules/@types/react-dom": {
1226
- "version": "18.3.7",
1227
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1228
- "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1229
  "dev": true,
1230
  "license": "MIT",
1231
  "peerDependencies": {
1232
- "@types/react": "^18.0.0"
1233
  }
1234
  },
1235
  "node_modules/@types/ws": {
@@ -1242,21 +1285,21 @@
1242
  }
1243
  },
1244
  "node_modules/@vitejs/plugin-react": {
1245
- "version": "4.7.0",
1246
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1247
- "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1248
  "dev": true,
1249
  "license": "MIT",
1250
  "dependencies": {
1251
- "@babel/core": "^7.28.0",
1252
  "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1253
  "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1254
- "@rolldown/pluginutils": "1.0.0-beta.27",
1255
  "@types/babel__core": "^7.20.5",
1256
- "react-refresh": "^0.17.0"
1257
  },
1258
  "engines": {
1259
- "node": "^14.18.0 || >=16.0.0"
1260
  },
1261
  "peerDependencies": {
1262
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
@@ -1367,9 +1410,9 @@
1367
  "license": "ISC"
1368
  },
1369
  "node_modules/esbuild": {
1370
- "version": "0.21.5",
1371
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1372
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1373
  "dev": true,
1374
  "hasInstallScript": true,
1375
  "license": "MIT",
@@ -1377,32 +1420,35 @@
1377
  "esbuild": "bin/esbuild"
1378
  },
1379
  "engines": {
1380
- "node": ">=12"
1381
  },
1382
  "optionalDependencies": {
1383
- "@esbuild/aix-ppc64": "0.21.5",
1384
- "@esbuild/android-arm": "0.21.5",
1385
- "@esbuild/android-arm64": "0.21.5",
1386
- "@esbuild/android-x64": "0.21.5",
1387
- "@esbuild/darwin-arm64": "0.21.5",
1388
- "@esbuild/darwin-x64": "0.21.5",
1389
- "@esbuild/freebsd-arm64": "0.21.5",
1390
- "@esbuild/freebsd-x64": "0.21.5",
1391
- "@esbuild/linux-arm": "0.21.5",
1392
- "@esbuild/linux-arm64": "0.21.5",
1393
- "@esbuild/linux-ia32": "0.21.5",
1394
- "@esbuild/linux-loong64": "0.21.5",
1395
- "@esbuild/linux-mips64el": "0.21.5",
1396
- "@esbuild/linux-ppc64": "0.21.5",
1397
- "@esbuild/linux-riscv64": "0.21.5",
1398
- "@esbuild/linux-s390x": "0.21.5",
1399
- "@esbuild/linux-x64": "0.21.5",
1400
- "@esbuild/netbsd-x64": "0.21.5",
1401
- "@esbuild/openbsd-x64": "0.21.5",
1402
- "@esbuild/sunos-x64": "0.21.5",
1403
- "@esbuild/win32-arm64": "0.21.5",
1404
- "@esbuild/win32-ia32": "0.21.5",
1405
- "@esbuild/win32-x64": "0.21.5"
 
 
 
1406
  }
1407
  },
1408
  "node_modules/escalade": {
@@ -1415,6 +1461,24 @@
1415
  "node": ">=6"
1416
  }
1417
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1418
  "node_modules/fsevents": {
1419
  "version": "2.3.3",
1420
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1453,6 +1517,7 @@
1453
  "version": "4.0.0",
1454
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1455
  "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
 
1456
  "license": "MIT"
1457
  },
1458
  "node_modules/jsesc": {
@@ -1481,18 +1546,6 @@
1481
  "node": ">=6"
1482
  }
1483
  },
1484
- "node_modules/loose-envify": {
1485
- "version": "1.4.0",
1486
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1487
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1488
- "license": "MIT",
1489
- "dependencies": {
1490
- "js-tokens": "^3.0.0 || ^4.0.0"
1491
- },
1492
- "bin": {
1493
- "loose-envify": "cli.js"
1494
- }
1495
- },
1496
  "node_modules/lru-cache": {
1497
  "version": "5.1.1",
1498
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -1543,6 +1596,19 @@
1543
  "dev": true,
1544
  "license": "ISC"
1545
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1546
  "node_modules/postcss": {
1547
  "version": "8.5.6",
1548
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -1573,34 +1639,30 @@
1573
  }
1574
  },
1575
  "node_modules/react": {
1576
- "version": "18.3.1",
1577
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1578
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1579
  "license": "MIT",
1580
- "dependencies": {
1581
- "loose-envify": "^1.1.0"
1582
- },
1583
  "engines": {
1584
  "node": ">=0.10.0"
1585
  }
1586
  },
1587
  "node_modules/react-dom": {
1588
- "version": "18.3.1",
1589
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1590
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1591
  "license": "MIT",
1592
  "dependencies": {
1593
- "loose-envify": "^1.1.0",
1594
- "scheduler": "^0.23.2"
1595
  },
1596
  "peerDependencies": {
1597
- "react": "^18.3.1"
1598
  }
1599
  },
1600
  "node_modules/react-refresh": {
1601
- "version": "0.17.0",
1602
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1603
- "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1604
  "dev": true,
1605
  "license": "MIT",
1606
  "engines": {
@@ -1650,13 +1712,10 @@
1650
  }
1651
  },
1652
  "node_modules/scheduler": {
1653
- "version": "0.23.2",
1654
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1655
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1656
- "license": "MIT",
1657
- "dependencies": {
1658
- "loose-envify": "^1.1.0"
1659
- }
1660
  },
1661
  "node_modules/semver": {
1662
  "version": "6.3.1",
@@ -1678,6 +1737,23 @@
1678
  "node": ">=0.10.0"
1679
  }
1680
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1681
  "node_modules/tslib": {
1682
  "version": "2.8.1",
1683
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -1722,21 +1798,24 @@
1722
  }
1723
  },
1724
  "node_modules/vite": {
1725
- "version": "5.4.21",
1726
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1727
- "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1728
  "dev": true,
1729
  "license": "MIT",
1730
  "dependencies": {
1731
- "esbuild": "^0.21.3",
1732
- "postcss": "^8.4.43",
1733
- "rollup": "^4.20.0"
 
 
 
1734
  },
1735
  "bin": {
1736
  "vite": "bin/vite.js"
1737
  },
1738
  "engines": {
1739
- "node": "^18.0.0 || >=20.0.0"
1740
  },
1741
  "funding": {
1742
  "url": "https://github.com/vitejs/vite?sponsor=1"
@@ -1745,19 +1824,25 @@
1745
  "fsevents": "~2.3.3"
1746
  },
1747
  "peerDependencies": {
1748
- "@types/node": "^18.0.0 || >=20.0.0",
1749
- "less": "*",
 
1750
  "lightningcss": "^1.21.0",
1751
- "sass": "*",
1752
- "sass-embedded": "*",
1753
- "stylus": "*",
1754
- "sugarss": "*",
1755
- "terser": "^5.4.0"
 
 
1756
  },
1757
  "peerDependenciesMeta": {
1758
  "@types/node": {
1759
  "optional": true
1760
  },
 
 
 
1761
  "less": {
1762
  "optional": true
1763
  },
@@ -1778,6 +1863,12 @@
1778
  },
1779
  "terser": {
1780
  "optional": true
 
 
 
 
 
 
1781
  }
1782
  }
1783
  },
 
8
  "name": "chatsmith-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@supabase/supabase-js": "^2.86.2",
12
+ "react": "^19.2.1",
13
+ "react-dom": "^19.2.1"
14
  },
15
  "devDependencies": {
16
+ "@types/react": "^19.2.7",
17
+ "@types/react-dom": "^19.2.3",
18
+ "@vitejs/plugin-react": "^5.1.1",
19
+ "vite": "^7.2.6"
20
  }
21
  },
22
  "node_modules/@babel/code-frame": {
 
302
  }
303
  },
304
  "node_modules/@esbuild/aix-ppc64": {
305
+ "version": "0.25.12",
306
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
307
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
308
  "cpu": [
309
  "ppc64"
310
  ],
 
315
  "aix"
316
  ],
317
  "engines": {
318
+ "node": ">=18"
319
  }
320
  },
321
  "node_modules/@esbuild/android-arm": {
322
+ "version": "0.25.12",
323
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
324
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
325
  "cpu": [
326
  "arm"
327
  ],
 
332
  "android"
333
  ],
334
  "engines": {
335
+ "node": ">=18"
336
  }
337
  },
338
  "node_modules/@esbuild/android-arm64": {
339
+ "version": "0.25.12",
340
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
341
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
342
  "cpu": [
343
  "arm64"
344
  ],
 
349
  "android"
350
  ],
351
  "engines": {
352
+ "node": ">=18"
353
  }
354
  },
355
  "node_modules/@esbuild/android-x64": {
356
+ "version": "0.25.12",
357
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
358
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
359
  "cpu": [
360
  "x64"
361
  ],
 
366
  "android"
367
  ],
368
  "engines": {
369
+ "node": ">=18"
370
  }
371
  },
372
  "node_modules/@esbuild/darwin-arm64": {
373
+ "version": "0.25.12",
374
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
375
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
376
  "cpu": [
377
  "arm64"
378
  ],
 
383
  "darwin"
384
  ],
385
  "engines": {
386
+ "node": ">=18"
387
  }
388
  },
389
  "node_modules/@esbuild/darwin-x64": {
390
+ "version": "0.25.12",
391
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
392
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
393
  "cpu": [
394
  "x64"
395
  ],
 
400
  "darwin"
401
  ],
402
  "engines": {
403
+ "node": ">=18"
404
  }
405
  },
406
  "node_modules/@esbuild/freebsd-arm64": {
407
+ "version": "0.25.12",
408
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
409
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
410
  "cpu": [
411
  "arm64"
412
  ],
 
417
  "freebsd"
418
  ],
419
  "engines": {
420
+ "node": ">=18"
421
  }
422
  },
423
  "node_modules/@esbuild/freebsd-x64": {
424
+ "version": "0.25.12",
425
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
426
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
427
  "cpu": [
428
  "x64"
429
  ],
 
434
  "freebsd"
435
  ],
436
  "engines": {
437
+ "node": ">=18"
438
  }
439
  },
440
  "node_modules/@esbuild/linux-arm": {
441
+ "version": "0.25.12",
442
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
443
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
444
  "cpu": [
445
  "arm"
446
  ],
 
451
  "linux"
452
  ],
453
  "engines": {
454
+ "node": ">=18"
455
  }
456
  },
457
  "node_modules/@esbuild/linux-arm64": {
458
+ "version": "0.25.12",
459
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
460
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
461
  "cpu": [
462
  "arm64"
463
  ],
 
468
  "linux"
469
  ],
470
  "engines": {
471
+ "node": ">=18"
472
  }
473
  },
474
  "node_modules/@esbuild/linux-ia32": {
475
+ "version": "0.25.12",
476
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
477
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
478
  "cpu": [
479
  "ia32"
480
  ],
 
485
  "linux"
486
  ],
487
  "engines": {
488
+ "node": ">=18"
489
  }
490
  },
491
  "node_modules/@esbuild/linux-loong64": {
492
+ "version": "0.25.12",
493
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
494
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
495
  "cpu": [
496
  "loong64"
497
  ],
 
502
  "linux"
503
  ],
504
  "engines": {
505
+ "node": ">=18"
506
  }
507
  },
508
  "node_modules/@esbuild/linux-mips64el": {
509
+ "version": "0.25.12",
510
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
511
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
512
  "cpu": [
513
  "mips64el"
514
  ],
 
519
  "linux"
520
  ],
521
  "engines": {
522
+ "node": ">=18"
523
  }
524
  },
525
  "node_modules/@esbuild/linux-ppc64": {
526
+ "version": "0.25.12",
527
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
528
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
529
  "cpu": [
530
  "ppc64"
531
  ],
 
536
  "linux"
537
  ],
538
  "engines": {
539
+ "node": ">=18"
540
  }
541
  },
542
  "node_modules/@esbuild/linux-riscv64": {
543
+ "version": "0.25.12",
544
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
545
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
546
  "cpu": [
547
  "riscv64"
548
  ],
 
553
  "linux"
554
  ],
555
  "engines": {
556
+ "node": ">=18"
557
  }
558
  },
559
  "node_modules/@esbuild/linux-s390x": {
560
+ "version": "0.25.12",
561
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
562
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
563
  "cpu": [
564
  "s390x"
565
  ],
 
570
  "linux"
571
  ],
572
  "engines": {
573
+ "node": ">=18"
574
  }
575
  },
576
  "node_modules/@esbuild/linux-x64": {
577
+ "version": "0.25.12",
578
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
579
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
580
  "cpu": [
581
  "x64"
582
  ],
 
587
  "linux"
588
  ],
589
  "engines": {
590
+ "node": ">=18"
591
+ }
592
+ },
593
+ "node_modules/@esbuild/netbsd-arm64": {
594
+ "version": "0.25.12",
595
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
596
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
597
+ "cpu": [
598
+ "arm64"
599
+ ],
600
+ "dev": true,
601
+ "license": "MIT",
602
+ "optional": true,
603
+ "os": [
604
+ "netbsd"
605
+ ],
606
+ "engines": {
607
+ "node": ">=18"
608
  }
609
  },
610
  "node_modules/@esbuild/netbsd-x64": {
611
+ "version": "0.25.12",
612
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
613
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
614
  "cpu": [
615
  "x64"
616
  ],
 
621
  "netbsd"
622
  ],
623
  "engines": {
624
+ "node": ">=18"
625
+ }
626
+ },
627
+ "node_modules/@esbuild/openbsd-arm64": {
628
+ "version": "0.25.12",
629
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
630
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
631
+ "cpu": [
632
+ "arm64"
633
+ ],
634
+ "dev": true,
635
+ "license": "MIT",
636
+ "optional": true,
637
+ "os": [
638
+ "openbsd"
639
+ ],
640
+ "engines": {
641
+ "node": ">=18"
642
  }
643
  },
644
  "node_modules/@esbuild/openbsd-x64": {
645
+ "version": "0.25.12",
646
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
647
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
648
  "cpu": [
649
  "x64"
650
  ],
 
655
  "openbsd"
656
  ],
657
  "engines": {
658
+ "node": ">=18"
659
+ }
660
+ },
661
+ "node_modules/@esbuild/openharmony-arm64": {
662
+ "version": "0.25.12",
663
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
664
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
665
+ "cpu": [
666
+ "arm64"
667
+ ],
668
+ "dev": true,
669
+ "license": "MIT",
670
+ "optional": true,
671
+ "os": [
672
+ "openharmony"
673
+ ],
674
+ "engines": {
675
+ "node": ">=18"
676
  }
677
  },
678
  "node_modules/@esbuild/sunos-x64": {
679
+ "version": "0.25.12",
680
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
681
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
682
  "cpu": [
683
  "x64"
684
  ],
 
689
  "sunos"
690
  ],
691
  "engines": {
692
+ "node": ">=18"
693
  }
694
  },
695
  "node_modules/@esbuild/win32-arm64": {
696
+ "version": "0.25.12",
697
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
698
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
699
  "cpu": [
700
  "arm64"
701
  ],
 
706
  "win32"
707
  ],
708
  "engines": {
709
+ "node": ">=18"
710
  }
711
  },
712
  "node_modules/@esbuild/win32-ia32": {
713
+ "version": "0.25.12",
714
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
715
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
716
  "cpu": [
717
  "ia32"
718
  ],
 
723
  "win32"
724
  ],
725
  "engines": {
726
+ "node": ">=18"
727
  }
728
  },
729
  "node_modules/@esbuild/win32-x64": {
730
+ "version": "0.25.12",
731
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
732
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
733
  "cpu": [
734
  "x64"
735
  ],
 
740
  "win32"
741
  ],
742
  "engines": {
743
+ "node": ">=18"
744
  }
745
  },
746
  "node_modules/@jridgewell/gen-mapping": {
 
794
  }
795
  },
796
  "node_modules/@rolldown/pluginutils": {
797
+ "version": "1.0.0-beta.47",
798
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
799
+ "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==",
800
  "dev": true,
801
  "license": "MIT"
802
  },
 
1255
  "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
1256
  "license": "MIT"
1257
  },
 
 
 
 
 
 
 
1258
  "node_modules/@types/react": {
1259
+ "version": "19.2.7",
1260
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
1261
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
1262
  "dev": true,
1263
  "license": "MIT",
1264
  "dependencies": {
 
1265
  "csstype": "^3.2.2"
1266
  }
1267
  },
1268
  "node_modules/@types/react-dom": {
1269
+ "version": "19.2.3",
1270
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
1271
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
1272
  "dev": true,
1273
  "license": "MIT",
1274
  "peerDependencies": {
1275
+ "@types/react": "^19.2.0"
1276
  }
1277
  },
1278
  "node_modules/@types/ws": {
 
1285
  }
1286
  },
1287
  "node_modules/@vitejs/plugin-react": {
1288
+ "version": "5.1.1",
1289
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
1290
+ "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==",
1291
  "dev": true,
1292
  "license": "MIT",
1293
  "dependencies": {
1294
+ "@babel/core": "^7.28.5",
1295
  "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1296
  "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1297
+ "@rolldown/pluginutils": "1.0.0-beta.47",
1298
  "@types/babel__core": "^7.20.5",
1299
+ "react-refresh": "^0.18.0"
1300
  },
1301
  "engines": {
1302
+ "node": "^20.19.0 || >=22.12.0"
1303
  },
1304
  "peerDependencies": {
1305
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
 
1410
  "license": "ISC"
1411
  },
1412
  "node_modules/esbuild": {
1413
+ "version": "0.25.12",
1414
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1415
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1416
  "dev": true,
1417
  "hasInstallScript": true,
1418
  "license": "MIT",
 
1420
  "esbuild": "bin/esbuild"
1421
  },
1422
  "engines": {
1423
+ "node": ">=18"
1424
  },
1425
  "optionalDependencies": {
1426
+ "@esbuild/aix-ppc64": "0.25.12",
1427
+ "@esbuild/android-arm": "0.25.12",
1428
+ "@esbuild/android-arm64": "0.25.12",
1429
+ "@esbuild/android-x64": "0.25.12",
1430
+ "@esbuild/darwin-arm64": "0.25.12",
1431
+ "@esbuild/darwin-x64": "0.25.12",
1432
+ "@esbuild/freebsd-arm64": "0.25.12",
1433
+ "@esbuild/freebsd-x64": "0.25.12",
1434
+ "@esbuild/linux-arm": "0.25.12",
1435
+ "@esbuild/linux-arm64": "0.25.12",
1436
+ "@esbuild/linux-ia32": "0.25.12",
1437
+ "@esbuild/linux-loong64": "0.25.12",
1438
+ "@esbuild/linux-mips64el": "0.25.12",
1439
+ "@esbuild/linux-ppc64": "0.25.12",
1440
+ "@esbuild/linux-riscv64": "0.25.12",
1441
+ "@esbuild/linux-s390x": "0.25.12",
1442
+ "@esbuild/linux-x64": "0.25.12",
1443
+ "@esbuild/netbsd-arm64": "0.25.12",
1444
+ "@esbuild/netbsd-x64": "0.25.12",
1445
+ "@esbuild/openbsd-arm64": "0.25.12",
1446
+ "@esbuild/openbsd-x64": "0.25.12",
1447
+ "@esbuild/openharmony-arm64": "0.25.12",
1448
+ "@esbuild/sunos-x64": "0.25.12",
1449
+ "@esbuild/win32-arm64": "0.25.12",
1450
+ "@esbuild/win32-ia32": "0.25.12",
1451
+ "@esbuild/win32-x64": "0.25.12"
1452
  }
1453
  },
1454
  "node_modules/escalade": {
 
1461
  "node": ">=6"
1462
  }
1463
  },
1464
+ "node_modules/fdir": {
1465
+ "version": "6.5.0",
1466
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1467
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1468
+ "dev": true,
1469
+ "license": "MIT",
1470
+ "engines": {
1471
+ "node": ">=12.0.0"
1472
+ },
1473
+ "peerDependencies": {
1474
+ "picomatch": "^3 || ^4"
1475
+ },
1476
+ "peerDependenciesMeta": {
1477
+ "picomatch": {
1478
+ "optional": true
1479
+ }
1480
+ }
1481
+ },
1482
  "node_modules/fsevents": {
1483
  "version": "2.3.3",
1484
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
1517
  "version": "4.0.0",
1518
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1519
  "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1520
+ "dev": true,
1521
  "license": "MIT"
1522
  },
1523
  "node_modules/jsesc": {
 
1546
  "node": ">=6"
1547
  }
1548
  },
 
 
 
 
 
 
 
 
 
 
 
 
1549
  "node_modules/lru-cache": {
1550
  "version": "5.1.1",
1551
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
 
1596
  "dev": true,
1597
  "license": "ISC"
1598
  },
1599
+ "node_modules/picomatch": {
1600
+ "version": "4.0.3",
1601
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1602
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1603
+ "dev": true,
1604
+ "license": "MIT",
1605
+ "engines": {
1606
+ "node": ">=12"
1607
+ },
1608
+ "funding": {
1609
+ "url": "https://github.com/sponsors/jonschlinkert"
1610
+ }
1611
+ },
1612
  "node_modules/postcss": {
1613
  "version": "8.5.6",
1614
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
 
1639
  }
1640
  },
1641
  "node_modules/react": {
1642
+ "version": "19.2.1",
1643
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
1644
+ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
1645
  "license": "MIT",
 
 
 
1646
  "engines": {
1647
  "node": ">=0.10.0"
1648
  }
1649
  },
1650
  "node_modules/react-dom": {
1651
+ "version": "19.2.1",
1652
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
1653
+ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
1654
  "license": "MIT",
1655
  "dependencies": {
1656
+ "scheduler": "^0.27.0"
 
1657
  },
1658
  "peerDependencies": {
1659
+ "react": "^19.2.1"
1660
  }
1661
  },
1662
  "node_modules/react-refresh": {
1663
+ "version": "0.18.0",
1664
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
1665
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
1666
  "dev": true,
1667
  "license": "MIT",
1668
  "engines": {
 
1712
  }
1713
  },
1714
  "node_modules/scheduler": {
1715
+ "version": "0.27.0",
1716
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1717
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1718
+ "license": "MIT"
 
 
 
1719
  },
1720
  "node_modules/semver": {
1721
  "version": "6.3.1",
 
1737
  "node": ">=0.10.0"
1738
  }
1739
  },
1740
+ "node_modules/tinyglobby": {
1741
+ "version": "0.2.15",
1742
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
1743
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
1744
+ "dev": true,
1745
+ "license": "MIT",
1746
+ "dependencies": {
1747
+ "fdir": "^6.5.0",
1748
+ "picomatch": "^4.0.3"
1749
+ },
1750
+ "engines": {
1751
+ "node": ">=12.0.0"
1752
+ },
1753
+ "funding": {
1754
+ "url": "https://github.com/sponsors/SuperchupuDev"
1755
+ }
1756
+ },
1757
  "node_modules/tslib": {
1758
  "version": "2.8.1",
1759
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
 
1798
  }
1799
  },
1800
  "node_modules/vite": {
1801
+ "version": "7.2.6",
1802
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
1803
+ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
1804
  "dev": true,
1805
  "license": "MIT",
1806
  "dependencies": {
1807
+ "esbuild": "^0.25.0",
1808
+ "fdir": "^6.5.0",
1809
+ "picomatch": "^4.0.3",
1810
+ "postcss": "^8.5.6",
1811
+ "rollup": "^4.43.0",
1812
+ "tinyglobby": "^0.2.15"
1813
  },
1814
  "bin": {
1815
  "vite": "bin/vite.js"
1816
  },
1817
  "engines": {
1818
+ "node": "^20.19.0 || >=22.12.0"
1819
  },
1820
  "funding": {
1821
  "url": "https://github.com/vitejs/vite?sponsor=1"
 
1824
  "fsevents": "~2.3.3"
1825
  },
1826
  "peerDependencies": {
1827
+ "@types/node": "^20.19.0 || >=22.12.0",
1828
+ "jiti": ">=1.21.0",
1829
+ "less": "^4.0.0",
1830
  "lightningcss": "^1.21.0",
1831
+ "sass": "^1.70.0",
1832
+ "sass-embedded": "^1.70.0",
1833
+ "stylus": ">=0.54.8",
1834
+ "sugarss": "^5.0.0",
1835
+ "terser": "^5.16.0",
1836
+ "tsx": "^4.8.1",
1837
+ "yaml": "^2.4.2"
1838
  },
1839
  "peerDependenciesMeta": {
1840
  "@types/node": {
1841
  "optional": true
1842
  },
1843
+ "jiti": {
1844
+ "optional": true
1845
+ },
1846
  "less": {
1847
  "optional": true
1848
  },
 
1863
  },
1864
  "terser": {
1865
  "optional": true
1866
+ },
1867
+ "tsx": {
1868
+ "optional": true
1869
+ },
1870
+ "yaml": {
1871
+ "optional": true
1872
  }
1873
  }
1874
  },
frontend/package.json CHANGED
@@ -8,14 +8,14 @@
8
  "preview": "vite preview"
9
  },
10
  "dependencies": {
11
- "@supabase/supabase-js": "^2.45.3",
12
- "react": "^18.3.1",
13
- "react-dom": "^18.3.1"
14
  },
15
  "devDependencies": {
16
- "@types/react": "^18.3.11",
17
- "@types/react-dom": "^18.3.0",
18
- "@vitejs/plugin-react": "^4.3.3",
19
- "vite": "^5.4.8"
20
  }
21
  }
 
8
  "preview": "vite preview"
9
  },
10
  "dependencies": {
11
+ "@supabase/supabase-js": "^2.86.2",
12
+ "react": "^19.2.1",
13
+ "react-dom": "^19.2.1"
14
  },
15
  "devDependencies": {
16
+ "@types/react": "^19.2.7",
17
+ "@types/react-dom": "^19.2.3",
18
+ "@vitejs/plugin-react": "^5.1.1",
19
+ "vite": "^7.2.6"
20
  }
21
  }
frontend/src/App.jsx CHANGED
@@ -27,7 +27,7 @@ export default function App() {
27
  const [session, setSession] = useState(null);
28
  const [status, setStatus] = useState("");
29
  const [forceRefresh, setForceRefresh] = useState(false);
30
- const [urlValue, setUrlValue] = useState("");
31
  const [jobResult, setJobResult] = useState(null);
32
  const [systemPrompt, setSystemPrompt] = useState("");
33
  const [siteName, setSiteName] = useState("Bot");
@@ -41,6 +41,7 @@ export default function App() {
41
  const [resetOtpEntered, setResetOtpEntered] = useState(false);
42
  const [resetOtpValue, setResetOtpValue] = useState("");
43
  const [isRunning, setIsRunning] = useState(false);
 
44
  const resetEmailRef = useRef(null);
45
  const resetOtpRef = useRef(null);
46
  const resetNewPassRef = useRef(null);
@@ -115,6 +116,8 @@ export default function App() {
115
  };
116
 
117
  const handleLogin = async () => {
 
 
118
  const email = loginEmailRef.current?.value?.trim() || "";
119
  const password = loginPassRef.current?.value || "";
120
  setStatus("Logging in...");
@@ -124,13 +127,15 @@ export default function App() {
124
  });
125
  if (error) {
126
  setStatus(`Login failed: ${error.message}`);
 
127
  } else {
 
128
  setSession(data.session);
129
  setEmailDisplay(data.session?.user?.email || email);
130
  const fn = data.session?.user?.user_metadata?.first_name;
131
  setFirstNameDisplay(fn || firstNameDisplay || (email ? email.split("@")[0] : ""));
132
  setStatus("Logged in.");
133
- setView("app");
134
  }
135
  };
136
 
@@ -229,7 +234,7 @@ export default function App() {
229
  };
230
 
231
  const runJob = async () => {
232
- const targetUrl = urlInputRef.current?.value?.trim() || defaultUrl;
233
  setIsRunning(true);
234
  setStatus("Submitting job...");
235
  setJobResult(null);
@@ -305,7 +310,9 @@ export default function App() {
305
  ref={loginPassRef}
306
  defaultValue=""
307
  />
308
- <button onClick={handleLogin}>Log In</button>
 
 
309
  <p className="link" onClick={() => setView("signup")}>
310
  Don’t have an account? Sign up
311
  </p>
@@ -396,8 +403,12 @@ export default function App() {
396
  <label className="label">Website URL</label>
397
  <input
398
  placeholder="https://example.com"
399
- defaultValue={defaultUrl}
400
  ref={urlInputRef}
 
 
 
 
401
  />
402
  <label className="checkbox">
403
  <input type="checkbox" checked={forceRefresh} onChange={(e) => setForceRefresh(e.target.checked)} />
@@ -440,10 +451,6 @@ export default function App() {
440
  />
441
  <button onClick={sendChat}>Send</button>
442
  <div className="status">{chatStatus}</div>
443
- <details style={{ marginTop: 8 }}>
444
- <summary className="muted small">View system prompt</summary>
445
- <pre className="result" style={{ maxHeight: 160 }}>{systemPrompt}</pre>
446
- </details>
447
  </>
448
  )}
449
  </>
 
27
  const [session, setSession] = useState(null);
28
  const [status, setStatus] = useState("");
29
  const [forceRefresh, setForceRefresh] = useState(false);
30
+ const [urlValue, setUrlValue] = useState("https://example.com");
31
  const [jobResult, setJobResult] = useState(null);
32
  const [systemPrompt, setSystemPrompt] = useState("");
33
  const [siteName, setSiteName] = useState("Bot");
 
41
  const [resetOtpEntered, setResetOtpEntered] = useState(false);
42
  const [resetOtpValue, setResetOtpValue] = useState("");
43
  const [isRunning, setIsRunning] = useState(false);
44
+ const [isAuthLoading, setIsAuthLoading] = useState(false);
45
  const resetEmailRef = useRef(null);
46
  const resetOtpRef = useRef(null);
47
  const resetNewPassRef = useRef(null);
 
116
  };
117
 
118
  const handleLogin = async () => {
119
+ if (isAuthLoading) return;
120
+ setIsAuthLoading(true);
121
  const email = loginEmailRef.current?.value?.trim() || "";
122
  const password = loginPassRef.current?.value || "";
123
  setStatus("Logging in...");
 
127
  });
128
  if (error) {
129
  setStatus(`Login failed: ${error.message}`);
130
+ setIsAuthLoading(false);
131
  } else {
132
+ setView("app"); // jump to app immediately on success
133
  setSession(data.session);
134
  setEmailDisplay(data.session?.user?.email || email);
135
  const fn = data.session?.user?.user_metadata?.first_name;
136
  setFirstNameDisplay(fn || firstNameDisplay || (email ? email.split("@")[0] : ""));
137
  setStatus("Logged in.");
138
+ setIsAuthLoading(false);
139
  }
140
  };
141
 
 
234
  };
235
 
236
  const runJob = async () => {
237
+ const targetUrl = (urlInputRef.current?.value || "").trim() || defaultUrl;
238
  setIsRunning(true);
239
  setStatus("Submitting job...");
240
  setJobResult(null);
 
310
  ref={loginPassRef}
311
  defaultValue=""
312
  />
313
+ <button onClick={handleLogin} disabled={isAuthLoading} className={isAuthLoading ? "loading" : ""}>
314
+ {isAuthLoading ? "Logging in..." : "Log In"}
315
+ </button>
316
  <p className="link" onClick={() => setView("signup")}>
317
  Don’t have an account? Sign up
318
  </p>
 
403
  <label className="label">Website URL</label>
404
  <input
405
  placeholder="https://example.com"
406
+ defaultValue={urlValue}
407
  ref={urlInputRef}
408
+ autoCorrect="off"
409
+ autoCapitalize="none"
410
+ spellCheck={false}
411
+ onChange={(e) => setUrlValue(e.target.value)}
412
  />
413
  <label className="checkbox">
414
  <input type="checkbox" checked={forceRefresh} onChange={(e) => setForceRefresh(e.target.checked)} />
 
451
  />
452
  <button onClick={sendChat}>Send</button>
453
  <div className="status">{chatStatus}</div>
 
 
 
 
454
  </>
455
  )}
456
  </>
requirements.txt CHANGED
@@ -1,64 +1,27 @@
1
- # ChatSMITH - Website to Chatbot Generator
2
- # Requirements file - Updated December 2025
3
 
4
- # ============================================================
5
- # CORE DEPENDENCIES
6
- # ============================================================
 
 
7
 
8
- # OpenAI SDK and Agents
9
- openai>=1.0.0
10
- openai-agents # OpenAI Agents SDK for multi-agent orchestration
11
- # Alternative: Install from GitHub if PyPI version has issues:
12
- # git+https://github.com/openai/openai-agents-python.git
13
 
14
- # Data Models
15
- pydantic>=2.0.0
16
 
17
- # Environment Variables
18
- python-dotenv>=1.0.0
 
 
 
19
 
20
- # ============================================================
21
- # WEB SCRAPING (Smart Website Scraper - Phase 1)
22
- # ============================================================
23
 
24
- # Async HTTP client for fast parallel scraping
25
- aiohttp>=3.9.0
26
-
27
- # SSL certificates (REQUIRED for Windows!)
28
- certifi>=2024.0.0
29
-
30
- # HTML parsing and content extraction
31
- beautifulsoup4>=4.12.0
32
- lxml>=5.0.0
33
-
34
- # ============================================================
35
- # USER INTERFACE
36
- # ============================================================
37
-
38
- # Gradio for web UI (6.x recommended)
39
- gradio>=4.0.0
40
- # Supabase authentication
41
- supabase>=2.4.0
42
- # FastAPI backend
43
- fastapi>=0.115.0
44
- uvicorn[standard]>=0.29.0
45
-
46
- # ============================================================
47
- # OPTIONAL DEPENDENCIES
48
- # ============================================================
49
-
50
- # Export to Word (uncomment if needed)
51
- # python-docx
52
-
53
- # Email functionality (uncomment if needed)
54
- # sendgrid
55
-
56
- # JS rendering for heavy websites (uncomment if needed)
57
- # Install browsers with: playwright install
58
- # playwright>=1.48
59
-
60
- # ============================================================
61
- # DEVELOPMENT & TESTING
62
- # ============================================================
63
-
64
- pytest>=8.0.0
 
1
+ # ChatSMITH - backend requirements (FastAPI + scraper + Supabase auth)
2
+ # Updated: 2025-12-06 (pinned for reproducibility with current stack)
3
 
4
+ fastapi==0.123.10
5
+ uvicorn[standard]==0.38.0
6
+ python-dotenv==1.2.1
7
+ # gradio 6.0.2 caps pydantic at 2.12.4
8
+ pydantic==2.12.4
9
 
10
+ # OpenAI + agent stack
11
+ openai==2.9.0
12
+ openai-agents==0.6.2
 
 
13
 
14
+ # Supabase auth
15
+ supabase==2.25.0
16
 
17
+ # Scraping & parsing
18
+ aiohttp==3.13.2
19
+ certifi==2025.11.12
20
+ beautifulsoup4==4.14.3
21
+ lxml==6.0.2
22
 
23
+ # UI helpers (gradio callbacks used in pipeline)
24
+ gradio==6.0.2
 
25
 
26
+ # Dev / testing
27
+ pytest==9.0.1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_metrics.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+ from types import SimpleNamespace
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
+
11
+ from backend.app.services import metrics_logger as ml # noqa: E402
12
+ from backend.app.services import scrape_pipeline as sp # noqa: E402
13
+
14
+
15
+ class DummyProgress:
16
+ def __call__(self, *args, **kwargs):
17
+ return None
18
+
19
+
20
+ @pytest.mark.asyncio
21
+ async def test_cache_hit_metrics(monkeypatch):
22
+ monkeypatch.setattr(sp, "ENABLE_METRICS", True)
23
+ monkeypatch.setattr(sp.gr, "update", lambda **kwargs: {"update": kwargs})
24
+ monkeypatch.setattr(sp, "is_cached", lambda url: True)
25
+ monkeypatch.setattr(
26
+ sp,
27
+ "get_cached_knowledge",
28
+ lambda url: {"metadata": {"name": "CachedSite", "url": url, "pages_scraped": 2}},
29
+ )
30
+ monkeypatch.setattr(sp, "knowledge_to_chatbot_context", lambda knowledge: "ctx")
31
+ monkeypatch.setattr(sp, "build_status_new", lambda *args, **kwargs: "status")
32
+
33
+ result = await sp.run_full_research_new("https://example.com", progress=DummyProgress())
34
+ _, _, _, _, _, _, stats = result
35
+
36
+ assert stats["cache_hit"] is True
37
+ assert "tcr_seconds" in stats and stats["tcr_seconds"] >= 0
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_tcr_metrics_non_cache(monkeypatch, tmp_path):
42
+ monkeypatch.setattr(sp, "ENABLE_METRICS", True)
43
+ monkeypatch.setattr(sp.gr, "update", lambda **kwargs: {"update": kwargs})
44
+ monkeypatch.setattr(sp, "is_cached", lambda url: False)
45
+ monkeypatch.setattr(
46
+ sp,
47
+ "scrape_website",
48
+ lambda url: {
49
+ "success": True,
50
+ "total_pages": 1,
51
+ "pages": [{"title": "Home", "description": "", "sections": [], "content": "", "url": url, "page_type": "homepage"}],
52
+ "errors": [],
53
+ },
54
+ )
55
+ monkeypatch.setattr(sp, "format_scraped_content_for_context", lambda scraped_data: "content")
56
+ monkeypatch.setattr(
57
+ sp,
58
+ "analyze_content_gaps",
59
+ lambda scraped_content, url: SimpleNamespace(has_gaps=False, gaps_found=[], confidence_score=10, recommended_searches=[]),
60
+ )
61
+ monkeypatch.setattr(sp, "knowledge_to_chatbot_context", lambda knowledge: "ctx")
62
+ monkeypatch.setattr(sp, "extract_name_from_text", lambda text, url: "Site")
63
+ monkeypatch.setattr(sp, "create_knowledge_json", lambda url, scraped_data, web_search_results, raw_name: {})
64
+ monkeypatch.setattr(sp, "save_knowledge_json", lambda knowledge, url: tmp_path / "stub.json")
65
+ monkeypatch.setattr(sp, "build_status_new", lambda *args, **kwargs: "status")
66
+
67
+ result = await sp.run_full_research_new("https://example.com", progress=DummyProgress())
68
+ _, _, _, _, _, _, stats = result
69
+
70
+ assert stats["cache_hit"] is False
71
+ assert "tcr_seconds" in stats and stats["tcr_seconds"] >= 0
72
+
73
+
74
+ def test_log_chat_answer(tmp_path):
75
+ log_file = tmp_path / "chat.jsonl"
76
+ ml.log_chat_answer(
77
+ question="Q?",
78
+ answer="A!",
79
+ provenance="primary_only",
80
+ user="user@example.com",
81
+ log_path=log_file,
82
+ )
83
+
84
+ data = log_file.read_text(encoding="utf-8").strip().splitlines()
85
+ assert len(data) == 1
86
+ record = json.loads(data[0])
87
+ assert record["question"] == "Q?"
88
+ assert record["answer"] == "A!"
89
+ assert record["provenance"] == "primary_only"
90
+ assert record["user"] == "user@example.com"
91
+
92
+
93
+ def test_save_job_metrics_no_supabase(monkeypatch):
94
+ monkeypatch.setattr(ml, "get_supabase_client", lambda: None)
95
+ ml.save_job_metrics_to_supabase("https://example.com", {"cache_hit": True})
96
+ # Should not raise
97
+
98
+
99
+ def test_save_chat_answer_no_supabase(monkeypatch):
100
+ monkeypatch.setattr(ml, "get_supabase_client", lambda: None)
101
+ ml.save_chat_answer_to_supabase("q", "a", system_prompt="ctx")
102
+ # Should not raise
103
+
104
+
105
+ def test_save_job_metrics_payload(monkeypatch):
106
+ captured = {}
107
+
108
+ class Table:
109
+ def __init__(self, name):
110
+ self.name = name
111
+
112
+ def insert(self, payload):
113
+ captured["table"] = self.name
114
+ captured["payload"] = payload
115
+ return self
116
+
117
+ def execute(self):
118
+ captured["executed"] = True
119
+ return True
120
+
121
+ class Client:
122
+ def table(self, name):
123
+ return Table(name)
124
+
125
+ monkeypatch.setattr(ml, "get_supabase_client", lambda: Client())
126
+ ml.save_job_metrics_to_supabase(
127
+ "https://example.com",
128
+ {"cache_hit": True, "tcr_seconds": 1.5, "searches_run": 2, "pages_scraped": 3, "gaps_found": 1},
129
+ user_id="user-1",
130
+ )
131
+ assert captured["table"] == "metrics_job_runs"
132
+ assert captured["payload"]["url"] == "https://example.com"
133
+ assert captured["payload"]["cache_hit"] is True
134
+ assert captured["payload"]["tcr_seconds"] == 1.5
135
+ assert captured["payload"]["searches_run"] == 2
136
+ assert captured["payload"]["pages_scraped"] == 3
137
+ assert captured["payload"]["gaps_found"] == 1
138
+ assert captured["payload"]["user_id"] == "user-1"
139
+ assert captured["executed"] is True
140
+
141
+
142
+ def test_save_chat_answer_payload(monkeypatch):
143
+ captured = {}
144
+
145
+ class Table:
146
+ def __init__(self, name):
147
+ self.name = name
148
+
149
+ def insert(self, payload):
150
+ captured["table"] = self.name
151
+ captured["payload"] = payload
152
+ return self
153
+
154
+ def execute(self):
155
+ captured["executed"] = True
156
+ return True
157
+
158
+ class Client:
159
+ def table(self, name):
160
+ return Table(name)
161
+
162
+ monkeypatch.setattr(ml, "get_supabase_client", lambda: Client())
163
+ ml.save_chat_answer_to_supabase(
164
+ question="How?",
165
+ answer="Here",
166
+ system_prompt="Contains SECONDARY SOURCE",
167
+ user_id="user-2",
168
+ url="https://example.com",
169
+ )
170
+ assert captured["table"] == "metrics_chat_answers"
171
+ assert captured["payload"]["question"] == "How?"
172
+ assert captured["payload"]["answer"] == "Here"
173
+ assert captured["payload"]["provenance"] == "primary_plus_secondary"
174
+ assert captured["payload"]["url"] == "https://example.com"
175
+ assert captured["payload"]["user_id"] == "user-2"
176
+ assert captured["executed"] is True