eumora-api / frontend /lib /api.ts
Aditey Kshirsagar
Refactor: update frontend to version 0.2.0, remove Tailwind CSS and PostCSS configurations, and add new Lamp component with dynamic physics simulation
451c559
Raw
History Blame Contribute Delete
5.67 kB
/* ── EUMORA API client ────────────────────────────────────────────────────── */
import { EMOTIONS, BACKEND_EMOTION_MAP, type EmotionPreset } from "./data";
const API_URL =
process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
// ── Types ────────────────────────────────────────────────────────────────────
export interface BackendTrack {
name: string;
artist: string;
album: string;
album_art?: string;
spotify_url?: string;
preview_url?: string | null;
popularity: number;
match_score: number;
}
export interface BackendResult {
emotion: string;
confidence: number;
probabilities: Record<string, number>;
explanation?: string;
music_context?: Record<string, unknown>;
targets_used?: Record<string, unknown> | null;
tracks: BackendTrack[];
spotify_unavailable?: boolean;
}
/** The shape consumed by the New Frontend UI */
export interface AnalysisResult {
emotion: string; // key in EMOTIONS (melancholy, euphoric, etc.)
confidence: number; // 0–1
summary: string; // poetic descriptor
tracks: DisplayTrack[]; // either from Spotify or curated fallback
fromBackend: boolean; // true if real backend was used
}
export interface DisplayTrack {
title: string;
artist: string;
album: string;
year: number | string;
duration: string;
album_art?: string;
spotify_url?: string;
match_score?: number;
}
// ── API call ─────────────────────────────────────────────────────────────────
/**
* Call the backend /api/recommend endpoint and translate the response into
* the format expected by the New Frontend UI.
*
* Falls back to curated presets if:
* - Backend is unreachable
* - Spotify is unavailable on the backend
*/
export async function analyze(text: string): Promise<AnalysisResult> {
try {
const res = await fetch(`${API_URL}/api/recommend`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, limit: 10 }),
});
if (!res.ok) {
throw new Error(`Backend returned ${res.status}`);
}
const data: BackendResult = await res.json();
// Map backend emotion β†’ frontend emotion key
const frontendEmotion = BACKEND_EMOTION_MAP[data.emotion] ?? "melancholy";
const emo: EmotionPreset = EMOTIONS[frontendEmotion] ?? EMOTIONS.melancholy;
// If Spotify returned real tracks, use them
if (data.tracks && data.tracks.length > 0 && !data.spotify_unavailable) {
const displayTracks: DisplayTrack[] = data.tracks.map((t) => ({
title: t.name,
artist: t.artist,
album: t.album,
year: "",
duration: "",
album_art: t.album_art,
spotify_url: t.spotify_url,
match_score: t.match_score,
}));
return {
emotion: frontendEmotion,
confidence: data.confidence,
summary: data.explanation || emo.descriptor,
tracks: displayTracks,
fromBackend: true,
};
}
// Spotify unavailable β€” use curated fallback tracks
return {
emotion: frontendEmotion,
confidence: data.confidence,
summary: data.explanation || emo.descriptor,
tracks: emo.tracks.map((t) => ({ ...t, year: t.year })),
fromBackend: true,
};
} catch {
// Backend completely unreachable β€” do client-side fallback
return analyzeClientFallback(text);
}
}
// ── Client-side fallback ─────────────────────────────────────────────────────
function analyzeClientFallback(text: string): AnalysisResult {
const t = text.toLowerCase();
const score: Record<string, number> = {};
const kw: Record<string, string[]> = {
melancholy: ["sad", "heavy", "blue", "rain", "alone", "empty", "tired", "ache"],
euphoric: ["smiling", "alive", "happy", "joy", "ecstatic", "high", "best", "summer", "sun"],
anxious: ["panic", "spiral", "anxious", "racing", "can't", "3am", "awake", "stress"],
lonely: ["lonely", "miss", "without", "her name", "his name", "empty room", "alone"],
nostalgic: ["remember", "back when", "used to", "old", "childhood", "summer of", "missed"],
yearning: ["want", "need you", "long for", "wish", "ache", "almost", "across"],
peaceful: ["calm", "quiet", "still", "soft", "warm", "easy", "settled"],
defiant: ["hate", "no more", "done", "fight", "scream", "burn", "angry"],
};
Object.entries(kw).forEach(([emo, words]) => {
score[emo] = words.reduce((s, w) => s + (t.includes(w) ? 1 : 0), 0);
});
const sorted = Object.entries(score).sort((a, b) => b[1] - a[1]);
const top = sorted[0][1] > 0 ? sorted[0][0] : "melancholy";
const confidence = sorted[0][1] > 0
? Math.min(0.95, 0.6 + sorted[0][1] * 0.08)
: 0.52;
const emo = EMOTIONS[top] ?? EMOTIONS.melancholy;
return {
emotion: top,
confidence,
summary: emo.descriptor,
tracks: emo.tracks.map((tr) => ({ ...tr, year: tr.year })),
fromBackend: false,
};
}