Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 103 additions & 22 deletions src/app/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { firebaseAuth } from "./firebase";
const viteEnv = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;
const API_BASE_URL = (viteEnv?.VITE_API_BASE_URL || "http://127.0.0.1:8000").replace(/\/$/, "");

const ANALYSIS_TIMEOUTS_MS = {
text: 45_000,
url: 60_000,
image: 90_000,
video: 180_000,
} as const;

const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
const MAX_VIDEO_BYTES = 50 * 1024 * 1024;

type Prediction = "FAKE" | "REAL" | "UNCERTAIN";

type ApiError = {
Expand Down Expand Up @@ -83,16 +93,54 @@ async function readResponse<T>(response: Response): Promise<T> {
return response.json() as Promise<T>;
}

async function requestWithTimeout(
input: RequestInfo | URL,
init: RequestInit,
timeoutMs: number,
): Promise<Response> {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);

try {
return await fetch(input, { ...init, signal: controller.signal });
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
throw new Error(
`The analysis timed out after ${Math.round(timeoutMs / 1000)} seconds. Please try a smaller file or try again later.`,
);
}

if (error instanceof TypeError) {
throw new Error("Unable to reach the VeritasAI analysis service. Check the backend URL and your connection.");
}

throw error instanceof Error ? error : new Error("Analysis request failed.");
} finally {
window.clearTimeout(timeoutId);
}
}

function validateUpload(file: File, kind: "image" | "video") {
const expectedType = `${kind}/`;
if (!file || !file.type.startsWith(expectedType)) {
throw new Error(`Please select a valid ${kind} file.`);
}

const maxBytes = kind === "image" ? MAX_IMAGE_BYTES : MAX_VIDEO_BYTES;
if (file.size > maxBytes) {
const maxMb = maxBytes / (1024 * 1024);
throw new Error(`${kind === "image" ? "Image" : "Video"} is too large. Maximum supported size is ${maxMb} MB.`);
}

if (file.size === 0) {
throw new Error(`The selected ${kind} file is empty.`);
}
}

function isFirebaseIdToken(value: string): boolean {
return value.split(".").length === 3 && value.startsWith("eyJ");
}

/**
* Email/password authentication is handled entirely by Firebase Authentication.
* Render is only used for VeritasAI application APIs after authentication.
* The JWT-shaped password path exists only for the existing Google flow, whose
* caller already has a Firebase ID token and should not make a second auth request.
*/
export async function login(payload: {
email: string;
password: string;
Expand Down Expand Up @@ -169,45 +217,78 @@ export async function signup(payload: {
}

export async function analyzeText(text: string): Promise<AnalysisResult> {
const response = await fetch(`${API_BASE_URL}/api/analyze/text`, {
method: "POST", headers: withUserHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ text }),
});
if (!text.trim()) throw new Error("Please enter some text to analyze.");
const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/text`, {
method: "POST",
headers: withUserHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ text }),
}, ANALYSIS_TIMEOUTS_MS.text);
return readResponse<AnalysisResult>(response);
}

export async function analyzeUrl(url: string): Promise<AnalysisResult> {
const response = await fetch(`${API_BASE_URL}/api/analyze/url`, {
method: "POST", headers: withUserHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ url }),
});
if (!url.trim()) throw new Error("Please enter a URL to analyze.");
let parsed: URL;
try {
parsed = new URL(url.trim());
} catch {
throw new Error("Please enter a valid URL.");
}
if (!/^https?:$/.test(parsed.protocol)) throw new Error("Only HTTP(S) URLs are supported.");

const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/url`, {
method: "POST",
headers: withUserHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ url: parsed.toString() }),
}, ANALYSIS_TIMEOUTS_MS.url);
return readResponse<AnalysisResult>(response);
}

export async function analyzeImage(file: File): Promise<AnalysisResult> {
validateUpload(file, "image");
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`${API_BASE_URL}/api/analyze/image`, {
method: "POST", headers: withUserHeaders(), body: formData,
});
const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/image`, {
method: "POST",
headers: withUserHeaders(),
body: formData,
}, ANALYSIS_TIMEOUTS_MS.image);
return readResponse<AnalysisResult>(response);
}

export async function analyzeVideo(file: File): Promise<AnalysisResult> {
validateUpload(file, "video");
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`${API_BASE_URL}/api/analyze/video`, {
method: "POST", headers: withUserHeaders(), body: formData,
});
const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/video`, {
method: "POST",
headers: withUserHeaders(),
body: formData,
}, ANALYSIS_TIMEOUTS_MS.video);
return readResponse<AnalysisResult>(response);
}

export async function analyzeVideoUrl(url: string): Promise<AnalysisResult> {
const response = await fetch(`${API_BASE_URL}/api/analyze/video-url`, {
method: "POST", headers: withUserHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ url }),
});
if (!url.trim()) throw new Error("Please enter a video URL.");
let parsed: URL;
try {
parsed = new URL(url.trim());
} catch {
throw new Error("Please enter a valid video URL.");
}
if (!/^https?:$/.test(parsed.protocol)) throw new Error("Only HTTP(S) video URLs are supported.");

const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/video-url`, {
method: "POST",
headers: withUserHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ url: parsed.toString() }),
}, ANALYSIS_TIMEOUTS_MS.video);
return readResponse<AnalysisResult>(response);
}

export async function fetchHistory(): Promise<HistoryItem[]> {
const response = await fetch(`${API_BASE_URL}/api/history`, { headers: withUserHeaders() });
const response = await requestWithTimeout(`${API_BASE_URL}/api/history`, {
headers: withUserHeaders(),
}, 30_000);
return readResponse<HistoryItem[]>(response);
}
Loading