Bankbot / frontend /src /lib /api.ts
mohsin-devs's picture
Fix document PDF text extraction
1f11d61
Raw
History Blame Contribute Delete
23.7 kB
/**
* BankBot API client β€” typed fetch wrapper for all backend endpoints.
* Handles auth headers, token refresh, and error normalization.
*
* HF Spaces mode: when NEXT_PUBLIC_API_URL is empty, all /api/* calls
* go to the same origin β€” Nginx routes them to FastAPI internally.
* WebSocket also uses the same origin with ws:// or wss://.
*/
// In HF mode NEXT_PUBLIC_API_URL is "" β€” use empty string so fetch uses same origin
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "";
// ─── WebSocket URL derivation ─────────────────────────────────────────────────
// HF mode (API_BASE=""): derive from window.location at runtime
// External mode: convert http→ws, https→wss
function getWsBase(): string {
if (typeof window === "undefined") return "";
if (!API_BASE) {
// Same-origin WebSocket β€” use current page protocol
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${window.location.host}`;
}
return API_BASE.replace(/^https/, "wss").replace(/^http/, "ws");
}
// ─── Token management (browser-only) ─────────────────────────────────────────
export const tokenStore = {
getAccess: (): string | null =>
typeof window !== "undefined" ? localStorage.getItem("bb_access_token") : null,
getRefresh: (): string | null =>
typeof window !== "undefined" ? localStorage.getItem("bb_refresh_token") : null,
setTokens: (access: string, refresh?: string) => {
if (typeof window === "undefined") return;
localStorage.setItem("bb_access_token", access);
if (refresh) localStorage.setItem("bb_refresh_token", refresh);
},
clear: () => {
if (typeof window === "undefined") return;
localStorage.removeItem("bb_access_token");
localStorage.removeItem("bb_refresh_token");
localStorage.removeItem("bb_user");
},
};
// ─── Core fetch wrapper ───────────────────────────────────────────────────────
async function apiFetch<T>(
path: string,
options: RequestInit = {},
retry = true
): Promise<T> {
const token = tokenStore.getAccess();
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
};
if (token) headers["Authorization"] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
// Auto-refresh on 401
if (res.status === 401 && retry) {
const refreshToken = tokenStore.getRefresh();
if (refreshToken) {
try {
const refreshRes = await fetch(`${API_BASE}/api/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (refreshRes.ok) {
const data = await refreshRes.json();
tokenStore.setTokens(data.access_token);
return apiFetch<T>(path, options, false);
}
} catch {
// refresh failed β€” clear tokens
}
}
tokenStore.clear();
throw new ApiError(401, "Session expired. Please log in again.");
}
if (!res.ok) {
let detail = `HTTP ${res.status}`;
try {
const err = await res.json();
detail = err.detail || detail;
} catch { /* ignore */ }
throw new ApiError(res.status, detail);
}
return res.json() as Promise<T>;
}
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = "ApiError";
}
}
// ─── Auth ─────────────────────────────────────────────────────────────────────
export interface LoginResponse {
access_token: string;
refresh_token: string;
token_type: string;
user_id: string;
name: string;
email: string;
}
export const authApi = {
login: (email: string, password: string) => {
const form = new URLSearchParams({ username: email, password });
return apiFetch<LoginResponse>("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form.toString(),
});
},
register: (email: string, password: string, name: string) =>
apiFetch<LoginResponse>("/api/auth/register", {
method: "POST",
body: JSON.stringify({ email, password, name }),
}),
me: () =>
apiFetch<{
user_id: string;
email: string;
name: string;
financial_personality: string;
notifications_enabled?: boolean;
ai_coaching_enabled?: boolean;
fraud_alerts_enabled?: boolean;
}>("/api/auth/me"),
updateSettings: (data: {
name?: string;
financial_personality?: string;
notifications_enabled?: boolean;
ai_coaching_enabled?: boolean;
fraud_alerts_enabled?: boolean;
}) =>
apiFetch<{
user_id: string;
email: string;
name: string;
financial_personality: string;
notifications_enabled?: boolean;
ai_coaching_enabled?: boolean;
fraud_alerts_enabled?: boolean;
}>("/api/auth/settings", { method: "PATCH", body: JSON.stringify(data) }),
logout: () => {
tokenStore.clear();
return Promise.resolve();
},
};
export interface FinancialGoal {
id: string;
title: string;
target_amount: number;
current_amount: number;
progress_percent: number;
target_date: string | null;
days_left: number | null;
monthly_contribution?: number;
months_remaining?: number;
on_track: boolean;
}
export const goalsApi = {
list: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{
goals: FinancialGoal[];
summary: { count: number; total_target: number; total_saved: number; overall_progress: number };
}>(`/api/goals${qs}`);
},
contribute: (goalId: string, amount: number, userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ id: string; current_amount: number; progress_percent: number; completed: boolean }>(
`/api/goals/${goalId}/contribute${qs}`,
{ method: "POST", body: JSON.stringify({ amount }) }
);
},
};
export const loansApi = {
eligibility: (data: {
salary: number;
credit_score: number;
existing_loans: number;
employment_years: number;
age: number;
loan_amount: number;
}, userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{
approval_probability: number;
approval_status: string;
risk_level: string;
loan_score: number;
emi: number;
monthly_emi: number;
recommendations: string[];
issues: string[];
comparison: Array<{ rate: string; tenure: string; emi: number; total_amount: number; interest: number }>;
}>(`/api/loans/eligibility${qs}`, { method: "POST", body: JSON.stringify(data) });
},
};
// ─── Dashboard ────────────────────────────────────────────────────────────────
export interface DashboardOverview {
total_balance: number;
accounts: Array<{ id: string; type: string; balance: number; currency: string }>;
monthly_income: number;
monthly_expenses: number;
savings_rate: number;
spending_by_category: Array<{ name: string; value: number }>;
recent_transactions: Array<{
id: string; merchant: string; category: string;
amount: number; type: string; timestamp: string;
}>;
cash_flow: Array<{ month: string; income: number; expenses: number; savings: number }>;
health_score: number;
fraud_alert_count: number;
ai_briefing: { summary?: string; briefing?: string; insights?: string[] };
}
export const dashboardApi = {
overview: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<DashboardOverview>(`/api/dashboard/overview${qs}`);
},
};
// ─── AI ───────────────────────────────────────────────────────────────────────
export interface HealthScore {
overall_score: number;
categories: Record<string, { score: number; max: number; label: string }>;
}
export interface ForecastResult {
monthly_projections?: Array<{ month: number; balance: number; savings: number }>;
scenarios?: { conservative: number; expected: number; optimistic: number };
}
export const aiApi = {
healthScore: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<HealthScore>(`/api/ai/coaching/score${qs}`);
},
briefing: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ summary: string; insights: string[] }>(`/api/ai/coaching/briefing${qs}`);
},
behaviorInsights: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ insights: string[]; metrics: Record<string, number> }>(`/api/ai/behavior/insights${qs}`);
},
twinPredict: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<ForecastResult>(`/api/ai/twin/predict${qs}`);
},
twinFuture: (months = 12, userId?: string) => {
const qs = new URLSearchParams({ months: String(months), ...(userId ? { user_id: userId } : {}) });
return apiFetch<ForecastResult>(`/api/ai/twin/future?${qs}`);
},
twinScenarios: (months = 6, userId?: string) => {
const qs = new URLSearchParams({ months: String(months), ...(userId ? { user_id: userId } : {}) });
return apiFetch<{ scenarios: Record<string, number[]> }>(`/api/ai/twin/scenarios?${qs}`);
},
fraudAnalysis: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ alerts: Array<{ id: string; risk_score: number; details: string }> }>(`/api/ai/fraud/analysis${qs}`);
},
subscriptions: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ subscriptions: Array<{ merchant: string; amount: number; recommendation: string }> }>(`/api/ai/subscriptions/optimize${qs}`);
},
chat: (message: string, userId?: string, sessionId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ response: string; session_id: string }>(`/api/ai/chat${qs}`, {
method: "POST",
body: JSON.stringify({ message, session_id: sessionId }),
});
},
chatSessions: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{
sessions: Array<{
id: string;
title: string;
created_at: string | null;
updated_at: string | null;
message_count: number;
preview: string;
}>;
count: number;
}>(`/api/ai/chat/sessions${qs}`);
},
createChatSession: (userId?: string, title = "New chat") => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{
id: string;
title: string;
created_at: string | null;
updated_at: string | null;
message_count: number;
preview: string;
}>(`/api/ai/chat/sessions${qs}`, {
method: "POST",
body: JSON.stringify({ title }),
});
},
deleteChatSession: (sessionId: string, userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ ok: boolean }>(`/api/ai/chat/sessions/${sessionId}${qs}`, { method: "DELETE" });
},
chatHistory: (sessionId: string, userId?: string, limit = 100) => {
const params = new URLSearchParams({ session_id: sessionId, limit: String(limit) });
if (userId) params.set("user_id", userId);
return apiFetch<{
session_id: string;
messages: Array<{ id: string; role: "user" | "assistant"; content: string; created_at: string | null }>;
count: number;
}>(`/api/ai/chat/history?${params}`);
},
clearChatHistory: (sessionId: string, userId?: string) => {
const params = new URLSearchParams({ session_id: sessionId });
if (userId) params.set("user_id", userId);
return apiFetch<{ ok: boolean; message: string; session_id: string }>(
`/api/ai/chat/history?${params}`,
{ method: "DELETE" }
);
},
simulatePurchase: (amount: number, merchant: string, category: string, userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<Record<string, unknown>>(`/api/ai/simulate/purchase${qs}`, {
method: "POST",
body: JSON.stringify({ amount, merchant, category }),
});
},
// ── Coach Mode ──────────────────────────────────────────────────────────────
weeklyCoaching: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{
generated_at: string;
week_start: string;
week_end: string;
health_score: number;
week_spend: number;
week_income: number;
spend_delta_pct: number;
top_categories: Array<{ name: string; amount: number }>;
anomalies: string[];
coaching_report: string;
improvements: string[];
}>(`/api/ai/coach/weekly${qs}`);
},
// ── Fraud Explanation ───────────────────────────────────────────────────────
fraudExplain: (fraudLogId: string) =>
apiFetch<{
fraud_log_id: string;
transaction_id: string;
merchant: string;
amount: number;
timestamp: string;
risk_score: number;
raw_reasons: string[];
ai_explanation: string;
user_avg_amount: number;
amount_vs_avg: number;
}>(`/api/ai/fraud/explain/${fraudLogId}`),
// ── Monthly Narrative ───────────────────────────────────────────────────────
monthlyNarrative: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{
month: string;
generated_at: string;
summary: {
total_spend: number;
total_income: number;
spend_delta_pct: number;
savings_balance: number;
investment_value: number;
investment_gain_pct: number;
monthly_subscriptions: number;
};
category_changes: Array<{
category: string;
this_month: number;
last_month: number;
delta_pct: number;
}>;
narrative: string;
}>(`/api/ai/narrative/monthly${qs}`);
},
};
// ─── Notifications ────────────────────────────────────────────────────────────
export interface NotificationItem {
id: string;
title: string;
message: string;
type: "alert" | "insight" | "warning" | string;
read: boolean;
created_at: string;
}
export const notificationsApi = {
list: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ notifications: NotificationItem[]; unread_count: number }>(`/api/notifications/${qs}`);
},
markRead: (id: string, userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ success: boolean }>(`/api/notifications/${id}/read${qs}`, { method: "PATCH" });
},
markAllRead: (userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ success: boolean }>(`/api/notifications/read-all${qs}`, { method: "PATCH" });
},
dismiss: (id: string, userId?: string) => {
const qs = userId ? `?user_id=${userId}` : "";
return apiFetch<{ success: boolean }>(`/api/notifications/${id}${qs}`, { method: "DELETE" });
},
};
// ─── Transactions ─────────────────────────────────────────────────────────────
export const transactionsApi = {
list: (params: { page?: number; limit?: number; category?: string; type?: string; userId?: string } = {}) => {
const qs = new URLSearchParams();
if (params.page) qs.set("page", String(params.page));
if (params.limit) qs.set("limit", String(params.limit));
if (params.category) qs.set("category", params.category);
if (params.type) qs.set("type", params.type);
if (params.userId) qs.set("user_id", params.userId);
return apiFetch<{
transactions: Array<{ id: string; merchant: string; category: string; amount: number; type: string; timestamp: string }>;
total: number; page: number; pages: number;
}>(`/api/transactions/?${qs}`);
},
};
// ─── Payments ─────────────────────────────────────────────────────────────────
export interface Payment {
id: string;
user_id: string;
amount: number;
currency: string;
recipient_name: string;
recipient_account: string;
payment_type: string;
status: "pending" | "completed" | "failed" | "flagged";
risk_score: number;
fraud_flag: boolean;
created_at: string;
transaction_reference: string | null;
note: string | null;
ai_insight: string | null;
}
export interface PaymentHistoryResponse {
payments: Payment[];
total: number;
page: number;
pages: number;
stats: {
total_sent: number;
total_received: number;
flagged_count: number;
};
}
export const paymentsApi = {
create: (data: {
amount: number;
currency?: string;
recipient_name: string;
recipient_account: string;
payment_type?: string;
note?: string;
}) =>
apiFetch<Payment>("/api/payments/create", {
method: "POST",
body: JSON.stringify(data),
}),
transfer: (data: { amount: number; to_account_type: string; note?: string }) =>
apiFetch<Payment>("/api/payments/transfer", {
method: "POST",
body: JSON.stringify(data),
}),
history: (params: {
page?: number;
limit?: number;
payment_type?: string;
status?: string;
} = {}) => {
const qs = new URLSearchParams();
if (params.page) qs.set("page", String(params.page));
if (params.limit) qs.set("limit", String(params.limit));
if (params.payment_type) qs.set("payment_type", params.payment_type);
if (params.status) qs.set("status", params.status);
return apiFetch<PaymentHistoryResponse>(`/api/payments/history?${qs}`);
},
get: (id: string) => apiFetch<Payment>(`/api/payments/${id}`),
verify: (payment_id: string, confirm: boolean) =>
apiFetch<{ message: string; payment: Payment }>("/api/payments/verify", {
method: "POST",
body: JSON.stringify({ payment_id, confirm }),
}),
cancel: (id: string) =>
apiFetch<{ message: string }>(`/api/payments/${id}`, { method: "DELETE" }),
};
// ─── WebSocket factory ────────────────────────────────────────────────────────
export function createChatWebSocket(userId?: string): WebSocket {
const wsBase = getWsBase();
const qs = userId ? `?user_id=${userId}` : "";
return new WebSocket(`${wsBase}/api/ai/chat/ws${qs}`);
}
// ─── Status ───────────────────────────────────────────────────────────────────
export const statusApi = {
check: () => apiFetch<{ ai_backend: string; ai_available: boolean; db_type: string; cache_type: string }>("/api/status"),
};
// ─── Memory ───────────────────────────────────────────────────────────────────
export interface ChatHistoryMessage {
id: string;
session_id: string | null;
role: "user" | "assistant";
content: string;
created_at: string;
}
export interface ChatSession {
id: string;
title: string;
created_at: string;
updated_at: string;
}
export const memoryApi = {
history: (sessionId?: string) => {
const qs = sessionId ? `?session_id=${sessionId}` : "";
return apiFetch<{ messages: ChatHistoryMessage[]; sessions: ChatSession[]; total: number }>(`/api/memory/history${qs}`);
},
save: (data: { session_id?: string; role: string; content: string; session_title?: string }) =>
apiFetch<ChatHistoryMessage>("/api/memory/save", { method: "POST", body: JSON.stringify(data) }),
clear: (sessionId?: string) => {
const qs = sessionId ? `?session_id=${sessionId}` : "";
return apiFetch<{ deleted: number }>(`/api/memory/clear${qs}`, { method: "DELETE" });
},
getPreferences: () =>
apiFetch<{ theme: string; language: string }>("/api/memory/preferences"),
updatePreferences: (data: { theme?: string; language?: string }) =>
apiFetch<{ theme: string; language: string }>("/api/memory/preferences", {
method: "PATCH",
body: JSON.stringify(data),
}),
};
// ─── Documents ────────────────────────────────────────────────────────────────
export interface DocumentRecord {
id: string;
filename: string;
file_type: string;
file_size: number;
summary: string | null;
insights: string[];
extracted_length: number;
created_at: string;
}
export interface DocumentDetail extends DocumentRecord {
messages: Array<{ id: string; role: string; content: string; language: string; created_at: string }>;
}
export const documentsApi = {
upload: (file: File, language = "en") => {
const form = new FormData();
form.append("file", file);
const token = tokenStore.getAccess();
return fetch(`${API_BASE}/api/documents/upload?language=${language}`, {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: form,
}).then(async (res) => {
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new ApiError(res.status, err.detail || `HTTP ${res.status}`);
}
return res.json() as Promise<DocumentRecord & { suspicious: string[]; extracted_length: number }>;
});
},
history: () => apiFetch<{ documents: DocumentRecord[] }>("/api/documents/history"),
get: (id: string) => apiFetch<DocumentDetail>(`/api/documents/${id}`),
chat: (id: string, question: string, language = "en") =>
apiFetch<{ question: string; answer: string; document_id: string; language: string }>(
`/api/documents/chat/${id}`,
{ method: "POST", body: JSON.stringify({ question, language }) }
),
analyze: (id: string, language = "en") =>
apiFetch<{ id: string; summary: string; insights: string[]; suspicious: string[] }>(
`/api/documents/analyze/${id}?language=${language}`,
{ method: "POST" }
),
delete: (id: string) =>
apiFetch<{ message: string }>(`/api/documents/${id}`, { method: "DELETE" }),
};