InnerVoice / frontend /lib /api.ts
E5K7's picture
fix: Dynamic API URL routing for production proxy support
b3a788a
// API client for InnerVoice backend
// In production/HuggingFace, we want empty string to use relative /api paths which hits the Next.js rewrite proxy.
const API_URL = process.env.NEXT_PUBLIC_API_URL || "";
function getAuthHeader(): Record<string, string> {
if (typeof window === "undefined") return {};
const token = localStorage.getItem("token");
return token ? { Authorization: `Bearer ${token}` } : {};
}
export async function apiGet<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
headers: { ...getAuthHeader() },
});
if (!res.ok) throw new Error(`API Error ${res.status}: ${await res.text()}`);
return res.json();
}
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", ...getAuthHeader() },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`API Error ${res.status}: ${await res.text()}`);
return res.json();
}
export async function apiPut<T>(path: string, body?: unknown): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "PUT",
headers: { "Content-Type": "application/json", ...getAuthHeader() },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`API Error ${res.status}: ${await res.text()}`);
return res.json();
}
export async function apiDelete<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "DELETE",
headers: { ...getAuthHeader() },
});
if (!res.ok) throw new Error(`API Error ${res.status}: ${await res.text()}`);
return res.json();
}
// ── Typed API functions ───────────────────────────────────────────────────────
export interface VoiceEntry {
id: string;
created_at: string;
primary_emotion: string;
emotion_confidence: number;
energy_score: number;
calmness_score: number;
mood_score: number;
clarity_score: number;
transcription: string;
duration_seconds: number;
pitch_mean: number;
speech_rate: number;
pause_count: number;
filler_rate: number;
}
export interface MoodAlert {
id: string;
created_at: string;
alert_type: string;
severity: string;
message: string;
suggested_action: string | null;
is_read: boolean;
}
export interface TrendsData {
entries_count: number;
streak: number;
most_common_emotion: string | null;
this_week: Record<string, number | null>;
last_week: Record<string, number | null>;
insights: string[];
}
export interface AnalyzeResult {
entry_id: string;
emotion: string;
confidence: number;
mood_scores: { energy: number; calmness: number; mood: number; clarity: number };
transcription: string;
features: Record<string, unknown>;
insight: string;
new_alerts: MoodAlert[];
}
export interface ChatChannel {
id: string;
title: string;
created_at: string;
updated_at: string;
message_count: number;
}
export const api = {
getEntries: (days = 30) =>
apiGet<VoiceEntry[]>(`/api/entries?days=${days}`),
getTrends: () =>
apiGet<TrendsData>(`/api/trends`),
getAlerts: () =>
apiGet<MoodAlert[]>(`/api/alerts`),
markAlertRead: (alertId: string) =>
apiPut<{ success: boolean }>(`/api/alerts/${alertId}/read`),
chat: (message: string, channelId: string) =>
apiPost<{ response: string; message_id: string }>("/api/chat", { message, channel_id: channelId }),
getChatHistory: (channelId: string) =>
apiGet<Array<{ id: string; role: string; content: string; created_at: string }>>(
`/api/chat/history?channel_id=${channelId}`
),
getChannels: () =>
apiGet<ChatChannel[]>("/api/chat/channels"),
createChannel: (title = "New Chat") =>
apiPost<ChatChannel>("/api/chat/channels", { title }),
renameChannel: (channelId: string, title: string) =>
apiPut<{ success: boolean; title: string }>(`/api/chat/channels/${channelId}`, { title }),
deleteChannel: (channelId: string) =>
fetch(`${API_URL}/api/chat/channels/${channelId}`, {
method: "DELETE",
headers: { ...getAuthHeader() },
}).then(r => r.json()),
getWeeklyReport: () =>
apiGet<Record<string, unknown>>("/api/weekly-report"),
getDailyPrompt: () =>
apiGet<{ prompt: string; context: string }>("/api/daily-prompt"),
logSleep: (entryId: string, sleepHours: number) =>
apiPost<{ success: boolean; sleep_hours: number }>(`/api/entries/${entryId}/sleep`, { sleep_hours: sleepHours }),
getSleepCorrelation: () =>
apiGet<Array<{ date: string; sleep_hours: number; mood_score: number; energy_score: number; emotion: string }>>("/api/sleep-correlation"),
inviteTrustedMember: (email: string) =>
apiPost<{ success: boolean; message: string }>("/api/trusted-circle/invite", { email }),
getTrustedMembers: () =>
apiGet<{ id: string; email: string; joined_at: string }[]>("/api/trusted-circle"),
removeTrustedMember: (id: string) =>
apiDelete<{ success: boolean; message: string }>(`/api/trusted-circle/${id}`),
broadcastWeeklyReport: () =>
apiPost<{ success: boolean; sent_count: number; errors?: string[] }>("/api/weekly-report/broadcast", {}),
analyzeAudio: async (audioBlob: Blob): Promise<AnalyzeResult> => {
const formData = new FormData();
formData.append("audio", audioBlob, "recording.webm");
const headers: Record<string, string> = {};
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
if (token) headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_URL}/api/analyze`, {
method: "POST",
headers,
body: formData,
});
if (!res.ok) throw new Error(`Analyze error ${res.status}`);
return res.json();
},
setBaseline: async (audioBlob: Blob): Promise<{msg: string, has_baseline: boolean}> => {
const formData = new FormData();
formData.append("audio", audioBlob, "calibration.webm");
const headers: Record<string, string> = {};
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
if (token) headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_URL}/api/auth/baseline`, {
method: "POST",
headers,
body: formData,
});
if (!res.ok) throw new Error(`Baseline error ${res.status}`);
return res.json();
}
};