xXArjunXx/MetaHackathon
0
1/**2 * Email Triage API Client with Mock Fallback3 */4 5import { mockAPI } from './mock/mockAPI';6import { MOCK_EMAILS, MOCK_TASKS } from './fixtures';7 8const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";9 10// Check if backend is reachable11let isBackendAvailable = true;12 13const checkBackendAvailability = async () => {14 try {15 const response = await fetch(`${API_BASE_URL}/health`, {16 method: "GET",17 mode: "no-cors",18 });19 isBackendAvailable = true;20 return true;21 } catch {22 isBackendAvailable = false;23 return false;24 }25};26 27export interface Email {28 id: string;29 sender: string;30 subject: string;31 preview: string;32 priority: "high" | "medium" | "low";33 has_attachment: boolean;34 is_read: boolean;35 action_taken?: string;36}37 38export interface Observation {39 inbox_count: number;40 unread_count: number;41 current_email: Email | null;42 email_list: Email[];43 action_taken?: string;44 message: string;45}46 47export interface Reward {48 score: number;49 reason: string;50 cumulative_score: number;51}52 53export interface State {54 task: string;55 task_description: string;56 step_count: number;57 cumulative_score: number;58 actions_taken: Record<string, string>;59 progress: string;60 correct_action_count?: number;61 normalized_score?: number;62 max_steps?: number;63 done?: boolean;64}65 66export interface StepResponse {67 success: boolean;68 session_id?: string;69 observation: Observation;70 reward: Reward;71 done: boolean;72 state: State;73}74 75export interface ResetResponse {76 success: boolean;77 session_id?: string;78 observation: Observation;79 state: State;80}81 82export interface GradeResponse {83 status: "complete" | "incomplete";84 message: string;85 score: number;86 total_actions?: number;87 correct_actions?: number;88}89 90export interface Task {91 id: string;92 name: string;93 description: string;94 email_count: number;95}96 97export interface TasksResponse {98 tasks: Task[];99}100 101type StateLike = Partial<State> & {102 cumulative_reward?: number;103 normalized_score?: number;104 max_steps?: number;105 done?: boolean;106};107 108const normalizeState = (rawState: StateLike | null | undefined): State => {109 const actionsTaken = rawState?.actions_taken ?? {};110 const stepCount = rawState?.step_count ?? 0;111 112 // Backend exposes cumulative_reward/normalized_score while frontend expects cumulative_score.113 const cumulativeScore =114 typeof rawState?.cumulative_score === "number"115 ? rawState.cumulative_score116 : typeof rawState?.cumulative_reward === "number"117 ? rawState.cumulative_reward118 : 0;119 120 // Preserve normalized_score from backend121 const normalizedScore = typeof rawState?.normalized_score === "number"122 ? rawState.normalized_score123 : undefined;124 125 return {126 task: rawState?.task ?? "easy",127 task_description: rawState?.task_description ?? "",128 step_count: stepCount,129 cumulative_score: cumulativeScore,130 actions_taken: actionsTaken,131 progress: rawState?.progress ?? `${Object.keys(actionsTaken).length}/${Math.max(stepCount, Object.keys(actionsTaken).length)}`,132 normalized_score: normalizedScore,133 correct_action_count: rawState?.correct_action_count,134 max_steps: rawState?.max_steps,135 done: rawState?.done,136 };137};138 139class EmailTriageAPI {140 private baseUrl: string;141 private useMockAPI: boolean = false;142 private sessionId: string | null = null;143 private readonly sessionStorageKey = "email-triage-session-id";144 145 constructor(baseUrl = API_BASE_URL) {146 this.baseUrl = baseUrl;147 this.useMockAPI = false;148 149 if (typeof window !== "undefined") {150 this.sessionId = window.localStorage.getItem(this.sessionStorageKey);151 }152 }153 154 isUsingMockAPI(): boolean {155 return this.useMockAPI;156 }157 158 getSessionId(): string | null {159 return this.sessionId;160 }161 162 private buildSessionId(): string {163 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {164 return crypto.randomUUID();165 }166 return `session-${Date.now()}`;167 }168 169 private ensureSessionId(): string {170 if (!this.sessionId) {171 this.sessionId = this.buildSessionId();172 if (typeof window !== "undefined") {173 window.localStorage.setItem(this.sessionStorageKey, this.sessionId);174 }175 }176 return this.sessionId;177 }178 179 private updateSessionIdFromResponse(data: { session_id?: string } | null | undefined): void {180 if (!data?.session_id) {181 return;182 }183 this.sessionId = data.session_id;184 if (typeof window !== "undefined") {185 window.localStorage.setItem(this.sessionStorageKey, this.sessionId);186 }187 }188 189 private withSessionQuery(path: string): string {190 const sid = this.ensureSessionId();191 const separator = path.includes("?") ? "&" : "?";192 return `${path}${separator}session_id=${encodeURIComponent(sid)}`;193 }194 195 async clearSession(): Promise<void> {196 this.sessionId = null;197 if (typeof window !== "undefined") {198 window.localStorage.removeItem(this.sessionStorageKey);199 }200 }201 202 async restoreSession(): Promise<ResetResponse | null> {203 if (this.useMockAPI) {204 return null;205 }206 207 const sid = this.sessionId;208 if (!sid) {209 return null;210 }211 212 try {213 const [stateResponse, obsResponse] = await Promise.all([214 fetch(`${this.baseUrl}/state?session_id=${encodeURIComponent(sid)}`),215 fetch(`${this.baseUrl}/observation?session_id=${encodeURIComponent(sid)}`),216 ]);217 218 if (!stateResponse.ok || !obsResponse.ok) {219 return null;220 }221 222 const [stateData, obsData] = await Promise.all([223 stateResponse.json(),224 obsResponse.json(),225 ]);226 227 return {228 success: true,229 session_id: sid,230 observation: obsData,231 state: normalizeState(stateData),232 };233 } catch {234 return null;235 }236 }237 238 async reset(task: string = "easy"): Promise<ResetResponse> {239 try {240 const sid = this.ensureSessionId();241 const response = await fetch(`${this.baseUrl}/reset`, {242 method: "POST",243 headers: { "Content-Type": "application/json" },244 body: JSON.stringify({ task, session_id: sid }),245 });246 247 if (!response.ok) {248 throw new Error(`Reset failed: ${response.statusText}`);249 }250 251 const data = await response.json();252 this.updateSessionIdFromResponse(data);253 return {254 ...data,255 state: normalizeState(data?.state),256 };257 } catch (error) {258 console.log("[v0] Backend unavailable, using mock API");259 this.useMockAPI = true;260 return mockAPI.reset(task);261 }262 }263 264 async step(265 action: string,266 emailId: string,267 details: Record<string, unknown> = {}268 ): Promise<StepResponse> {269 if (this.useMockAPI) {270 return mockAPI.step(action, emailId);271 }272 273 try {274 const response = await fetch(`${this.baseUrl}/step`, {275 method: "POST",276 headers: { "Content-Type": "application/json" },277 body: JSON.stringify({278 action,279 email_id: emailId,280 details,281 session_id: this.ensureSessionId(),282 }),283 });284 285 if (!response.ok) {286 throw new Error(`Step failed: ${response.statusText}`);287 }288 289 const data = await response.json();290 this.updateSessionIdFromResponse(data);291 return {292 ...data,293 state: normalizeState(data?.state),294 };295 } catch (error) {296 console.log("[v0] Backend unavailable, using mock API");297 this.useMockAPI = true;298 return mockAPI.step(action, emailId);299 }300 }301 302 async getState(): Promise<State> {303 if (this.useMockAPI) {304 return {305 task: "easy",306 task_description: "Sort emails",307 step_count: 0,308 cumulative_score: 0,309 actions_taken: {},310 progress: "0%",311 };312 }313 314 try {315 const response = await fetch(`${this.baseUrl}${this.withSessionQuery("/state")}`);316 317 if (!response.ok) {318 throw new Error(`Get state failed: ${response.statusText}`);319 }320 321 const data = await response.json();322 return normalizeState(data);323 } catch (error) {324 this.useMockAPI = true;325 return {326 task: "easy",327 task_description: "Sort emails",328 step_count: 0,329 cumulative_score: 0,330 actions_taken: {},331 progress: "0%",332 };333 }334 }335 336 async getObservation(): Promise<Observation> {337 if (this.useMockAPI) {338 const emails = MOCK_EMAILS.easy;339 return {340 inbox_count: emails.length,341 unread_count: emails.length,342 current_email: emails[0],343 email_list: emails,344 message: "Demo mode",345 };346 }347 348 try {349 const response = await fetch(`${this.baseUrl}${this.withSessionQuery("/observation")}`);350 351 if (!response.ok) {352 throw new Error(`Get observation failed: ${response.statusText}`);353 }354 355 return response.json();356 } catch (error) {357 this.useMockAPI = true;358 const emails = MOCK_EMAILS.easy;359 return {360 inbox_count: emails.length,361 unread_count: emails.length,362 current_email: emails[0],363 email_list: emails,364 message: "Demo mode",365 };366 }367 }368 369 async grade(): Promise<GradeResponse> {370 if (this.useMockAPI) {371 return mockAPI.grade();372 }373 374 try {375 const response = await fetch(`${this.baseUrl}${this.withSessionQuery("/grade")}`);376 377 if (!response.ok) {378 throw new Error(`Grade failed: ${response.statusText}`);379 }380 381 return response.json();382 } catch (error) {383 this.useMockAPI = true;384 return mockAPI.grade();385 }386 }387 388 async getTasks(): Promise<TasksResponse> {389 try {390 const controller = new AbortController();391 const timeoutId = setTimeout(() => controller.abort(), 3000);392 393 const response = await fetch(`${this.baseUrl}/tasks`, {394 signal: controller.signal,395 });396 397 clearTimeout(timeoutId);398 399 if (!response.ok) {400 throw new Error(`Get tasks failed: ${response.statusText}`);401 }402 403 return response.json();404 } catch (error) {405 console.log("[v0] Backend unavailable, using mock tasks", error);406 this.useMockAPI = true;407 return mockAPI.getTasks();408 }409 }410 411 // Real-time observation polling for Hugging Face model updates412 subscribeToUpdates(interval: number = 2000, onUpdate?: (obs: Observation) => void): () => void {413 const pollInterval = setInterval(async () => {414 try {415 const obs = await this.getObservation();416 if (onUpdate) {417 onUpdate(obs);418 }419 } catch (err) {420 console.error("[API] Polling error:", err);421 }422 }, interval);423 424 // Return cleanup function425 return () => clearInterval(pollInterval);426 }427}428 429export const emailTriageAPI = new EmailTriageAPI();430 