/** * RASAD FastAPI client. * * The backend (Python FastAPI) lives separately from Supabase. Auth is still * handled by Supabase; only verification calls flow through this adapter. * * Resolution order for the API base URL: * 1. VITE_RASAD_API_URL (env var) — explicit override wins * 2. localhost / 127.0.0.1 — dev → http://localhost:8000/api/v1 * 3. anything else (vercel, netlify, …) — prod → Render-deployed backend * * This means a `git clone` works for both: * • Judges running everything locally → step 2 picks the local backend. * • Frontend deployed to a cloud host → step 3 picks the live Render backend. */ function resolveApiBase(): string { const fromEnv = (import.meta as any).env?.VITE_RASAD_API_URL as string | undefined; if (fromEnv && fromEnv.trim().length > 0) return fromEnv; if (typeof window !== "undefined") { const host = window.location.hostname; if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) { return "http://localhost:8000/api/v1"; } } // Production fallback — Render-deployed backend (Frankfurt, free tier) return "https://rasad-backend-0fa5.onrender.com/api/v1"; } export const RASAD_API_BASE: string = resolveApiBase(); export type AgentMode = "real" | "demo" | "fallback" | "mixed"; export type ConfidenceLevel = "high" | "medium" | "low" | "insufficient_data" | "timeout"; export type VerdictLabel = | "VERIFIED" | "FALSE" | "MISLEADING" | "AI_GENERATED" | "MANIPULATED" | "OLD_NEWS" | "UNVERIFIED" | "DUPLICATE" | "HIGH_RISK"; export interface AgentResult { agent: string; score: number; confidence: ConfidenceLevel; evidence: string[]; raw: Record; elapsed_ms: number; mode: AgentMode; } export interface FinalVerdict { id: string; verdict: VerdictLabel; confidence: number; arabic_explanation: string; english_explanation?: string; agent_breakdown: Record; sources: string[]; processing_time_ms: number; content_type: string; content_hash?: string; weighted_score?: number; mode: AgentMode; cached?: boolean; created_at: string; } export interface WebSourceMatch { title: string; summary: string; url: string; source_name: string; domain: string; pub_date?: string | null; kind: "rss" | "web" | string; engine?: string; similarity: number; } export interface WebSearchHit { title: string; url: string; snippet: string; source_domain: string; language: string; pub_date?: string | null; engine: string; score: number; scraped?: { title: string; summary: string; word_count: number; language: string; method: string; pub_date?: string | null; }; } export interface SearchResponse { query: string; language: string; comprehensive: boolean; scraped: boolean; engines: Record; count: number; results: WebSearchHit[]; } export interface LiveItem { id: string; title: string; summary: string; url: string; source_name: string; source_domain: string; pub_date: string | null; seen_at: string; emotion_score: number; fake_score: number; risk_label: "verified" | "suspicious" | "risky" | "neutral" | string; flags: string[]; topic: string; image_url?: string | null; } export interface LiveFeedResponse { items: LiveItem[]; watermark: string | null; monitor: "live" | "disabled" | string; } export interface LiveStatsResponse { total: number; max: number; by_risk: Record; by_source: Record; cycles_completed: number; last_run_at: string | null; last_added_count: number; interval_seconds: number; errors_total: number; monitor: "live" | "disabled" | string; } export interface MonitorItem { id: string; title: string; summary: string; url: string; source_name: string; verdict?: VerdictLabel; confidence: number; published_at: string; flagged: boolean; } export interface RasadHealth { status: string; agents: number; version: string; services: Record; environment: string; } export class RasadApiError extends Error { status: number; detail?: string; constructor(message: string, status: number, detail?: string) { super(message); this.name = "RasadApiError"; this.status = status; this.detail = detail; } } async function request(path: string, init: RequestInit = {}): Promise { const url = `${RASAD_API_BASE}${path}`; const headers = new Headers(init.headers); if (!headers.has("Content-Type") && init.body && !(init.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } let resp: Response; try { resp = await fetch(url, { ...init, headers }); } catch (e) { // Network-level failure: backend unreachable, CORS, DNS, … const isLocalhost = /(?:localhost|127\.0\.0\.1)/.test(RASAD_API_BASE); const hint = isLocalhost ? "خادم التحقق غير شغّال على هذا الجهاز. شغّل الـ backend (uvicorn) أو حدّث VITE_RASAD_API_URL ليُشير لخادم بعيد." : `تعذّر الاتصال بـ ${RASAD_API_BASE}. تأكّد أن الـ backend منشور وأن CORS يسمح لهذا الدومين.`; throw new RasadApiError(hint, 0, e instanceof Error ? e.message : String(e)); } if (!resp.ok) { let detail: string | undefined; try { const body = await resp.json(); detail = body?.detail || body?.message || JSON.stringify(body).slice(0, 200); } catch { detail = await resp.text().catch(() => undefined); } // 404 on the verify endpoints almost always means the backend isn't deployed // (e.g. frontend on a static host pointing at localhost or a wrong URL). if (resp.status === 404 && /\/verify\//.test(path)) { throw new RasadApiError( `الـ API endpoint غير موجود (${path}). تحقّق من VITE_RASAD_API_URL — قد تكون قيمته مُهيّأة لـ localhost على Hostinger، أو الـ backend غير مُشغّل.`, 404, detail, ); } throw new RasadApiError( `RASAD API ${resp.status}: ${resp.statusText}`, resp.status, detail, ); } return (await resp.json()) as T; } export async function verifyText( text: string, options: { language?: "ar" | "en" | "auto"; signal?: AbortSignal } = {}, ): Promise { return request("/verify/text", { method: "POST", body: JSON.stringify({ text, language: options.language ?? "auto", include_breakdown: true, }), signal: options.signal, }); } export async function verifyUrl( url: string, options: { signal?: AbortSignal } = {}, ): Promise { return request("/verify/url", { method: "POST", body: JSON.stringify({ url, language: "auto", include_breakdown: true }), signal: options.signal, }); } export async function verifyMedia( file: File, options: { contextText?: string; mediaType?: "image" | "video" | "audio"; signal?: AbortSignal } = {}, ): Promise { const fd = new FormData(); fd.append("file", file); fd.append("media_type", options.mediaType ?? "image"); if (options.contextText) fd.append("context_text", options.contextText); return request("/verify/media", { method: "POST", body: fd, signal: options.signal, }); } export async function getLiveFeed( options: { limit?: number; since?: string | null; risk?: string; domain?: string; topic?: string; signal?: AbortSignal; } = {}, ): Promise { const params = new URLSearchParams({ limit: String(options.limit ?? 30) }); if (options.since) params.set("since", options.since); if (options.risk) params.set("risk", options.risk); if (options.domain) params.set("domain", options.domain); if (options.topic) params.set("topic", options.topic); return request(`/live/feed?${params.toString()}`, { signal: options.signal }); } export async function getLiveStats(): Promise { return request("/live/stats"); } export async function refreshLive(): Promise<{ ok: boolean; kicked_at: string }> { return request<{ ok: boolean; kicked_at: string }>("/live/refresh", { method: "POST" }); } export async function searchWeb( query: string, options: { limit?: number; language?: "ar" | "en"; comprehensive?: boolean; scrape?: boolean } = {}, ): Promise { const params = new URLSearchParams({ q: query, limit: String(options.limit ?? 10), language: options.language ?? "ar", comprehensive: String(options.comprehensive ?? false), scrape: String(options.scrape ?? false), }); return request(`/search?${params.toString()}`); } export async function getMonitorFeed(limit = 20): Promise<{ items: MonitorItem[]; sources: string[] }> { return request<{ items: MonitorItem[]; sources: string[] }>( `/monitor/feed?limit=${limit}`, ); } export async function getTrending(limit = 10): Promise<{ items: any[] }> { return request<{ items: any[] }>(`/monitor/trending?limit=${limit}`); } export async function getHealth(): Promise { return request("/health"); } export async function pingRasad(): Promise { try { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 1500); const resp = await fetch(`${RASAD_API_BASE}/health`, { signal: ctrl.signal }); clearTimeout(t); return resp.ok; } catch { return false; } }