ai-helpdesk-api / Frontend /src /user /pages /AIProcessing.jsx
ritesh19180's picture
Upload folder using huggingface_hub
846e2c3 verified
Raw
History Blame Contribute Delete
16.5 kB
import React, { useEffect, useRef, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Bot } from 'lucide-react';
import useToastStore from '../../store/toastStore';
import { Card } from "../../components/ui/card";
import AIProcessingSteps from "../components/AIProcessingSteps";
import useTicketStore from "../../store/ticketStore";
import useAdminStore from '../../admin/store/adminStore';
import useAuthStore from '../../store/authStore';
import { supabase } from '../../lib/supabaseClient';
import { API_CONFIG } from '../../config';
import { analyzeTicketWithAI } from '../../services/aiAssistant';
const steps = [
"Reading your message",
"Extracting technical entities",
"Detecting category and priority",
"Checking duplicate issues",
"Finding possible solutions"
];
const AIProcessing = () => {
const navigate = useNavigate();
const location = useLocation();
const { text, image_text, image_base64, template_id, template_used, user_modified, ticket_title, original_text, original_language } = location.state || {};
const setAITicket = useTicketStore((state) => state.setAITicket);
const { settings } = useAdminStore();
const { user, profile } = useAuthStore();
const { showToast } = useToastStore();
const hasCalledAPI = useRef(false);
const [activeStep, setActiveStep] = useState(0);
useEffect(() => {
if (!text) {
console.warn("[AIProcessing] No ticket text found. Redirecting to /create-ticket");
navigate('/create-ticket');
return;
}
if (hasCalledAPI.current) return;
hasCalledAPI.current = true;
const analyzeTicket = async () => {
console.log("[AIProcessing] Starting analysis for:", text);
try {
// === Single call to backend — handles ML classification + Gemini summary ===
// Classification, NER, priority, team assignment, duplicate detection → local ML model
// ── Upload Image if present ──
let uploadedImageUrl = null;
if (image_base64) {
try {
const base64Data = image_base64.split(',')[1] || image_base64;
const contentType =
image_base64.match(/data:(.*?);/)?.[1] || 'image/jpeg';
const fileExt = contentType.split('/')[1] || 'jpeg';
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: contentType
});
const fileName =
`${user?.id || 'anon'}/${Date.now()}-${Math.random()
.toString(36)
.substring(7)}.${fileExt}`;
const { error: uploadError } = await supabase.storage
.from('ticket-attachments')
.upload(fileName, blob, {
contentType,
upsert: true
});
if (!uploadError) {
const { data: publicUrlData } = supabase.storage
.from('ticket-attachments')
.getPublicUrl(fileName);
uploadedImageUrl = publicUrlData?.publicUrl;
}
} catch (err) {
console.error("[AIProcessing] Image upload failed:", err);
}
}
const payload = {
text: text,
image_text: image_text || "",
image_base64: image_base64 || "",
user_id: user?.id,
company:
profile?.company ||
user?.user_metadata?.company ||
"System",
company_id: profile?.company_id || null,
image_url: uploadedImageUrl,
confidence_threshold: settings.aiConfidenceThreshold,
duplicate_sensitivity: settings.duplicateSensitivity,
// Smart Template metadata (backend can use for improved routing)
template_id: template_id || null,
template_used: template_used || false,
user_modified: user_modified || false,
ticket_title: ticket_title || null,
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 6000);
const response = await fetch(
`${API_CONFIG.BACKEND_URL}/ai/analyze_stream`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error("Backend streaming failed");
}
// ==============================
// FIXED SSE BUFFERED PARSING
// ==============================
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let done = false;
let finalTicket = null;
// Buffer stores incomplete SSE chunks
let buffer = "";
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
// Append chunk to buffer
buffer += decoder.decode(value, { stream: true });
// SSE events separated by blank line
const events = buffer.split('\n\n');
// Keep incomplete trailing event
buffer = events.pop() || "";
for (const event of events) {
const lines = event.split('\n');
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const data = JSON.parse(
line.substring(6)
);
if (data.step === 'done') {
setActiveStep(steps.length);
finalTicket = data.result;
} else {
const stepIndex =
steps.indexOf(data.step);
if (stepIndex !== -1) {
setActiveStep(stepIndex);
}
}
} catch (e) {
console.error(
"Error parsing stream data",
e,
line
);
}
}
}
}
}
// Optional leftover buffer parsing
if (buffer.trim()) {
const lines = buffer.split('\n');
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const data = JSON.parse(
line.substring(6)
);
if (data.step === 'done') {
finalTicket = data.result;
}
} catch (e) {
console.error(
"Final buffer parse error",
e,
line
);
}
}
}
if (!finalTicket) {
throw new Error("BACKEND_STARTUP");
}
// Override the backend summary using the robust frontend multi-provider failover
try {
const aiResult = await analyzeTicketWithAI(text, image_text, image_base64);
finalTicket.summary = aiResult.summary || finalTicket.summary;
if (aiResult.image_description) {
finalTicket.image_description = aiResult.image_description;
}
// The local ML model is weak with regional languages (e.g., Telugu).
// If the LLM returned classification fields, we trust it more than a low-confidence ML prediction.
if (aiResult.category && (finalTicket.confidence < 0.6 || finalTicket.category === 'Unknown' || finalTicket.category === 'Access')) {
finalTicket.category = aiResult.category;
finalTicket.subcategory = aiResult.subcategory || finalTicket.subcategory;
finalTicket.priority = aiResult.priority || finalTicket.priority;
finalTicket.assigned_team = aiResult.assigned_team || finalTicket.assigned_team;
finalTicket.confidence = aiResult.confidence || 0.95;
}
} catch (aiErr) {
console.warn("[AIProcessing] Frontend summary generation failed:", aiErr);
}
const aiTicketObject = {
...finalTicket,
status: 'analyzing',
originalIssue: original_text || text,
originalLanguage: original_language || 'en',
capturedFileBase64: image_base64,
ocrText: image_text,
image_url: uploadedImageUrl || finalTicket?.image_url || null
};
setAITicket(aiTicketObject);
setTimeout(() => navigate('/ai-understanding'), 1000);
} catch (error) {
console.error("[AIProcessing] Analysis Failed:", error);
// Graceful fallback for any error (e.g. backend 503 offline, streaming failed, or network protocol errors)
if (
true // Always fallback gracefully to keep the ticket creation flow 100% operational!
) {
console.warn(
"[AIProcessing] Backend unreachable or preparing. Using local fallback."
);
let summary =
(text.charAt(0).toUpperCase() + text.slice(1))
.substring(0, 100)
+ (text.length > 100 ? '…' : '');
let image_description = "";
let fallbackCategory = "General";
let fallbackSub = "General Support";
let fallbackPriority = "Medium";
let fallbackTeam = "General Support";
try {
const aiResult = await analyzeTicketWithAI(text, image_text, image_base64);
summary = aiResult.summary || summary;
image_description = aiResult.image_description || "";
if (aiResult.category) {
fallbackCategory = aiResult.category;
fallbackSub = aiResult.subcategory || fallbackSub;
fallbackPriority = aiResult.priority || fallbackPriority;
fallbackTeam = aiResult.assigned_team || fallbackTeam;
}
} catch (aiErr) {
console.warn("[AIProcessing] Fallback AI summary failed:", aiErr);
}
const fallbackTicket = {
summary,
status: 'analyzing',
category: fallbackCategory,
subcategory: fallbackSub,
priority: fallbackPriority,
auto_resolve: false,
assigned_team: fallbackTeam,
entities: [],
duplicate_ticket: {
is_duplicate: false,
similarity: 0
},
confidence: 0.9,
needs_review: true,
reasoning:
"Analyzed via AI Fallback — backend ML model was unreachable.",
image_description,
ocr_text: image_text || "",
highlights: [],
originalIssue: original_text || text,
originalLanguage: original_language || 'en',
capturedFileBase64: image_base64,
ocrText: image_text,
image_url: uploadedImageUrl || null
};
setAITicket(fallbackTicket);
setTimeout(
() => navigate('/ai-understanding'),
500
);
} else {
showToast(
"AI Analysis sequence failed. Check network protocols.",
"error"
);
navigate('/create-ticket');
}
}
};
analyzeTicket();
}, [text, image_text, image_base64, navigate, setAITicket, settings, user, profile]);
return (
<div className="flex-1 flex items-center justify-center p-6 bg-[#f6f8f7] min-h-screen relative overflow-hidden">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-emerald-500/5 rounded-full blur-[100px] pointer-events-none"></div>
<Card className="w-full max-w-md bg-white border border-gray-100 shadow-xl shadow-gray-200/40 rounded-3xl overflow-hidden relative z-10">
<div className="p-10 flex flex-col items-center">
<div className="w-16 h-16 bg-emerald-50 rounded-2xl flex items-center justify-center mb-6 border border-emerald-100 shadow-sm relative">
<Bot className="w-8 h-8 text-emerald-600 relative z-10" />
<div
className="absolute inset-0 border-2 border-emerald-500/20 rounded-2xl animate-ping"
style={{ animationDuration: '2s' }}
></div>
</div>
<h1 className="text-2xl font-black text-gray-900 tracking-tight text-center mb-2">
Analyzing your issue
</h1>
<p className="text-sm font-medium text-gray-500 text-center px-4 mb-10">
Our AI is understanding your request and checking for solutions.
</p>
<AIProcessingSteps
steps={steps}
activeStep={activeStep}
/>
</div>
</Card>
</div>
);
};
export default AIProcessing;