import type { AgentReview, SubReview, AgentName, PDFPage } from '../types'; import { IPublicClientApplication } from '@azure/msal-browser'; import { getAccessToken } from './authService'; const HTTP_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000'; const POLL_INTERVAL_MS = 2000; /** * Options for proof analysis with optional database persistence. */ export interface AnalyzeProofOptions { campaignId?: string; proofName?: string; channel?: string; subChannel?: string; proofType?: string; /** Brand to use for brand guidelines analysis: 'Barclays' or 'Barclaycard' */ brand?: string; } /** * Result of proof analysis, including optional database IDs if persisted. */ export interface AnalyzeProofResult { review: AgentReview; proofId?: string; versionId?: string; pdfPages?: PDFPage[]; isIdenticalFile?: boolean; } /** Read a File as base64 string (without the data-url prefix). */ async function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve((reader.result as string).split(',')[1]); reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsDataURL(file); }); } /** Sleep for `ms` milliseconds. */ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); /** Authenticated fetch helper — adds Bearer token. */ async function authFetch( url: string, init: RequestInit, accessToken: string, ): Promise { return fetch(url, { ...init, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, ...(init.headers ?? {}), }, }); } /** * Analyze a proof using the backend REST API. * Provides real-time updates as each agent completes via polling. */ export const analyzeProof = async ( file: File, onAgentUpdate: (name: AgentName | 'Summary', review?: SubReview) => void, msalInstance: IPublicClientApplication, options?: AnalyzeProofOptions, onNotification?: (message: string) => void, ): Promise => { const accessToken = await getAccessToken(msalInstance); if (!accessToken) { throw new Error('Failed to acquire access token. Please sign in again.'); } const fileData = await fileToBase64(file); // Submit the analysis job const submitRes = await authFetch( `${HTTP_URL}/api/analyze`, { method: 'POST', body: JSON.stringify({ file_data: fileData, file_type: file.type, is_wip: false, campaign_id: options?.campaignId, proof_name: options?.proofName, channel: options?.channel, sub_channel: options?.subChannel, proof_type: options?.proofType, brand: options?.brand ?? 'Barclaycard', }), }, accessToken, ); if (!submitRes.ok) { const err = await submitRes.text(); throw new Error(`Failed to submit analysis: ${submitRes.status} ${err}`); } const { job_id } = await submitRes.json(); // Poll until complete const seenAgents = new Set(); let notifiedFallback = false; while (true) { await sleep(POLL_INTERVAL_MS); const pollRes = await authFetch( `${HTTP_URL}/api/analyze/${job_id}`, { method: 'GET' }, accessToken, ); if (!pollRes.ok) { throw new Error(`Polling failed: ${pollRes.status}`); } const job = await pollRes.json(); // Notify about model fallback once if (job.model_fallback && !notifiedFallback) { notifiedFallback = true; onNotification?.('The primary AI model is currently unavailable. Analysis is continuing with the backup model and may take longer than usual.'); } // Fire callbacks for newly completed agents for (const agentName of Object.keys(job.agents_completed ?? {})) { if (!seenAgents.has(agentName)) { seenAgents.add(agentName); onAgentUpdate(agentName as AgentName, job.agents_completed[agentName] as SubReview); } } if (job.status === 'complete') { return { review: job.result as AgentReview, proofId: job.proof_id ?? undefined, versionId: job.version_id ?? undefined, pdfPages: job.pdf_pages as PDFPage[] | undefined, isIdenticalFile: job.is_identical_file ?? undefined, }; } if (job.status === 'error') { throw new Error(job.error_message || 'Analysis failed'); } } }; /** * Analyze a work-in-progress proof. * Returns a conversational summary for the creative team. */ export const analyzeWIPProof = async ( file: File, onAgentUpdate: (name: AgentName | 'Summary', review?: SubReview) => void, msalInstance: IPublicClientApplication ): Promise => { const accessToken = await getAccessToken(msalInstance); if (!accessToken) { throw new Error('Failed to acquire access token. Please sign in again.'); } const fileData = await fileToBase64(file); // Submit the analysis job const submitRes = await authFetch( `${HTTP_URL}/api/analyze`, { method: 'POST', body: JSON.stringify({ file_data: fileData, file_type: file.type, is_wip: true, }), }, accessToken, ); if (!submitRes.ok) { const err = await submitRes.text(); throw new Error(`Failed to submit WIP analysis: ${submitRes.status} ${err}`); } const { job_id } = await submitRes.json(); // Poll until complete const seenAgents = new Set(); while (true) { await sleep(POLL_INTERVAL_MS); const pollRes = await authFetch( `${HTTP_URL}/api/analyze/${job_id}`, { method: 'GET' }, accessToken, ); if (!pollRes.ok) { throw new Error(`Polling failed: ${pollRes.status}`); } const job = await pollRes.json(); // Fire callbacks for newly completed agents for (const agentName of Object.keys(job.agents_completed ?? {})) { if (!seenAgents.has(agentName)) { seenAgents.add(agentName); onAgentUpdate(agentName as AgentName, job.agents_completed[agentName] as SubReview); } } if (job.status === 'complete') { return job.result?.leadAgentSummary || 'Analysis complete.'; } if (job.status === 'error') { throw new Error(job.error_message || 'Analysis failed'); } } }; /** * Get a chat response from the WIP Lead Agent. * Uses HTTP REST endpoint. */ export const getWIPChatResponse = async (prompt: string): Promise => { try { const response = await fetch(`${HTTP_URL}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.response || data.message || 'No response from Lead Agent.'; } catch (error) { console.error('Error getting WIP chat response:', error); return "I'm sorry, the chat feature requires the backend chat endpoint to be implemented. For now, please use the proof analysis feature."; } };