Akbub's picture
deploy: Nexus AI v0.2.0 - SAP C4C Lead Creation UI included in fresh frontend build
d1f3f31
Raw
History Blame Contribute Delete
11.6 kB
import axios from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Custom retry interceptor for robust AI backend handling
apiClient.interceptors.response.use(undefined, async (error) => {
const config = error.config as any;
if (!config || !config.retry) {
return Promise.reject(error);
}
config.retryCount = config.retryCount || 0;
if (config.retryCount >= config.retry) {
return Promise.reject(error);
}
config.retryCount += 1;
const backoff = new Promise(resolve => {
setTimeout(() => {
resolve(null);
}, config.retryDelay || 2000);
});
await backoff;
return apiClient(config);
});
export type BackendAnalysis = {
transcript: string;
diarizedTranscript?: Array<{
speaker: 'Customer' | 'Agent' | string;
rawSpeaker?: string;
text: string;
start?: number | null;
end?: number | null;
}>;
customerTranscript?: string;
customerBehavioralTranscript?: string;
agentTranscript?: string;
privacy?: {
entities: Array<{ type: string; value: string; source: string }>;
grouped: Record<string, string[]>;
redactionCount: number;
provider: string;
};
customerBehaviorSummary?: {
focus: string;
intentSignals: number;
hesitationScore: number;
urgencySignals: number;
objectionSignals: number;
wordCount: number;
privacySafe: boolean;
};
conversationSummary?: {
overview: string;
customerNeed: string;
keyPoints: string[];
outcome: string;
nextAction: string;
confidence: number;
provider?: string;
};
rawFeatures?: Array<{
name: string;
label: string;
}>;
pipelineFeatures?: {
sentiment_score?: number;
confidence_score?: number;
hesitation_score?: number;
delay_flag?: number;
feature_count?: number;
brand_count?: number;
interaction_length?: number;
extraction_provider?: string;
};
prediction?: {
prediction: number;
probability: number;
label: string;
reasons: string[];
};
followUpAlerts?: FollowUpAlert[];
products?: Array<{
name: string;
sentiment: string;
score: number;
confidence: number;
context?: string;
}>;
summary?: {
averageScore?: number;
dominant?: string;
totalProducts?: number;
};
conversionScore?: {
probability: number;
label: string;
confidence: number;
} | null;
audioQuality?: {
label: string;
confidence: number;
language?: string | null;
whisperModel?: string | null;
} | null;
metadata?: {
extractionQuality?: Record<string, number | string>;
extractionProvider?: string;
};
sapLead?: {
leadCreated: boolean;
leadId?: string | null;
objectId?: string | null;
sapStatus: string;
httpStatus?: number | null;
error?: string | null;
};
};
export type FollowUpAlert = {
id: string;
follow_up_required: boolean;
customer_name: string;
company_name: string;
action_needed: string;
priority: 'High' | 'Medium' | 'Low';
reason: string;
source_text: string;
created_date: string;
status: 'Pending' | 'Completed';
source_name?: string;
source_type?: string;
};
const toUiFeatures = (analysis: BackendAnalysis) => {
const products = analysis.products || [];
const reasons = analysis.prediction?.reasons || [];
const rawFeatures = analysis.rawFeatures || [];
// Extract explicit objections from LLaMA features first
const explicitObjections = rawFeatures
.filter((f) => f.label === 'OBJECTION' || f.label === 'OBJECTION_TYPE')
.map((f) => f.name || (f as any).value);
const productObjections = products
.filter((product) => product.sentiment === 'negative')
.map((product) => product.context || product.name);
const allObjections = [...explicitObjections, ...productObjections].slice(0, 5);
// Filter out positive sentiments from fallback reasons
const fallbackReasons = reasons.filter((r) =>
!r.toLowerCase().includes('positive') &&
!r.toLowerCase().includes('buying intent')
);
const pipelineSentiment = analysis.pipelineFeatures?.sentiment_score;
const modelLabel = analysis.prediction?.label || analysis.conversionScore?.label;
const rawDominant = analysis.summary?.dominant || 'neutral';
// If the backend returns our new rich labels (e.g., "Strong Buying Intent"), they will be spaced out strings.
const formattedEmotion = rawDominant.includes(' ')
? rawDominant.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
: rawDominant.charAt(0).toUpperCase() + rawDominant.slice(1);
const intentFeature = rawFeatures.find((f) => f.label === 'INTENT');
const buyingIntentStr = intentFeature
? (intentFeature.name || (intentFeature as any).value)
: (modelLabel ? modelLabel.charAt(0).toUpperCase() + modelLabel.slice(1) : (products.length > 0 ? 'Medium' : 'Unknown'));
const extractionProviderValue =
analysis.pipelineFeatures?.extraction_provider || analysis.metadata?.extractionProvider || 'llama';
return {
sentiment: Math.max(0, Math.min(1, ((pipelineSentiment ?? analysis.summary?.averageScore ?? 0) + 1) / 2)),
emotion: formattedEmotion,
buyingIntent: buyingIntentStr,
budgetDetected: rawFeatures.some((feature) => feature.label === 'BUDGET') || products.some((product) => product.name.toLowerCase().includes('budget') || /\d/.test(product.name)),
objections: allObjections.length ? allObjections : fallbackReasons,
rawFeatures,
extractionProvider: String(extractionProviderValue),
diarizedTranscript: analysis.diarizedTranscript || [],
privacy: analysis.privacy,
customerBehaviorSummary: analysis.customerBehaviorSummary,
conversationSummary: analysis.conversationSummary,
conversionScore: analysis.conversionScore,
audioQuality: analysis.audioQuality,
sapLead: analysis.sapLead,
// Store original prediction data for the next step
_rawPrediction: analysis.prediction,
_rawSummary: analysis.summary,
_rawFeaturesCount: analysis.rawFeatures?.length || 0,
};
};
const toUiPrediction = (analysis: BackendAnalysis) => {
const probability = analysis.prediction?.probability ?? analysis.conversionScore?.probability ?? 0;
const risk = probability >= 0.7 ? 'Low' : probability >= 0.4 ? 'Medium' : 'High';
const productCount = analysis.rawFeatures?.length || analysis.summary?.totalProducts || analysis.products?.length || 0;
const insights = analysis.prediction?.reasons?.length ? analysis.prediction.reasons : [
`${productCount} sales signal${productCount === 1 ? '' : 's'} detected`,
`Dominant sentiment is ${analysis.summary?.dominant || 'neutral'}`,
analysis.conversionScore
? `Lead classified as ${analysis.conversionScore.label}`
: 'Prediction model inferred from features',
];
const nextSteps = [];
if (risk === 'High') {
nextSteps.push('Offer flexible payment options (e.g., No-Cost EMI) to lower the entry barrier.');
nextSteps.push('Follow up within 24 hours specifically addressing their primary objection.');
} else if (risk === 'Medium') {
nextSteps.push('Highlight the long-term value and warranty of the product.');
nextSteps.push('Share case studies or testimonials related to their specific use-case.');
} else {
nextSteps.push('Send the checkout link immediately to capitalize on high intent.');
nextSteps.push('Attempt to upsell an extended warranty or premium accessories.');
}
if (insights.some(i => i.toLowerCase().includes('hesitant') || i.toLowerCase().includes('postponed'))) {
nextSteps.unshift('Identify their exact bottleneck (budget vs feature) to clear hesitation.');
}
return {
probability,
risk,
insights,
nextSteps,
};
};
export const apiService = {
uploadAudio: async (file: File) => {
const formData = new FormData();
formData.append('audio', file);
const response = await apiClient.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
// @ts-ignore
retry: 2,
retryDelay: 3000,
});
const jobId = response.data.job_id;
if (!jobId) {
// Fallback in case the backend wasn't fully restarted and returns the old response format directly
const analysis = response.data as BackendAnalysis;
return {
analysis,
transcription: analysis.transcript,
features: toUiFeatures(analysis),
prediction: toUiPrediction(analysis),
};
}
let jobData = response.data;
while (
jobData.status === 'pending' ||
jobData.status === 'processing' ||
jobData.status === 'awaiting_ml'
) {
await new Promise(resolve => setTimeout(resolve, 2000));
const jobRes = await apiClient.get(`/jobs/${jobId}`);
jobData = jobRes.data;
}
if (jobData.status === 'failed') {
throw new Error(jobData.error || 'Background audio processing failed');
}
const analysis = jobData.result as BackendAnalysis;
return {
analysis,
transcription: analysis.transcript,
features: toUiFeatures(analysis),
prediction: toUiPrediction(analysis),
};
},
extractFeatures: async (transcription: string, diarizedTranscript?: any[]) => {
const response = await apiClient.post('/analyze', {
text: transcription,
diarizedTranscript,
}, {
// @ts-ignore
retry: 1,
retryDelay: 2000,
});
const jobId = response.data.job_id;
let analysis: BackendAnalysis;
if (jobId) {
let jobData = response.data;
while (jobData.status === 'pending' || jobData.status === 'processing' || jobData.status === 'awaiting_ml') {
await new Promise(resolve => setTimeout(resolve, 1500));
const jobRes = await apiClient.get(`/jobs/${jobId}`);
jobData = jobRes.data;
}
if (jobData.status === 'failed') {
throw new Error(jobData.error || 'Background feature extraction failed');
}
analysis = jobData.result as BackendAnalysis;
} else {
analysis = response.data as BackendAnalysis;
}
return {
analysis,
transcription: analysis.transcript,
features: toUiFeatures(analysis),
prediction: toUiPrediction(analysis),
};
},
predictConversion: async (features: any) => {
// The backend actually computes prediction during the upload/analyze steps.
// We simulate a network delay here to maintain the premium UX animation flow.
await new Promise(resolve => setTimeout(resolve, 1500));
return toUiPrediction({
transcript: '',
prediction: features._rawPrediction,
summary: features._rawSummary,
conversionScore: features.conversionScore,
});
},
getFollowUpAlerts: async (filters?: { priority?: string; status?: string; customerName?: string }) => {
const response = await apiClient.get('/follow-up-alerts', {
params: {
priority: filters?.priority || undefined,
status: filters?.status || undefined,
customer_name: filters?.customerName || undefined,
},
});
return response.data.alerts as FollowUpAlert[];
},
updateFollowUpStatus: async (alertId: string, status: FollowUpAlert['status']) => {
const response = await apiClient.patch(`/follow-up-alerts/${alertId}`, { status });
return response.data.alert as FollowUpAlert;
}
};
export default apiClient;