feat: Enhance AI analysis with multi-question-type support and improved error handling

This commit is contained in:
Qaisyousuf 2025-09-09 16:34:26 +02:00
parent 7249d07ad0
commit 4c2901023a
9 changed files with 11070 additions and 2987 deletions

View file

@ -1,19 +1,20 @@
// Services/Implementation/AiAnalysisService.cs // Services/Implementation/AiAnalysisService.cs
using Azure; using Azure;
using Azure.AI.TextAnalytics;
using Azure.AI.OpenAI; using Azure.AI.OpenAI;
using OpenAI.Chat; using Azure.AI.TextAnalytics;
using Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Model;
using OpenAI.Chat;
using Services.AIViewModel; using Services.AIViewModel;
using Services.Interaces; using Services.Interaces;
using System.ClientModel;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Data;
using Microsoft.EntityFrameworkCore;
using System.ClientModel;
namespace Services.Implemnetation namespace Services.Implemnetation
{ {
@ -605,13 +606,32 @@ Respond with a JSON array of applicable categories: [""category1"", ""category2"
{ {
foreach (var detail in response.ResponseDetails) foreach (var detail in response.ResponseDetails)
{ {
string responseText = "";
// Handle text-based questions (existing)
if (!string.IsNullOrWhiteSpace(detail.TextResponse)) if (!string.IsNullOrWhiteSpace(detail.TextResponse))
{
responseText = detail.TextResponse;
}
// Handle CheckBox questions (NEW)
else if (detail.QuestionType == QuestionType.CheckBox && detail.ResponseAnswers.Any())
{
var selectedAnswers = detail.ResponseAnswers
.Select(ra => detail.Question.Answers.FirstOrDefault(a => a.Id == ra.AnswerId)?.Text)
.Where(text => !string.IsNullOrEmpty(text))
.ToList();
responseText = $"Multiple Selection Question: {detail.Question.Text}\nSelected Options: {string.Join(", ", selectedAnswers)}\nAnalyze these selected workplace factors for mental health implications and patterns.";
}
// Create analysis request for ALL supported responses
if (!string.IsNullOrEmpty(responseText))
{ {
var request = new AnalysisRequest var request = new AnalysisRequest
{ {
ResponseId = response.Id, ResponseId = response.Id,
QuestionId = detail.QuestionId, QuestionId = detail.QuestionId,
ResponseText = detail.TextResponse, ResponseText = responseText,
QuestionText = detail.Question?.Text ?? "" QuestionText = detail.Question?.Text ?? ""
}; };
@ -868,13 +888,32 @@ Respond with a JSON array of applicable categories: [""category1"", ""category2"
{ {
foreach (var detail in response.ResponseDetails) foreach (var detail in response.ResponseDetails)
{ {
string responseText = "";
// Handle text-based questions
if (!string.IsNullOrWhiteSpace(detail.TextResponse)) if (!string.IsNullOrWhiteSpace(detail.TextResponse))
{
responseText = detail.TextResponse;
}
// Handle CheckBox questions
else if (detail.QuestionType == QuestionType.CheckBox && detail.ResponseAnswers.Any())
{
var selectedAnswers = detail.ResponseAnswers
.Select(ra => detail.Question.Answers.FirstOrDefault(a => a.Id == ra.AnswerId)?.Text)
.Where(text => !string.IsNullOrEmpty(text))
.ToList();
responseText = $"Multiple Selection Question: {detail.Question.Text}\nSelected Options: {string.Join(", ", selectedAnswers)}\nAnalyze these selected workplace factors for mental health implications and patterns.";
}
// Create analysis request if we have text to analyze
if (!string.IsNullOrEmpty(responseText))
{ {
var request = new AnalysisRequest var request = new AnalysisRequest
{ {
ResponseId = response.Id, ResponseId = response.Id,
QuestionId = detail.QuestionId, QuestionId = detail.QuestionId,
ResponseText = detail.TextResponse, ResponseText = responseText,
QuestionText = detail.Question?.Text ?? "" QuestionText = detail.Question?.Text ?? ""
}; };
@ -1087,6 +1126,8 @@ Respond with a JSON array of applicable categories: [""category1"", ""category2"
return false; return false;
} }
#endregion #endregion
} }
} }

View file

@ -40,23 +40,27 @@ namespace Web.Areas.Admin.Controllers
{ {
var questionnaires = await _context.Questionnaires var questionnaires = await _context.Questionnaires
.Include(q => q.Questions) .Include(q => q.Questions)
.Select(q => new .ToListAsync();
var result = questionnaires.Select(q => new
{ {
q.Id, q.Id,
q.Title, q.Title,
q.Description, q.Description,
QuestionCount = q.Questions.Count, QuestionCount = q.Questions.Count,
ResponseCount = _context.Responses.Count(r => r.QuestionnaireId == q.Id), ResponseCount = _context.Responses.Count(r => r.QuestionnaireId == q.Id),
TextResponseCount = _context.Responses AnalyzableResponseCount = _context.Responses
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.QuestionnaireId == q.Id) .Where(r => r.QuestionnaireId == q.Id)
.SelectMany(r => r.ResponseDetails) .SelectMany(r => r.ResponseDetails)
.Count(rd => !string.IsNullOrEmpty(rd.TextResponse)), .Count(rd => !string.IsNullOrEmpty(rd.TextResponse) ||
(rd.QuestionType == QuestionType.CheckBox && rd.ResponseAnswers.Any())),
LastResponse = _context.Responses LastResponse = _context.Responses
.Where(r => r.QuestionnaireId == q.Id) .Where(r => r.QuestionnaireId == q.Id)
.OrderByDescending(r => r.SubmissionDate) .OrderByDescending(r => r.SubmissionDate)
.Select(r => r.SubmissionDate) .Select(r => r.SubmissionDate)
.FirstOrDefault(), .FirstOrDefault(),
// Add Users information for displaying participant details
Users = _context.Responses Users = _context.Responses
.Where(r => r.QuestionnaireId == q.Id && !string.IsNullOrEmpty(r.UserName)) .Where(r => r.QuestionnaireId == q.Id && !string.IsNullOrEmpty(r.UserName))
.OrderByDescending(r => r.SubmissionDate) .OrderByDescending(r => r.SubmissionDate)
@ -66,14 +70,12 @@ namespace Web.Areas.Admin.Controllers
Email = r.UserEmail Email = r.UserEmail
}) })
.Distinct() .Distinct()
.Take(5) // Show up to 5 recent participants .Take(5)
.ToList() .ToList()
}) }).ToList();
.ToListAsync();
ViewBag.ServiceHealth = await _aiAnalysisService.GetServiceHealthStatusAsync(); ViewBag.ServiceHealth = await _aiAnalysisService.GetServiceHealthStatusAsync();
return View(result);
return View(questionnaires);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -101,21 +103,52 @@ namespace Web.Areas.Admin.Controllers
} }
// Check if there are responses to analyze // Check if there are responses to analyze
var hasResponses = await _context.Responses var totalResponses = await _context.Responses
.AnyAsync(r => r.QuestionnaireId == id); .CountAsync(r => r.QuestionnaireId == id);
if (!hasResponses) if (totalResponses == 0)
{ {
TempData["WarningMessage"] = "No responses found for this questionnaire."; TempData["WarningMessage"] = "No responses found for this questionnaire.";
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
_logger.LogInformation("Starting analysis for questionnaire {QuestionnaireId}", id); // Calculate analyzable responses (same logic as dashboard)
var analyzableCount = await _context.Responses
.Where(r => r.QuestionnaireId == id)
.SelectMany(r => r.ResponseDetails)
.CountAsync(rd => !string.IsNullOrEmpty(rd.TextResponse) ||
(rd.QuestionType == QuestionType.CheckBox &&
_context.ResponseAnswers.Any(ra => ra.ResponseDetailId == rd.Id)));
if (analyzableCount == 0)
{
TempData["WarningMessage"] = "No analyzable responses found. Responses must contain text or checkbox selections.";
return RedirectToAction(nameof(Index));
}
_logger.LogInformation("Starting analysis for questionnaire {QuestionnaireId}. Total responses: {TotalResponses}, Analyzable: {AnalyzableCount}",
id, totalResponses, analyzableCount);
// Generate comprehensive analysis // Generate comprehensive analysis
var analysisOverview = await _aiAnalysisService.GenerateQuestionnaireOverviewAsync(id); var analysisOverview = await _aiAnalysisService.GenerateQuestionnaireOverviewAsync(id);
_logger.LogInformation("Analysis completed successfully for questionnaire {QuestionnaireId}", id); var actuallyAnalyzed = analysisOverview.AnalyzedResponses;
_logger.LogInformation("Analysis completed for questionnaire {QuestionnaireId}. " +
"Analyzable: {AnalyzableCount}, Successfully Analyzed: {ActuallyAnalyzed}",
id, analyzableCount, actuallyAnalyzed);
// Provide user feedback about the difference if significant
if (analyzableCount > actuallyAnalyzed && (analyzableCount - actuallyAnalyzed) > 0)
{
var difference = analyzableCount - actuallyAnalyzed;
TempData["InfoMessage"] = $"Analysis completed successfully. {actuallyAnalyzed} of {analyzableCount} analyzable responses were processed. " +
$"{difference} response(s) could not be analyzed due to processing limitations or API constraints.";
}
else
{
TempData["SuccessMessage"] = $"Analysis completed successfully. All {actuallyAnalyzed} analyzable responses were processed.";
}
return View(analysisOverview); return View(analysisOverview);
} }
@ -295,17 +328,39 @@ namespace Web.Areas.Admin.Controllers
foreach (var response in responses) foreach (var response in responses)
{ {
foreach (var detail in response.ResponseDetails.Where(rd => !string.IsNullOrWhiteSpace(rd.TextResponse))) foreach (var detail in response.ResponseDetails)
{
string responseText = "";
// Handle text-based questions
if (!string.IsNullOrWhiteSpace(detail.TextResponse))
{
responseText = detail.TextResponse;
}
// Handle CheckBox questions
else if (detail.QuestionType == QuestionType.CheckBox && detail.ResponseAnswers.Any())
{
var selectedAnswers = detail.ResponseAnswers
.Select(ra => detail.Question.Answers.FirstOrDefault(a => a.Id == ra.AnswerId)?.Text)
.Where(text => !string.IsNullOrEmpty(text))
.ToList();
responseText = $"Multiple Selection Question: {detail.Question.Text}\nSelected Options: {string.Join(", ", selectedAnswers)}\nAnalyze these selected workplace factors for mental health implications and patterns.";
}
// Add to analysis requests if we have text to analyze
if (!string.IsNullOrEmpty(responseText))
{ {
analysisRequests.Add(new AnalysisRequest analysisRequests.Add(new AnalysisRequest
{ {
ResponseId = response.Id, ResponseId = response.Id,
QuestionId = detail.QuestionId, QuestionId = detail.QuestionId,
ResponseText = detail.TextResponse, ResponseText = responseText,
QuestionText = detail.Question?.Text ?? "" QuestionText = detail.Question?.Text ?? ""
}); });
} }
} }
}
if (!analysisRequests.Any()) if (!analysisRequests.Any())
{ {
@ -346,17 +401,39 @@ namespace Web.Areas.Admin.Controllers
foreach (var response in responses) foreach (var response in responses)
{ {
foreach (var detail in response.ResponseDetails.Where(rd => !string.IsNullOrWhiteSpace(rd.TextResponse))) foreach (var detail in response.ResponseDetails)
{
string responseText = "";
// Handle text-based questions
if (!string.IsNullOrWhiteSpace(detail.TextResponse))
{
responseText = detail.TextResponse;
}
// Handle CheckBox questions
else if (detail.QuestionType == QuestionType.CheckBox && detail.ResponseAnswers.Any())
{
var selectedAnswers = detail.ResponseAnswers
.Select(ra => detail.Question.Answers.FirstOrDefault(a => a.Id == ra.AnswerId)?.Text)
.Where(text => !string.IsNullOrEmpty(text))
.ToList();
responseText = $"Multiple Selection Question: {detail.Question.Text}\nSelected Options: {string.Join(", ", selectedAnswers)}\nAnalyze these selected workplace factors for mental health implications and patterns.";
}
// Add to analysis requests if we have text to analyze
if (!string.IsNullOrEmpty(responseText))
{ {
analysisRequests.Add(new AnalysisRequest analysisRequests.Add(new AnalysisRequest
{ {
ResponseId = response.Id, ResponseId = response.Id,
QuestionId = detail.QuestionId, QuestionId = detail.QuestionId,
ResponseText = detail.TextResponse, ResponseText = responseText,
QuestionText = detail.Question?.Text ?? "" QuestionText = detail.Question?.Text ?? ""
}); });
} }
} }
}
var results = await _aiAnalysisService.BatchAnalyzeResponsesAsync(analysisRequests); var results = await _aiAnalysisService.BatchAnalyzeResponsesAsync(analysisRequests);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -376,8 +376,8 @@
} }
.chip-status { .chip-status {
width: 10px; width: 15px;
height: 10px; height: 15px;
border-radius: 50%; border-radius: 50%;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@ -1062,6 +1062,7 @@
transition: all 0.2s ease; transition: all 0.2s ease;
min-height: 80px; min-height: 80px;
gap: 0.6rem; gap: 0.6rem;
cursor: pointer;
} }
.quick-btn:hover { .quick-btn:hover {
@ -2388,6 +2389,127 @@
.modal-footer-nexgen { .modal-footer-nexgen {
pointer-events: auto; pointer-events: auto;
position: relative; position: relative;
}
/* Enhanced Service Status Icons */
.service-status-nexgen {
display: flex;
align-items: center;
gap: 0.8rem;
font-family: var(--font-mono);
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.1em;
}
.status-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.service-nexgen.online .status-icon-wrapper {
background: rgba(52, 211, 153, 0.2);
box-shadow: 0 0 15px rgba(52, 211, 153, 0.3);
}
.service-nexgen.offline .status-icon-wrapper {
background: rgba(248, 113, 113, 0.2);
box-shadow: 0 0 15px rgba(248, 113, 113, 0.3);
}
/* Enhanced System Status */
.system-status .status-title {
display: flex;
align-items: center;
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.05em;
color: #ffffff;
margin-bottom: 0.3rem;
}
/* Enhanced Error States */
.error-nexgen .error-info h5 {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--neon-red);
margin-bottom: 0.4rem;
}
/* Service Strip Status Icons */
.service-chip .chip-status {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.service-chip.online .chip-status::before {
content: '\f058'; /* fa-check-circle */
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 0.8rem;
color: var(--neon-green);
position: absolute;
top: -2px;
right: -2px;
}
.service-chip.offline .chip-status::before {
content: '\f057'; /* fa-times-circle */
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 0.8rem;
color: var(--neon-red);
position: absolute;
}
/* Analysis Results Icons */
.result-header-nexgen .result-title::before {
margin-right: 0.5rem;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
}
.result-nexgen.sentiment .result-title::before {
content: '\f004'; /* fa-heart */
color: var(--neon-pink);
}
.result-nexgen.risk .result-title::before {
content: '\f3ed'; /* fa-shield-virus */
color: var(--neon-yellow);
}
/* Loading States with Icons */
.loading-nexgen h5::before {
content: '\f1ce'; /* fa-circle-notch */
font-family: 'Font Awesome 5 Free';
font-weight: 900;
margin-right: 0.5rem;
animation: fa-spin 2s linear infinite;
color: var(--neon-blue);
}
/* Success States */
.success-nexgen .success-title::before {
content: '\f058'; /* fa-check-circle */
font-family: 'Font Awesome 5 Free';
font-weight: 900;
margin-right: 0.5rem;
color: var(--neon-green);
} }
</style> </style>
} }
@ -2472,13 +2594,33 @@
<p>Real-time service monitoring</p> <p>Real-time service monitoring</p>
</div> </div>
</div> </div>
<!-- Enhanced Services Strip with Status Icons -->
<div class="services-strip"> <div class="services-strip">
@foreach (var service in serviceHealth) @foreach (var service in serviceHealth)
{ {
<div class="service-chip @(service.Value ? "online" : "offline")"> <div class="service-chip @(service.Value ? "online" : "offline")">
<div class="chip-status"></div> <div class="chip-status">
@if (service.Value)
{
<i class="fas fa-check-circle" style="color: #34d399;"></i>
}
else
{
<i class="fas fa-times-circle" style="color: #f87171;"></i>
}
</div>
<div class="chip-name">@service.Key.Replace("Azure", "")</div> <div class="chip-name">@service.Key.Replace("Azure", "")</div>
<div class="chip-state">@(service.Value ? "ACTIVE" : "DOWN")</div> <div class="chip-state">
@if (service.Value)
{
<i class="fas fa-check" style="color: #34d399; margin-right: 0.3rem;"></i>
}
else
{
<i class="fas fa-times" style="color: #f87171; margin-right: 0.3rem;"></i>
}
@(service.Value ? "ACTIVE" : "DOWN")
</div>
</div> </div>
} }
</div> </div>
@ -2687,8 +2829,9 @@
<div class="metric-chip textdata"> <div class="metric-chip textdata">
<div class="metric-icon"><i class="fas fa-comment-medical"></i></div> <div class="metric-icon"><i class="fas fa-comment-medical"></i></div>
<div class="metric-data"> <div class="metric-data">
<div class="metric-number">@questionnaire.TextResponseCount</div>
<div class="metric-text">TEXT DATA</div> <div class="metric-number">@questionnaire.AnalyzableResponseCount</div>
<div class="metric-text">ANALYZABLE DATA</div>
</div> </div>
</div> </div>
</div> </div>
@ -2697,12 +2840,12 @@
{ {
<div class="activity-indicator"> <div class="activity-indicator">
<div class="activity-pulse"></div> <div class="activity-pulse"></div>
<span>LAST ACTIVITY: @((DateTime)questionnaire.LastResponse).ToString("MMM dd, yyyy").ToUpper()</span> <span>LAST ACTIVITY: @((DateTime)questionnaire.LastResponse)</span>
</div> </div>
} }
<div class="action-zone"> <div class="action-zone">
@if (questionnaire.TextResponseCount > 0) @if (questionnaire.AnalyzableResponseCount > 0)
{ {
<button onclick="startAnalysisWithProgress(@questionnaire.Id, '@questionnaire.Title')" <button onclick="startAnalysisWithProgress(@questionnaire.Id, '@questionnaire.Title')"
class="primary-action"> class="primary-action">
@ -2722,21 +2865,21 @@
</button> </button>
<div class="quick-actions"> <div class="quick-actions">
<a href="@Url.Action("HighRiskResponses", new { id = questionnaire.Id })" <button onclick="startRiskScanWithProgress(@questionnaire.Id, '@questionnaire.Title')"
class="quick-btn risk-btn"> class="quick-btn risk-btn">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
<span>RISK SCAN</span> <span>RISK SCAN</span>
</a> </button>
<a href="@Url.Action("BatchAnalyze", new { id = questionnaire.Id })" <button onclick="startBatchOpsWithProgress(@questionnaire.Id, '@questionnaire.Title')"
class="quick-btn batch-btn"> class="quick-btn batch-btn">
<i class="fas fa-layer-group"></i> <i class="fas fa-layer-group"></i>
<span>BATCH OPS</span> <span>BATCH OPS</span>
</a> </button>
<a href="@Url.Action("AnalyzeTrends", new { id = questionnaire.Id })" <button onclick="startTrendsWithProgress(@questionnaire.Id, '@questionnaire.Title')"
class="quick-btn trend-btn"> class="quick-btn trend-btn">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
<span>TRENDS</span> <span>TRENDS</span>
</a> </button>
</div> </div>
} }
else else
@ -2747,7 +2890,7 @@
</div> </div>
<div class="disabled-text"> <div class="disabled-text">
<div class="disabled-title">INSUFFICIENT DATA</div> <div class="disabled-title">INSUFFICIENT DATA</div>
<div class="disabled-message">Requires text responses for analysis</div> <div class="disabled-message">Requires responses for analysis</div>
</div> </div>
</div> </div>
} }
@ -2985,7 +3128,137 @@
@section Scripts { @section Scripts {
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script> <script>
// Enhanced AI Services Check with proper event handling // Enhanced progress functions for all three buttons
function startRiskScanWithProgress(questionnaireId, title) {
document.getElementById('progressSubtitle').textContent = `Risk scanning "${title}"`;
document.getElementById('progressTitle').textContent = 'SCANNING FOR RISK INDICATORS';
// Update pipeline for risk scan
updatePipelineSteps(['INIT', 'SCAN', 'DETECT', 'ASSESS', 'ALERT']);
const modal = new bootstrap.Modal(document.getElementById('analysisProgressModal'));
modal.show();
animateProgress(() => {
window.location.href = '@Url.Action("HighRiskResponses")?id=' + questionnaireId;
});
}
function startBatchOpsWithProgress(questionnaireId, title) {
document.getElementById('progressSubtitle').textContent = `Batch processing "${title}"`;
document.getElementById('progressTitle').textContent = 'EXECUTING BATCH OPERATIONS';
// Update pipeline for batch operations
updatePipelineSteps(['INIT', 'QUEUE', 'PROCESS', 'VERIFY', 'COMPLETE']);
const modal = new bootstrap.Modal(document.getElementById('analysisProgressModal'));
modal.show();
animateProgress(() => {
window.location.href = '@Url.Action("BatchAnalyze")?id=' + questionnaireId;
});
}
function startTrendsWithProgress(questionnaireId, title) {
document.getElementById('progressSubtitle').textContent = `Analyzing trends in "${title}"`;
document.getElementById('progressTitle').textContent = 'CALCULATING TREND PATTERNS';
// Update pipeline for trends analysis
updatePipelineSteps(['INIT', 'COLLECT', 'PATTERN', 'TREND', 'VISUAL']);
const modal = new bootstrap.Modal(document.getElementById('analysisProgressModal'));
modal.show();
animateProgress(() => {
window.location.href = '@Url.Action("AnalyzeTrends")?id=' + questionnaireId;
});
}
// Updated main analysis function to use the new pipeline system
function startAnalysisWithProgress(questionnaireId, title) {
document.getElementById('progressSubtitle').textContent = `Processing "${title}"`;
document.getElementById('progressTitle').textContent = 'PROCESSING QUESTIONNAIRE DATA';
// Update pipeline for main analysis
updatePipelineSteps(['INIT', 'PARSE', 'ANALYZE', 'ASSESS', 'REPORT']);
const modal = new bootstrap.Modal(document.getElementById('analysisProgressModal'));
modal.show();
animateProgress(() => {
window.location.href = '@Url.Action("AnalyzeQuestionnaire")?id=' + questionnaireId;
});
}
// Helper function to update pipeline steps
function updatePipelineSteps(stepLabels) {
const pipeline = document.querySelector('.progress-pipeline');
let pipelineHTML = '';
stepLabels.forEach((label, index) => {
const isFirst = index === 0;
pipelineHTML += `
<div class="pipeline-step ${isFirst ? 'completed' : ''}">
<div class="step-marker"></div>
<div class="step-label">${label}</div>
</div>
`;
// Add connector except for last step
if (index < stepLabels.length - 1) {
pipelineHTML += '<div class="pipeline-connector"></div>';
}
});
pipeline.innerHTML = pipelineHTML;
}
// Reusable progress animation function
function animateProgress(onComplete) {
let progress = 0;
const progressFill = document.querySelector('.progress-fill-nexgen');
const progressPercent = document.querySelector('.progress-percent');
const steps = document.querySelectorAll('.pipeline-step');
const interval = setInterval(() => {
progress += Math.random() * 12 + 3;
if (progress > 95) progress = 95;
progressFill.style.width = progress + '%';
progressPercent.textContent = Math.round(progress) + '%';
// Update pipeline
const currentStep = Math.floor(progress / 20);
steps.forEach((step, index) => {
step.classList.remove('active', 'completed');
if (index < currentStep) {
step.classList.add('completed');
} else if (index === currentStep) {
step.classList.add('active');
}
});
if (progress >= 95) {
clearInterval(interval);
setTimeout(() => {
progressFill.style.width = '100%';
progressPercent.textContent = '100%';
steps.forEach(step => {
step.classList.remove('active');
step.classList.add('completed');
});
setTimeout(() => {
onComplete();
}, 800);
}, 300);
}
}, 150);
}
function testAIServices() { function testAIServices() {
const modal = new bootstrap.Modal(document.getElementById('aiServicesModal'), { const modal = new bootstrap.Modal(document.getElementById('aiServicesModal'), {
backdrop: true, backdrop: true,
@ -3008,14 +3281,20 @@
content += '</div>'; content += '</div>';
const allOnline = langService && openaiService; const allOnline = langService && openaiService;
const systemStatusIcon = allOnline ? 'fa-check-circle' : 'fa-exclamation-triangle';
const systemStatusColor = allOnline ? '#34d399' : '#fbbf24';
content += ` content += `
<div class="system-status ${allOnline ? 'operational' : 'degraded'}"> <div class="system-status ${allOnline ? 'operational' : 'degraded'}">
<div class="status-indicator"> <div class="status-indicator">
<div class="indicator-ring"></div> <div class="indicator-ring"></div>
<i class="fas ${allOnline ? 'fa-check' : 'fa-exclamation'}"></i> <i class="fas ${systemStatusIcon}" style="color: ${systemStatusColor};"></i>
</div> </div>
<div class="status-info"> <div class="status-info">
<div class="status-title">${allOnline ? 'ALL SYSTEMS OPERATIONAL' : 'SYSTEM DEGRADED'}</div> <div class="status-title">
<i class="fas ${systemStatusIcon}" style="color: ${systemStatusColor}; margin-right: 0.5rem;"></i>
${allOnline ? 'ALL SYSTEMS OPERATIONAL' : 'SYSTEM DEGRADED'}
</div>
<div class="status-message">${data.message}</div> <div class="status-message">${data.message}</div>
</div> </div>
</div> </div>
@ -3026,10 +3305,13 @@
document.getElementById('servicesStatusContent').innerHTML = ` document.getElementById('servicesStatusContent').innerHTML = `
<div class="error-nexgen"> <div class="error-nexgen">
<div class="error-visual"> <div class="error-visual">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-times-circle" style="color: #f87171; font-size: 2rem;"></i>
</div> </div>
<div class="error-info"> <div class="error-info">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-times-circle" style="color: #f87171;"></i>
<h5>DIAGNOSTIC FAILED</h5> <h5>DIAGNOSTIC FAILED</h5>
</div>
<p>${data.error || 'Unable to verify system status'}</p> <p>${data.error || 'Unable to verify system status'}</p>
</div> </div>
</div> </div>
@ -3040,10 +3322,13 @@
document.getElementById('servicesStatusContent').innerHTML = ` document.getElementById('servicesStatusContent').innerHTML = `
<div class="error-nexgen"> <div class="error-nexgen">
<div class="error-visual"> <div class="error-visual">
<i class="fas fa-wifi-slash"></i> <i class="fas fa-wifi-slash" style="color: #f87171; font-size: 2rem;"></i>
</div> </div>
<div class="error-info"> <div class="error-info">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-times-circle" style="color: #f87171;"></i>
<h5>CONNECTION TIMEOUT</h5> <h5>CONNECTION TIMEOUT</h5>
</div>
<p>Unable to reach diagnostic endpoint</p> <p>Unable to reach diagnostic endpoint</p>
</div> </div>
</div> </div>
@ -3054,6 +3339,8 @@
function createNexGenServiceCard(name, description, isOnline, icon) { function createNexGenServiceCard(name, description, isOnline, icon) {
const statusClass = isOnline ? 'online' : 'offline'; const statusClass = isOnline ? 'online' : 'offline';
const statusText = isOnline ? 'ACTIVE' : 'OFFLINE'; const statusText = isOnline ? 'ACTIVE' : 'OFFLINE';
const statusIcon = isOnline ? 'fa-check-circle' : 'fa-times-circle';
const statusIconColor = isOnline ? '#34d399' : '#f87171';
return ` return `
<div class="service-nexgen ${statusClass}"> <div class="service-nexgen ${statusClass}">
@ -3064,7 +3351,9 @@
<i class="fas ${icon}"></i> <i class="fas ${icon}"></i>
</div> </div>
<div class="service-status-nexgen"> <div class="service-status-nexgen">
<div class="status-dot-nexgen"></div> <div class="status-icon-wrapper">
<i class="fas ${statusIcon}" style="color: ${statusIconColor}; font-size: 1.2rem;"></i>
</div>
<span>${statusText}</span> <span>${statusText}</span>
</div> </div>
</div> </div>
@ -3227,54 +3516,6 @@
} }
} }
// Next-Gen Progress Animation
function startAnalysisWithProgress(questionnaireId, title) {
document.getElementById('progressSubtitle').textContent = `Processing "${title}"`;
const modal = new bootstrap.Modal(document.getElementById('analysisProgressModal'));
modal.show();
let progress = 0;
const progressFill = document.querySelector('.progress-fill-nexgen');
const progressPercent = document.querySelector('.progress-percent');
const steps = document.querySelectorAll('.pipeline-step');
const interval = setInterval(() => {
progress += Math.random() * 12 + 3;
if (progress > 95) progress = 95;
progressFill.style.width = progress + '%';
progressPercent.textContent = Math.round(progress) + '%';
// Update pipeline
const currentStep = Math.floor(progress / 20);
steps.forEach((step, index) => {
if (index <= currentStep) {
step.classList.add('active');
} else {
step.classList.remove('active');
}
});
if (progress >= 95) {
clearInterval(interval);
setTimeout(() => {
progressFill.style.width = '100%';
progressPercent.textContent = '100%';
steps.forEach(step => {
step.classList.add('active');
});
setTimeout(() => {
window.location.href = '@Url.Action("AnalyzeQuestionnaire")?id=' + questionnaireId;
}, 800);
}, 300);
}
}, 150);
}
// Custom SweetAlert styling // Custom SweetAlert styling
const swalStyle = document.createElement('style'); const swalStyle = document.createElement('style');
swalStyle.textContent = ` swalStyle.textContent = `
@ -3360,4 +3601,3 @@
}); });
</script> </script>
} }