diff --git a/Services/AIViewModel/AIViewModels.cs b/Services/AIViewModel/AIViewModels.cs new file mode 100644 index 0000000..c2cbfcd --- /dev/null +++ b/Services/AIViewModel/AIViewModels.cs @@ -0,0 +1,109 @@ +// Services/AIViewModel/AIAnalysisViewModels.cs +namespace Services.AIViewModel +{ + public enum RiskLevel + { + Low = 1, + Moderate = 2, + High = 3, + Critical = 4 + } + + public class SentimentAnalysisResult + { + public string Sentiment { get; set; } = string.Empty; // Positive, Negative, Neutral + public double ConfidenceScore { get; set; } + public double PositiveScore { get; set; } + public double NegativeScore { get; set; } + public double NeutralScore { get; set; } + public DateTime AnalyzedAt { get; set; } = DateTime.UtcNow; + } + + public class KeyPhrasesResult + { + public List KeyPhrases { get; set; } = new List(); + public List WorkplaceFactors { get; set; } = new List(); + public List EmotionalIndicators { get; set; } = new List(); + public DateTime ExtractedAt { get; set; } = DateTime.UtcNow; + } + + public class MentalHealthRiskAssessment + { + public RiskLevel RiskLevel { get; set; } + public double RiskScore { get; set; } // 0-1 scale + public List RiskIndicators { get; set; } = new List(); + public List ProtectiveFactors { get; set; } = new List(); + public bool RequiresImmediateAttention { get; set; } + public string RecommendedAction { get; set; } = string.Empty; + public DateTime AssessedAt { get; set; } = DateTime.UtcNow; + } + + public class WorkplaceInsight + { + public string Category { get; set; } = string.Empty; // e.g., "Work-Life Balance", "Management", "Workload" + public string Issue { get; set; } = string.Empty; + public string RecommendedIntervention { get; set; } = string.Empty; + public int Priority { get; set; } // 1-5 scale + public List AffectedAreas { get; set; } = new List(); + public DateTime IdentifiedAt { get; set; } = DateTime.UtcNow; + } + + public class ResponseAnalysisResult + { + public int ResponseId { get; set; } + public int QuestionId { get; set; } + public string QuestionText { get; set; } = string.Empty; + public string ResponseText { get; set; } = string.Empty; + public string AnonymizedResponseText { get; set; } = string.Empty; // PII removed + + // Azure Language Service Results + public SentimentAnalysisResult? SentimentAnalysis { get; set; } + public KeyPhrasesResult? KeyPhrases { get; set; } + + // Azure OpenAI Results + public MentalHealthRiskAssessment? RiskAssessment { get; set; } + public List Insights { get; set; } = new List(); + + public DateTime AnalyzedAt { get; set; } = DateTime.UtcNow; + public bool IsAnalysisComplete { get; set; } = false; + } + + public class QuestionnaireAnalysisOverview + { + public int QuestionnaireId { get; set; } + public string QuestionnaireTitle { get; set; } = string.Empty; + public int TotalResponses { get; set; } + public int AnalyzedResponses { get; set; } + + // Overall Statistics + public double OverallPositiveSentiment { get; set; } + public double OverallNegativeSentiment { get; set; } + public double OverallNeutralSentiment { get; set; } + + // Risk Distribution + public int LowRiskResponses { get; set; } + public int ModerateRiskResponses { get; set; } + public int HighRiskResponses { get; set; } + public int CriticalRiskResponses { get; set; } + + // Top Issues + public List TopWorkplaceIssues { get; set; } = new List(); + public List MostCommonKeyPhrases { get; set; } = new List(); + + public DateTime LastAnalyzedAt { get; set; } + public string ExecutiveSummary { get; set; } = string.Empty; + } + + public class AnalysisRequest + { + public int ResponseId { get; set; } + public int QuestionId { get; set; } + public string ResponseText { get; set; } = string.Empty; + public string QuestionText { get; set; } = string.Empty; + public string QuestionType { get; set; } = string.Empty; + public bool IncludeSentimentAnalysis { get; set; } = true; + public bool IncludeKeyPhraseExtraction { get; set; } = true; + public bool IncludeRiskAssessment { get; set; } = true; + public bool IncludeWorkplaceInsights { get; set; } = true; + } +} \ No newline at end of file diff --git a/Services/Implemnetation/AiAnalysisService.cs b/Services/Implemnetation/AiAnalysisService.cs index d4c0858..fe02c33 100644 --- a/Services/Implemnetation/AiAnalysisService.cs +++ b/Services/Implemnetation/AiAnalysisService.cs @@ -1,51 +1,1092 @@ -using Azure; +// Services/Implementation/AiAnalysisService.cs +using Azure; using Azure.AI.TextAnalytics; +using Azure.AI.OpenAI; +using OpenAI.Chat; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Services.AIViewModel; using Services.Interaces; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Data; +using Microsoft.EntityFrameworkCore; +using System.ClientModel; namespace Services.Implemnetation { - public class AiAnalysisService : IAiAnalysisService + public class AiAnalysisService : IAiAnalysisService, IDisposable { - private readonly TextAnalyticsClient _client; + private readonly TextAnalyticsClient _textAnalyticsClient; + private readonly AzureOpenAIClient _azureOpenAIClient; + private readonly ChatClient _chatClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly SurveyContext _context; + private readonly string _openAIDeploymentName; + private bool _disposed = false; - public AiAnalysisService(IConfiguration configuration) + public AiAnalysisService( + IConfiguration configuration, + ILogger logger, + SurveyContext context) { - var endpoint = configuration["AzureLanguageService:Endpoint"]; - var key = configuration["AzureLanguageService:Key"]; + _configuration = configuration; + _logger = logger; + _context = context; - _client = new TextAnalyticsClient(new Uri(endpoint), new AzureKeyCredential(key)); + // Initialize Azure Language Service + var languageEndpoint = _configuration["AzureLanguageService:Endpoint"]; + var languageKey = _configuration["AzureLanguageService:Key"]; + _textAnalyticsClient = new TextAnalyticsClient(new Uri(languageEndpoint), new AzureKeyCredential(languageKey)); + + // Initialize Azure OpenAI + var openAIEndpoint = _configuration["AzureOpenAI:Endpoint"]; + var openAIKey = _configuration["AzureOpenAI:Key"]; + _openAIDeploymentName = _configuration["AzureOpenAI:DeploymentName"]; + + _azureOpenAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), new AzureKeyCredential(openAIKey)); + _chatClient = _azureOpenAIClient.GetChatClient(_openAIDeploymentName); + + _logger.LogInformation("AiAnalysisService initialized successfully"); } - public async Task AnalyzeSentimentAsync(string text) - { - var response = await _client.AnalyzeSentimentAsync(text); - return response.Value; - } + #region Azure Language Service Methods - public async Task GetRiskAssessmentAsync(string text) + public async Task AnalyzeSentimentAsync(string text) { - var sentiment = await AnalyzeSentimentAsync(text); + try + { + if (string.IsNullOrWhiteSpace(text)) + { + return new SentimentAnalysisResult { Sentiment = "Neutral", ConfidenceScore = 0.0 }; + } - // Simple risk assessment logic - if (sentiment.Sentiment == TextSentiment.Negative && sentiment.ConfidenceScores.Negative > 0.8) - { - return "High Risk - Consider follow-up"; + var response = await _textAnalyticsClient.AnalyzeSentimentAsync(text); + var sentiment = response.Value; + + return new SentimentAnalysisResult + { + Sentiment = sentiment.Sentiment.ToString(), + ConfidenceScore = sentiment.ConfidenceScores.Positive > sentiment.ConfidenceScores.Negative + ? sentiment.ConfidenceScores.Positive + : sentiment.ConfidenceScores.Negative, + PositiveScore = sentiment.ConfidenceScores.Positive, + NegativeScore = sentiment.ConfidenceScores.Negative, + NeutralScore = sentiment.ConfidenceScores.Neutral, + AnalyzedAt = DateTime.UtcNow + }; } - else if (sentiment.Sentiment == TextSentiment.Negative) + catch (Exception ex) { - return "Medium Risk - Monitor"; - } - else - { - return "Low Risk"; + _logger.LogError(ex, "Error analyzing sentiment for text: {Text}", text); + throw; } } - public async Task> ExtractKeyPhrasesAsync(string text) + public async Task ExtractKeyPhrasesAsync(string text) { - var response = await _client.ExtractKeyPhrasesAsync(text); - return response.Value.ToList(); + try + { + if (string.IsNullOrWhiteSpace(text)) + { + return new KeyPhrasesResult(); + } + + var response = await _textAnalyticsClient.ExtractKeyPhrasesAsync(text); + var keyPhrases = response.Value.ToList(); + + // Mental health specific categorization + var workplaceFactors = keyPhrases.Where(phrase => + phrase.Contains("work") || phrase.Contains("manager") || phrase.Contains("team") || + phrase.Contains("deadline") || phrase.Contains("pressure") || phrase.Contains("colleague") || + phrase.Contains("office") || phrase.Contains("meeting") || phrase.Contains("project")).ToList(); + + var emotionalIndicators = keyPhrases.Where(phrase => + phrase.Contains("stress") || phrase.Contains("anxious") || phrase.Contains("tired") || + phrase.Contains("overwhelmed") || phrase.Contains("frustrated") || phrase.Contains("happy") || + phrase.Contains("satisfied") || phrase.Contains("motivated") || phrase.Contains("burned")).ToList(); + + return new KeyPhrasesResult + { + KeyPhrases = keyPhrases, + WorkplaceFactors = workplaceFactors, + EmotionalIndicators = emotionalIndicators, + ExtractedAt = DateTime.UtcNow + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting key phrases for text: {Text}", text); + throw; + } } + + public async Task AnonymizeTextAsync(string text) + { + try + { + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + var response = await _textAnalyticsClient.RecognizePiiEntitiesAsync(text); + var piiEntities = response.Value; + + string anonymizedText = text; + foreach (var entity in piiEntities.OrderByDescending(e => e.Offset)) + { + var replacement = entity.Category.ToString() switch + { + "Person" => "[NAME]", + "Email" => "[EMAIL]", + "PhoneNumber" => "[PHONE]", + "Address" => "[ADDRESS]", + _ => "[REDACTED]" + }; + + anonymizedText = anonymizedText.Remove(entity.Offset, entity.Length) + .Insert(entity.Offset, replacement); + } + + return anonymizedText; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error anonymizing text: {Text}", text); + return text; // Return original text if anonymization fails + } + } + + public async Task> DetectEntitiesAsync(string text) + { + try + { + if (string.IsNullOrWhiteSpace(text)) + { + return new List(); + } + + var response = await _textAnalyticsClient.RecognizeEntitiesAsync(text); + var entities = response.Value; + + return entities.Select(entity => $"{entity.Category}: {entity.Text}").ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting entities for text: {Text}", text); + throw; + } + } + + #endregion + + #region Azure OpenAI Methods + + public async Task AssessMentalHealthRiskAsync(string anonymizedText, string questionContext) + { + try + { + var prompt = $@" +As a mental health professional, analyze this workplace survey response and assess the mental health risk level. + +Question Context: {questionContext} +Response: {anonymizedText} + +Please provide: +1. Risk Level (Low, Moderate, High, Critical) +2. Risk Score (0.0 to 1.0) +3. Risk Indicators (specific concerns found) +4. Protective Factors (positive elements found) +5. Requires Immediate Attention (true/false) +6. Recommended Action + +Respond in this JSON format: +{{ + ""riskLevel"": ""Low"", + ""riskScore"": 0.0, + ""riskIndicators"": [""indicator1"", ""indicator2""], + ""protectiveFactors"": [""factor1"", ""factor2""], + ""requiresImmediateAttention"": false, + ""recommendedAction"": ""specific action recommendation"" +}}"; + + var messages = new List + { + new SystemChatMessage("You are a mental health professional specialized in workplace wellness. Always respond with a single valid JSON object only. No markdown, no explanations."), + new UserChatMessage(prompt) + }; + + var chatOptions = new ChatCompletionOptions() + { + Temperature = 0.3f + }; + + var response = await _chatClient.CompleteChatAsync(messages, chatOptions); + var content = response.Value.Content[0].Text; + + _logger.LogInformation("OpenAI Response: {Content}", content); + + // Parse more defensively + var jsonDoc = ParseLenient(content, out var parseErr); + if (jsonDoc == null) + { + _logger.LogWarning("Failed to parse JSON response from OpenAI: {Err}. Raw (first 800): {Raw}", + parseErr, content?.Length > 800 ? content[..800] : content); + + return new MentalHealthRiskAssessment + { + RiskLevel = RiskLevel.Moderate, + RiskScore = 0.5, + RiskIndicators = new List { "Unable to parse AI response" }, + ProtectiveFactors = new List(), + RequiresImmediateAttention = false, + RecommendedAction = "Manual review recommended due to analysis error", + AssessedAt = DateTime.UtcNow + }; + } + + var root = jsonDoc.RootElement; + + return new MentalHealthRiskAssessment + { + RiskLevel = Enum.TryParse(root.TryGetProperty("riskLevel", out var rl) ? (rl.GetString() ?? "Low") : "Low", out var riskLevel) + ? riskLevel : RiskLevel.Low, + RiskScore = root.TryGetProperty("riskScore", out var scoreProperty) + ? TryGetDoubleFlexible(scoreProperty, out var d) ? d : 0.0 + : 0.0, + RiskIndicators = root.TryGetProperty("riskIndicators", out var indicatorsProperty) + ? indicatorsProperty.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrEmpty(s)).ToList() + : new List(), + ProtectiveFactors = root.TryGetProperty("protectiveFactors", out var factorsProperty) + ? factorsProperty.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrEmpty(s)).ToList() + : new List(), + RequiresImmediateAttention = root.TryGetProperty("requiresImmediateAttention", out var attentionProperty) && + TryGetBoolFlexible(attentionProperty, out var b) && b, + RecommendedAction = root.TryGetProperty("recommendedAction", out var actionProperty) ? (actionProperty.GetString() ?? "") : "", + AssessedAt = DateTime.UtcNow + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error assessing mental health risk for text: {Text}", anonymizedText); + + // Return a safe default assessment instead of throwing + return new MentalHealthRiskAssessment + { + RiskLevel = RiskLevel.Moderate, + RiskScore = 0.5, + RiskIndicators = new List { $"Analysis error: {ex.Message}" }, + ProtectiveFactors = new List(), + RequiresImmediateAttention = false, + RecommendedAction = "Manual review recommended due to system error", + AssessedAt = DateTime.UtcNow + }; + } + } + + public async Task> GenerateWorkplaceInsightsAsync(string anonymizedText, string questionContext) + { + try + { + var prompt = $@" +Analyze this workplace survey response and identify specific workplace insights and intervention recommendations. + +Question Context: {questionContext} +Response: {anonymizedText} + +Identify workplace issues and provide specific, actionable intervention recommendations. Focus on: +- Work-life balance issues +- Management/leadership concerns +- Team dynamics problems +- Workload and stress factors +- Communication issues +- Organizational culture problems + +Respond in this JSON format: +{{ + ""insights"": [ + {{ + ""category"": ""category name"", + ""issue"": ""specific issue identified"", + ""recommendedIntervention"": ""specific action to take"", + ""priority"": 1, + ""affectedAreas"": [""area1"", ""area2""] + }} + ] +}}"; + + var messages = new List + { + new SystemChatMessage("You are a workplace mental health consultant. Always respond with a SINGLE valid JSON object ONLY. No markdown, no code fences, no explanations."), + new UserChatMessage(prompt) + }; + + var chatOptions = new ChatCompletionOptions() + { + Temperature = 0.4f + }; + + var response = await _chatClient.CompleteChatAsync(messages, chatOptions); + var content = response.Value.Content[0].Text; + + var jsonDoc = ParseLenient(content, out var parseErr); + if (jsonDoc == null) + { + _logger.LogWarning("Insights JSON parse failed: {Err}. Raw (first 800): {Raw}", + parseErr, content?.Length > 800 ? content[..800] : content); + + // Fail soft (avoid crashing controller) + return new List(); + } + + var root = jsonDoc.RootElement; + + if (!root.TryGetProperty("insights", out var insightsElement) || insightsElement.ValueKind != JsonValueKind.Array) + { + // No insights array — return empty rather than throw + return new List(); + } + + var workplaceInsights = new List(); + + foreach (var insight in insightsElement.EnumerateArray()) + { + // Be defensive with each field + string category = insight.TryGetProperty("category", out var cat) ? (cat.GetString() ?? "") : ""; + string issue = insight.TryGetProperty("issue", out var iss) ? (iss.GetString() ?? "") : ""; + string recommended = insight.TryGetProperty("recommendedIntervention", out var rec) ? (rec.GetString() ?? "") : ""; + int priority = 3; + if (insight.TryGetProperty("priority", out var pr)) + { + if (pr.ValueKind == JsonValueKind.Number && pr.TryGetInt32(out var pInt)) priority = pInt; + else if (pr.ValueKind == JsonValueKind.String && int.TryParse(pr.GetString(), out var pStr)) priority = pStr; + } + var affectedAreas = new List(); + if (insight.TryGetProperty("affectedAreas", out var aa) && aa.ValueKind == JsonValueKind.Array) + { + affectedAreas = aa.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); + } + + workplaceInsights.Add(new WorkplaceInsight + { + Category = category, + Issue = issue, + RecommendedIntervention = recommended, + Priority = priority, + AffectedAreas = affectedAreas, + IdentifiedAt = DateTime.UtcNow + }); + } + + return workplaceInsights; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating workplace insights for text: {Text}", anonymizedText); + // Fail soft to prevent controller crash + return new List(); + } + } + + public async Task GenerateExecutiveSummaryAsync(List analysisResults) + { + try + { + var totalResponses = analysisResults.Count; + var highRiskCount = analysisResults.Count(r => r.RiskAssessment?.RiskLevel >= RiskLevel.High); + var positiveResponses = analysisResults.Count(r => r.SentimentAnalysis?.Sentiment == "Positive"); + + var commonIssues = analysisResults + .SelectMany(r => r.Insights) + .GroupBy(i => i.Category) + .OrderByDescending(g => g.Count()) + .Take(5) + .Select(g => $"{g.Key}: {g.Count()} instances") + .ToList(); + + var prompt = $@" +Create an executive summary for a workplace mental health survey analysis. + +Survey Statistics: +- Total Responses: {totalResponses} +- High Risk Responses: {highRiskCount} +- Positive Responses: {positiveResponses} +- Most Common Issues: {string.Join(", ", commonIssues)} + +Create a comprehensive executive summary that includes: +1. Overall mental health status +2. Key findings and trends +3. Areas of concern +4. Positive indicators +5. Immediate action items +6. Long-term recommendations + +Keep it professional, actionable, and suitable for senior management."; + + var messages = new List + { + new SystemChatMessage("You are an executive consultant specializing in workplace mental health reporting."), + new UserChatMessage(prompt) + }; + + var chatOptions = new ChatCompletionOptions() + { + Temperature = 0.3f + }; + + var response = await _chatClient.CompleteChatAsync(messages, chatOptions); + return response.Value.Content[0].Text; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating executive summary"); + throw; + } + } + + public async Task> CategorizeResponseAsync(string anonymizedText) + { + try + { + var prompt = $@" +Categorize this workplace mental health survey response into relevant themes. + +Response: {anonymizedText} + +Choose from these categories (select all that apply): +- Work-Life Balance +- Workload Management +- Team Dynamics +- Leadership/Management +- Communication Issues +- Job Satisfaction +- Stress Management +- Career Development +- Work Environment +- Organizational Culture +- Mental Health Support +- Burnout Prevention + +Respond with a JSON array of applicable categories: [""category1"", ""category2""]"; + + var messages = new List + { + new SystemChatMessage("You are a mental health professional categorizing workplace responses. Always respond with a JSON array only."), + new UserChatMessage(prompt) + }; + + var chatOptions = new ChatCompletionOptions() + { + Temperature = 0.2f + }; + + var response = await _chatClient.CompleteChatAsync(messages, chatOptions); + var content = response.Value.Content[0].Text; + + // lenient array parse + var categories = DeserializeLenient>(content, out var err) ?? new List(); + if (!string.IsNullOrEmpty(err)) + { + _logger.LogWarning("Category JSON parse failed: {Err}. Raw (first 800): {Raw}", + err, content?.Length > 800 ? content[..800] : content); + } + + return categories; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error categorizing response: {Text}", anonymizedText); + throw; + } + } + + #endregion + + #region Combined Analysis Methods + + public async Task AnalyzeCompleteResponseAsync(AnalysisRequest request) + { + try + { + var result = new ResponseAnalysisResult + { + ResponseId = request.ResponseId, + QuestionId = request.QuestionId, + QuestionText = request.QuestionText, + ResponseText = request.ResponseText, + AnalyzedAt = DateTime.UtcNow + }; + + // Step 1: Anonymize the text + result.AnonymizedResponseText = await AnonymizeTextAsync(request.ResponseText); + + // Step 2: Azure Language Service Analysis + if (request.IncludeSentimentAnalysis) + { + result.SentimentAnalysis = await AnalyzeSentimentAsync(result.AnonymizedResponseText); + } + + if (request.IncludeKeyPhraseExtraction) + { + result.KeyPhrases = await ExtractKeyPhrasesAsync(result.AnonymizedResponseText); + } + + // Step 3: Azure OpenAI Analysis + if (request.IncludeRiskAssessment) + { + result.RiskAssessment = await AssessMentalHealthRiskAsync(result.AnonymizedResponseText, request.QuestionText); + } + + if (request.IncludeWorkplaceInsights) + { + result.Insights = await GenerateWorkplaceInsightsAsync(result.AnonymizedResponseText, request.QuestionText); + } + + result.IsAnalysisComplete = true; + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in complete response analysis for ResponseId: {ResponseId}", request.ResponseId); + throw; + } + } + + public async Task> AnalyzeQuestionResponsesAsync(int questionId, List requests) + { + var results = new List(); + + foreach (var request in requests) + { + try + { + var result = await AnalyzeCompleteResponseAsync(request); + results.Add(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing response {ResponseId} for question {QuestionId}", request.ResponseId, questionId); + // Continue with other responses even if one fails + } + } + + return results; + } + + public async Task GenerateQuestionnaireOverviewAsync(int questionnaireId) + { + try + { + // Get all responses for this questionnaire + var responses = await _context.Responses + .Include(r => r.Questionnaire) + .Include(r => r.ResponseDetails) + .ThenInclude(rd => rd.Question) + .Where(r => r.QuestionnaireId == questionnaireId) + .ToListAsync(); + + if (!responses.Any()) + { + return new QuestionnaireAnalysisOverview + { + QuestionnaireId = questionnaireId, + QuestionnaireTitle = "No responses found", + TotalResponses = 0 + }; + } + + // Analyze text responses + var analysisResults = new List(); + foreach (var response in responses) + { + foreach (var detail in response.ResponseDetails) + { + if (!string.IsNullOrWhiteSpace(detail.TextResponse)) + { + var request = new AnalysisRequest + { + ResponseId = response.Id, + QuestionId = detail.QuestionId, + ResponseText = detail.TextResponse, + QuestionText = detail.Question?.Text ?? "" + }; + + var analysisResult = await AnalyzeCompleteResponseAsync(request); + analysisResults.Add(analysisResult); + } + } + } + + // Generate overview + var overview = new QuestionnaireAnalysisOverview + { + QuestionnaireId = questionnaireId, + QuestionnaireTitle = responses.First().Questionnaire?.Title ?? "Unknown", + TotalResponses = responses.Count, + AnalyzedResponses = analysisResults.Count, + LastAnalyzedAt = DateTime.UtcNow + }; + + if (analysisResults.Any()) + { + // Calculate sentiment distribution + var sentimentResults = analysisResults.Where(r => r.SentimentAnalysis != null).ToList(); + if (sentimentResults.Any()) + { + overview.OverallPositiveSentiment = sentimentResults.Average(r => r.SentimentAnalysis!.PositiveScore); + overview.OverallNegativeSentiment = sentimentResults.Average(r => r.SentimentAnalysis!.NegativeScore); + overview.OverallNeutralSentiment = sentimentResults.Average(r => r.SentimentAnalysis!.NeutralScore); + } + + // Calculate risk distribution + var riskResults = analysisResults.Where(r => r.RiskAssessment != null).ToList(); + if (riskResults.Any()) + { + overview.LowRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.Low); + overview.ModerateRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.Moderate); + overview.HighRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.High); + overview.CriticalRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.Critical); + } + + // Get top workplace issues + overview.TopWorkplaceIssues = analysisResults + .SelectMany(r => r.Insights) + .GroupBy(i => i.Category) + .OrderByDescending(g => g.Count()) + .Take(5) + .Select(g => g.First()) + .ToList(); + + // Get common key phrases + overview.MostCommonKeyPhrases = analysisResults + .Where(r => r.KeyPhrases != null) + .SelectMany(r => r.KeyPhrases!.KeyPhrases) + .GroupBy(k => k) + .OrderByDescending(g => g.Count()) + .Take(10) + .Select(g => g.Key) + .ToList(); + + // Generate executive summary + overview.ExecutiveSummary = await GenerateExecutiveSummaryAsync(analysisResults); + } + + return overview; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating questionnaire overview for QuestionnaireId: {QuestionnaireId}", questionnaireId); + throw; + } + } + + public async Task> BatchAnalyzeResponsesAsync(List requests) + { + var results = new List(); + var tasks = new List>(); + + // Process in batches to avoid overwhelming the API + const int batchSize = 5; + for (int i = 0; i < requests.Count; i += batchSize) + { + var batch = requests.Skip(i).Take(batchSize); + foreach (var request in batch) + { + tasks.Add(AnalyzeCompleteResponseAsync(request)); + } + + // Wait for current batch to complete before starting next + var batchResults = await Task.WhenAll(tasks); + results.AddRange(batchResults); + tasks.Clear(); + + // Small delay between batches to respect API limits + await Task.Delay(1000); + } + + return results; + } + + #endregion + + #region Mental Health Specific Methods + + public async Task> IdentifyHighRiskResponsesAsync(int questionnaireId) + { + try + { + var overview = await GenerateQuestionnaireOverviewAsync(questionnaireId); + var allResults = await ExportAnonymizedAnalysisAsync(questionnaireId); + + return allResults + .Where(r => r.RiskAssessment != null && + (r.RiskAssessment.RiskLevel >= RiskLevel.High || r.RiskAssessment.RequiresImmediateAttention)) + .OrderByDescending(r => r.RiskAssessment!.RiskScore) + .ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error identifying high risk responses for QuestionnaireId: {QuestionnaireId}", questionnaireId); + throw; + } + } + + public async Task> AnalyzeMentalHealthTrendsAsync(int questionnaireId, DateTime fromDate, DateTime toDate) + { + try + { + var responses = await _context.Responses + .Include(r => r.ResponseDetails) + .ThenInclude(rd => rd.Question) + .Where(r => r.QuestionnaireId == questionnaireId && + r.SubmissionDate >= fromDate && + r.SubmissionDate <= toDate) + .OrderBy(r => r.SubmissionDate) + .ToListAsync(); + + // This would typically involve more complex trend analysis + // For now, return general insights + var insights = new List + { + new WorkplaceInsight + { + Category = "Trend Analysis", + Issue = $"Analysis of {responses.Count} responses from {fromDate:yyyy-MM-dd} to {toDate:yyyy-MM-dd}", + RecommendedIntervention = "Regular monitoring and follow-up assessments recommended", + Priority = 3, + IdentifiedAt = DateTime.UtcNow + } + }; + + return insights; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing mental health trends for QuestionnaireId: {QuestionnaireId}", questionnaireId); + throw; + } + } + + public async Task> CompareTeamMentalHealthAsync(int questionnaireId, List teamIdentifiers) + { + // This would require team identification in responses + // For now, return a basic implementation + var result = new Dictionary(); + + foreach (var team in teamIdentifiers) + { + result[team] = await GenerateQuestionnaireOverviewAsync(questionnaireId); + } + + return result; + } + + public async Task> GenerateInterventionRecommendationsAsync(int questionnaireId) + { + try + { + var overview = await GenerateQuestionnaireOverviewAsync(questionnaireId); + return overview.TopWorkplaceIssues; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating intervention recommendations for QuestionnaireId: {QuestionnaireId}", questionnaireId); + throw; + } + } + + #endregion + + #region Reporting Methods + + public async Task GenerateDetailedAnalysisReportAsync(int questionnaireId) + { + try + { + var overview = await GenerateQuestionnaireOverviewAsync(questionnaireId); + var highRiskResponses = await IdentifyHighRiskResponsesAsync(questionnaireId); + + var report = new StringBuilder(); + report.AppendLine($"# Mental Health Analysis Report"); + report.AppendLine($"**Questionnaire:** {overview.QuestionnaireTitle}"); + report.AppendLine($"**Analysis Date:** {DateTime.Now:yyyy-MM-dd HH:mm}"); + report.AppendLine($"**Total Responses:** {overview.TotalResponses}"); + report.AppendLine($"**Analyzed Responses:** {overview.AnalyzedResponses}"); + report.AppendLine(); + + report.AppendLine("## Executive Summary"); + report.AppendLine(overview.ExecutiveSummary); + report.AppendLine(); + + report.AppendLine("## Risk Distribution"); + report.AppendLine($"- Low Risk: {overview.LowRiskResponses}"); + report.AppendLine($"- Moderate Risk: {overview.ModerateRiskResponses}"); + report.AppendLine($"- High Risk: {overview.HighRiskResponses}"); + report.AppendLine($"- Critical Risk: {overview.CriticalRiskResponses}"); + report.AppendLine(); + + if (highRiskResponses.Any()) + { + report.AppendLine("## High Risk Responses Requiring Attention"); + report.AppendLine($"Found {highRiskResponses.Count} responses requiring immediate attention."); + report.AppendLine(); + } + + report.AppendLine("## Top Workplace Issues"); + foreach (var issue in overview.TopWorkplaceIssues.Take(5)) + { + report.AppendLine($"- **{issue.Category}:** {issue.Issue}"); + report.AppendLine($" - Recommended Action: {issue.RecommendedIntervention}"); + } + + return report.ToString(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating detailed analysis report for QuestionnaireId: {QuestionnaireId}", questionnaireId); + throw; + } + } + + public async Task> ExportAnonymizedAnalysisAsync(int questionnaireId) + { + try + { + var responses = await _context.Responses + .Include(r => r.ResponseDetails) + .ThenInclude(rd => rd.Question) + .Where(r => r.QuestionnaireId == questionnaireId) + .ToListAsync(); + + var results = new List(); + + foreach (var response in responses) + { + foreach (var detail in response.ResponseDetails) + { + if (!string.IsNullOrWhiteSpace(detail.TextResponse)) + { + var request = new AnalysisRequest + { + ResponseId = response.Id, + QuestionId = detail.QuestionId, + ResponseText = detail.TextResponse, + QuestionText = detail.Question?.Text ?? "" + }; + + var result = await AnalyzeCompleteResponseAsync(request); + results.Add(result); + } + } + } + + return results; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting anonymized analysis for QuestionnaireId: {QuestionnaireId}", questionnaireId); + throw; + } + } + + public async Task GenerateManagementDashboardAsync(int questionnaireId) + { + return await GenerateQuestionnaireOverviewAsync(questionnaireId); + } + + #endregion + + #region Utility Methods + + public async Task TestAzureLanguageServiceConnectionAsync() + { + try + { + await _textAnalyticsClient.AnalyzeSentimentAsync("Test connection"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Azure Language Service connection test failed"); + return false; + } + } + + public async Task TestAzureOpenAIConnectionAsync() + { + try + { + var messages = new List + { + new UserChatMessage("Test connection") + }; + + var chatOptions = new ChatCompletionOptions(); + + await _chatClient.CompleteChatAsync(messages, chatOptions); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Azure OpenAI connection test failed"); + return false; + } + } + + public Task ValidateAnalysisRequestAsync(AnalysisRequest request) + { + return Task.FromResult(!string.IsNullOrWhiteSpace(request.ResponseText) && + request.ResponseId > 0 && + request.QuestionId > 0); + } + + public async Task> GetServiceHealthStatusAsync() + { + var status = new Dictionary(); + + status["AzureLanguageService"] = await TestAzureLanguageServiceConnectionAsync(); + status["AzureOpenAI"] = await TestAzureOpenAIConnectionAsync(); + + return status; + } + + #endregion + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Dispose managed resources + _context?.Dispose(); + } + _disposed = true; + } + } + + #endregion + + #region Private JSON Helpers (no new models) + + private static readonly Regex JsonObjectRegex = + new(@"(\{(?:[^{}]|(?\{)|(?<-o>\}))*(?(o)(?!))\})", + RegexOptions.Singleline | RegexOptions.Compiled); + + private static bool TryExtractJsonObject(string text, out string json) + { + json = text?.Trim() ?? ""; + + // strip markdown code fences if present + if (json.StartsWith("```")) + { + json = Regex.Replace(json, "^```(?:json)?\\s*|\\s*```$", "", + RegexOptions.IgnoreCase | RegexOptions.Singleline).Trim(); + } + + if (json.StartsWith("{") && json.EndsWith("}")) + return true; + + var m = JsonObjectRegex.Match(json); + if (!m.Success) return false; + + json = m.Value; + return true; + } + + private static JsonDocument? ParseLenient(string content, out string? error) + { + error = null; + + if (!TryExtractJsonObject(content, out var jsonOnly)) + { + error = "No JSON object found in model response."; + return null; + } + + var docOptions = new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip + }; + + try + { + return JsonDocument.Parse(jsonOnly, docOptions); + } + catch (Exception ex) + { + error = ex.Message; + return null; + } + } + + private static T? DeserializeLenient(string content, out string? error) + { + error = null; + + if (!TryExtractJsonObject(content, out var jsonOnly)) + { + // also try plain array (for CategorizeResponseAsync) + var trimmed = (content ?? "").Trim(); + if (typeof(T) == typeof(List) && trimmed.StartsWith("[") && trimmed.EndsWith("]")) + { + jsonOnly = trimmed; + } + else + { + error = "No JSON object/array found in model response."; + return default; + } + } + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + + try + { + return JsonSerializer.Deserialize(jsonOnly, options); + } + catch (Exception ex) + { + error = ex.Message; + return default; + } + } + + private static bool TryGetDoubleFlexible(JsonElement el, out double value) + { + value = 0; + if (el.ValueKind == JsonValueKind.Number) return el.TryGetDouble(out value); + if (el.ValueKind == JsonValueKind.String) return double.TryParse(el.GetString(), out value); + return false; + } + + private static bool TryGetBoolFlexible(JsonElement el, out bool value) + { + value = false; + if (el.ValueKind == JsonValueKind.True) { value = true; return true; } + if (el.ValueKind == JsonValueKind.False) { value = false; return true; } + if (el.ValueKind == JsonValueKind.String) return bool.TryParse(el.GetString(), out value); + return false; + } + + #endregion } } diff --git a/Services/Interaces/IAiAnalysisService.cs b/Services/Interaces/IAiAnalysisService.cs index fd083ec..b399cfe 100644 --- a/Services/Interaces/IAiAnalysisService.cs +++ b/Services/Interaces/IAiAnalysisService.cs @@ -1,11 +1,147 @@ -using Azure.AI.TextAnalytics; +// Services/Interfaces/IAiAnalysisService.cs +using Services.AIViewModel; namespace Services.Interaces { public interface IAiAnalysisService { - Task AnalyzeSentimentAsync(string text); - Task GetRiskAssessmentAsync(string text); - Task> ExtractKeyPhrasesAsync(string text); + #region Azure Language Service Methods + + /// + /// Analyzes sentiment of response text using Azure Language Service + /// + Task AnalyzeSentimentAsync(string text); + + /// + /// Extracts key phrases and workplace factors from response text + /// + Task ExtractKeyPhrasesAsync(string text); + + /// + /// Removes PII (Personally Identifiable Information) from response text + /// + Task AnonymizeTextAsync(string text); + + /// + /// Detects entities in text (workplace factors, departments, roles, etc.) + /// + Task> DetectEntitiesAsync(string text); + + #endregion + + #region Azure OpenAI Methods + + /// + /// Assesses mental health risk level using GPT-3.5 Turbo + /// + Task AssessMentalHealthRiskAsync(string anonymizedText, string questionContext); + + /// + /// Generates workplace insights and intervention recommendations + /// + Task> GenerateWorkplaceInsightsAsync(string anonymizedText, string questionContext); + + /// + /// Creates executive summary for questionnaire analysis + /// + Task GenerateExecutiveSummaryAsync(List analysisResults); + + /// + /// Categorizes responses into mental health themes + /// + Task> CategorizeResponseAsync(string anonymizedText); + + #endregion + + #region Combined Analysis Methods + + /// + /// Performs complete AI analysis on a single response (both Azure services) + /// + Task AnalyzeCompleteResponseAsync(AnalysisRequest request); + + /// + /// Analyzes multiple responses for a specific question + /// + Task> AnalyzeQuestionResponsesAsync(int questionId, List requests); + + /// + /// Generates comprehensive analysis overview for entire questionnaire + /// + Task GenerateQuestionnaireOverviewAsync(int questionnaireId); + + /// + /// Batch processes multiple responses efficiently + /// + Task> BatchAnalyzeResponsesAsync(List requests); + + #endregion + + #region Mental Health Specific Methods + + /// + /// Identifies responses requiring immediate attention (high risk) + /// + Task> IdentifyHighRiskResponsesAsync(int questionnaireId); + + /// + /// Generates mental health trends across time periods + /// + Task> AnalyzeMentalHealthTrendsAsync(int questionnaireId, DateTime fromDate, DateTime toDate); + + /// + /// Compares mental health metrics between departments/teams + /// + Task> CompareTeamMentalHealthAsync(int questionnaireId, List teamIdentifiers); + + /// + /// Generates intervention recommendations based on overall analysis + /// + Task> GenerateInterventionRecommendationsAsync(int questionnaireId); + + #endregion + + #region Reporting Methods + + /// + /// Creates detailed analysis report for specific questionnaire + /// + Task GenerateDetailedAnalysisReportAsync(int questionnaireId); + + /// + /// Generates anonymized data export for further analysis + /// + Task> ExportAnonymizedAnalysisAsync(int questionnaireId); + + /// + /// Creates management dashboard summary + /// + Task GenerateManagementDashboardAsync(int questionnaireId); + + #endregion + + #region Utility Methods + + /// + /// Tests connection to Azure Language Service + /// + Task TestAzureLanguageServiceConnectionAsync(); + + /// + /// Tests connection to Azure OpenAI Service + /// + Task TestAzureOpenAIConnectionAsync(); + + /// + /// Validates analysis request before processing + /// + Task ValidateAnalysisRequestAsync(AnalysisRequest request); + + /// + /// Gets analysis service health status + /// + Task> GetServiceHealthStatusAsync(); + + #endregion } -} +} \ No newline at end of file diff --git a/Services/Options/AzureOptions.cs b/Services/Options/AzureOptions.cs new file mode 100644 index 0000000..e828e08 --- /dev/null +++ b/Services/Options/AzureOptions.cs @@ -0,0 +1,17 @@ +// Services/Options/AzureOptions.cs +namespace Services.Options +{ + public class AzureLanguageServiceOptions + { + public string Endpoint { get; set; } = default!; + public string Key { get; set; } = default!; + public string Region { get; set; } = default!; + } + + public class AzureOpenAIOptions + { + public string Endpoint { get; set; } = default!; + public string Key { get; set; } = default!; + public string DeploymentName { get; set; } = default!; + } +} diff --git a/Services/Services.csproj b/Services/Services.csproj index 9af6a3d..97344c3 100644 --- a/Services/Services.csproj +++ b/Services/Services.csproj @@ -7,6 +7,7 @@ + diff --git a/Web/Areas/Admin/Controllers/NewslettersController.cs b/Web/Areas/Admin/Controllers/NewslettersController.cs index fcbf7b1..9b7f4c4 100644 --- a/Web/Areas/Admin/Controllers/NewslettersController.cs +++ b/Web/Areas/Admin/Controllers/NewslettersController.cs @@ -8,7 +8,7 @@ using Microsoft.VisualStudio.TextTemplating; using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages.Manage; using Model; using Newtonsoft.Json.Linq; -using OpenAI_API; + using Services.EmailSend; using Services.Implemnetation; using Services.Interaces; diff --git a/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs b/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs index 95a1596..d670c20 100644 --- a/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs +++ b/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs @@ -1,173 +1,623 @@ -using Azure; +// Web/Areas/Admin/Controllers/SurveyAnalysisController.cs using Data; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Model; -using Web.ViewModel.QuestionnaireVM; - -using System.IO; -using Microsoft.AspNetCore.Authorization; -using Services.Implemnetation; +using Services.AIViewModel; using Services.Interaces; +using System.Text; +using System.Text.Json; namespace Web.Areas.Admin.Controllers { - - + public class SurveyAnalysisController : Controller { - private readonly SurveyContext _context; private readonly IAiAnalysisService _aiAnalysisService; + private readonly SurveyContext _context; + private readonly ILogger _logger; - public SurveyAnalysisController(SurveyContext context, IAiAnalysisService aiAnalysisService) + public SurveyAnalysisController( + IAiAnalysisService aiAnalysisService, + SurveyContext context, + ILogger logger) { + _aiAnalysisService = aiAnalysisService; _context = context; - _aiAnalysisService = aiAnalysisService; - } - public IActionResult Index() - { - var questionnaires = _context.Responses - .Include(r => r.Questionnaire) // Ensure the navigation property is correctly set up in the Response model - .Select(r => r.Questionnaire) - .Distinct() // Ensure each questionnaire is listed once - .ToList(); - - var viewModel = questionnaires.Select(q => new ResponseQuestionnaireWithUsersViewModel - { - Id = q.Id, - Title = q.Title - - }).ToList(); - - return View(viewModel); + _logger = logger; } + #region Dashboard and Overview - - - [HttpGet] - public IActionResult Analysis(int id) + /// + /// Main dashboard showing all questionnaires available for analysis + /// + public async Task Index() { - var viewModel = _context.Responses - .Where(r => r.QuestionnaireId == id) - .Include(r => r.Questionnaire) - .ThenInclude(q => q.Questions) - .ThenInclude(q => q.Answers) - .Select(r => new ResponseQuestionnaireWithUsersViewModel - { - Id = r.Questionnaire.Id, - Title = r.Questionnaire.Title, - Description = r.Questionnaire.Description, - UserName = r.UserName, // Assuming you want the user who answered the questionnaire - Email = r.UserEmail, - ParticipantCount = _context.Responses.Count(rs => rs.QuestionnaireId == id), - QuestionsAnsweredPercentage = _context.Questionnaires - .Where(q => q.Id == id) - .SelectMany(q => q.Questions) - .Count() > 0 - ? (double)_context.ResponseDetails - .Where(rd => rd.Response.QuestionnaireId == id && rd.TextResponse != null) - .Select(rd => rd.QuestionId) - .Distinct() - .Count() / _context.Questionnaires - .Where(q => q.Id == id) - .SelectMany(q => q.Questions) - .Count() * 100.0 - : 0.0, // Avoid division by zero - Questions = r.Questionnaire.Questions.Select(q => new ResponseQuestionViewModel - { - Id = q.Id, - Text = q.Text, - Type = q.Type, - Answers = q.Answers.Select(a => new ResponseAnswerViewModel - { - Id = a.Id, - Text = a.Text ?? _context.ResponseDetails - .Where(rd => rd.QuestionId == q.Id && rd.ResponseId == r.Id) - .Select(rd => rd.TextResponse) - .FirstOrDefault(), - Count = _context.ResponseAnswers.Count(ra => ra.AnswerId == a.Id) // Count how many times each answer was selected - }).ToList(), - SelectedAnswerIds = _context.ResponseDetails - .Where(rd => rd.QuestionId == q.Id) - .SelectMany(rd => rd.ResponseAnswers) - .Select(ra => ra.AnswerId) - .Distinct() - .ToList(), - SelectedText = _context.ResponseDetails - .Where(rd => rd.QuestionId == q.Id) - .Select(rd => rd.TextResponse) - .Where(t => !string.IsNullOrEmpty(t)) - .ToList() - }).ToList(), - Users = _context.Responses - .Where(rs => rs.QuestionnaireId == id) - .Select(rs => new ResponseUserViewModel - { - UserName = rs.UserName, - Email = rs.UserEmail - }).Distinct().ToList() - }) - .FirstOrDefault(); - - if (viewModel == null) + try { - return NotFound("No questionnaire found for the given ID."); - } - - return View(viewModel); - - } - - [HttpGet] - public async Task AiAnalysis(int id) - { - // Get survey responses for the questionnaire - var responses = await _context.Responses - .Where(r => r.QuestionnaireId == id) - .Include(r => r.ResponseDetails) - .ThenInclude(rd => rd.Question) - .Include(r => r.Questionnaire) - .ToListAsync(); - - if (!responses.Any()) - { - return NotFound("No responses found for this questionnaire."); - } - - var analysisResults = new List(); - - foreach (var response in responses) - { - foreach (var detail in response.ResponseDetails) - { - if (!string.IsNullOrWhiteSpace(detail.TextResponse)) + var questionnaires = await _context.Questionnaires + .Include(q => q.Questions) + .Select(q => new { - // Analyze the text response with AI - var sentiment = await _aiAnalysisService.AnalyzeSentimentAsync(detail.TextResponse); - var riskAssessment = await _aiAnalysisService.GetRiskAssessmentAsync(detail.TextResponse); - var keyPhrases = await _aiAnalysisService.ExtractKeyPhrasesAsync(detail.TextResponse); + q.Id, + q.Title, + q.Description, + QuestionCount = q.Questions.Count, + ResponseCount = _context.Responses.Count(r => r.QuestionnaireId == q.Id), + TextResponseCount = _context.Responses + .Where(r => r.QuestionnaireId == q.Id) + .SelectMany(r => r.ResponseDetails) + .Count(rd => !string.IsNullOrEmpty(rd.TextResponse)), + LastResponse = _context.Responses + .Where(r => r.QuestionnaireId == q.Id) + .OrderByDescending(r => r.SubmissionDate) + .Select(r => r.SubmissionDate) + .FirstOrDefault() + }) + .ToListAsync(); - analysisResults.Add(new + ViewBag.ServiceHealth = await _aiAnalysisService.GetServiceHealthStatusAsync(); + + return View(questionnaires); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading survey analysis dashboard"); + TempData["ErrorMessage"] = "Error loading dashboard. Please try again."; + return View(new List()); + } + } + + /// + /// Generate comprehensive analysis overview for a questionnaire + /// + public async Task AnalyzeQuestionnaire(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .Include(q => q.Questions) + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + // Check if there are responses to analyze + var hasResponses = await _context.Responses + .AnyAsync(r => r.QuestionnaireId == id); + + if (!hasResponses) + { + TempData["WarningMessage"] = "No responses found for this questionnaire."; + return RedirectToAction(nameof(Index)); + } + + _logger.LogInformation("Starting analysis for questionnaire {QuestionnaireId}", id); + + // Generate comprehensive analysis + var analysisOverview = await _aiAnalysisService.GenerateQuestionnaireOverviewAsync(id); + + _logger.LogInformation("Analysis completed successfully for questionnaire {QuestionnaireId}", id); + + return View(analysisOverview); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing questionnaire {QuestionnaireId}: {ErrorMessage}", id, ex.Message); + TempData["ErrorMessage"] = $"Error analyzing questionnaire: {ex.Message}. Please check the logs for more details."; + return RedirectToAction(nameof(Index)); + } + } + + #endregion + + #region High-Risk Response Management + + /// + /// Identify and display high-risk responses requiring immediate attention + /// + public async Task HighRiskResponses(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + var highRiskResponses = await _aiAnalysisService.IdentifyHighRiskResponsesAsync(id); + + ViewBag.QuestionnaireName = questionnaire.Title; + ViewBag.QuestionnaireId = id; + + return View(highRiskResponses); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error identifying high-risk responses for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error identifying high-risk responses. Please try again."; + return RedirectToAction(nameof(Index)); + } + } + + /// + /// View detailed analysis of a specific high-risk response + /// + public async Task ViewHighRiskResponse(int questionnaireId, int responseId) + { + try + { + var response = await _context.Responses + .Include(r => r.Questionnaire) + .Include(r => r.ResponseDetails) + .ThenInclude(rd => rd.Question) + .FirstOrDefaultAsync(r => r.Id == responseId && r.QuestionnaireId == questionnaireId); + + if (response == null) + { + TempData["ErrorMessage"] = "Response not found."; + return RedirectToAction(nameof(HighRiskResponses), new { id = questionnaireId }); + } + + // Get AI analysis for each text response + var analysisResults = new List(); + + foreach (var detail in response.ResponseDetails.Where(rd => !string.IsNullOrWhiteSpace(rd.TextResponse))) + { + var analysisRequest = new AnalysisRequest + { + ResponseId = response.Id, + QuestionId = detail.QuestionId, + ResponseText = detail.TextResponse, + QuestionText = detail.Question?.Text ?? "" + }; + + var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); + analysisResults.Add(analysis); + } + + ViewBag.Response = response; + return View(analysisResults); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error viewing high-risk response {ResponseId}", responseId); + TempData["ErrorMessage"] = "Error loading response details. Please try again."; + return RedirectToAction(nameof(HighRiskResponses), new { id = questionnaireId }); + } + } + + #endregion + + #region Individual Response Analysis + + /// + /// Analyze a single response in detail + /// + [HttpPost] + public async Task AnalyzeResponse(int responseId, int questionId, string responseText, string questionText) + { + try + { + var analysisRequest = new AnalysisRequest + { + ResponseId = responseId, + QuestionId = questionId, + ResponseText = responseText, + QuestionText = questionText + }; + + var isValid = await _aiAnalysisService.ValidateAnalysisRequestAsync(analysisRequest); + if (!isValid) + { + return Json(new { success = false, message = "Invalid analysis request." }); + } + + var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); + + return Json(new + { + success = true, + analysis = new + { + sentiment = analysis.SentimentAnalysis, + keyPhrases = analysis.KeyPhrases?.KeyPhrases ?? new List(), + riskLevel = analysis.RiskAssessment?.RiskLevel.ToString(), + riskScore = analysis.RiskAssessment?.RiskScore ?? 0, + requiresAttention = analysis.RiskAssessment?.RequiresImmediateAttention ?? false, + recommendedAction = analysis.RiskAssessment?.RecommendedAction ?? "", + insights = analysis.Insights.Select(i => new { + category = i.Category, + issue = i.Issue, + intervention = i.RecommendedIntervention, + priority = i.Priority + }) + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing individual response {ResponseId}", responseId); + return Json(new { success = false, message = "Error analyzing response. Please try again." }); + } + } + + #endregion + + #region Batch Analysis + + /// + /// Process batch analysis for all responses in a questionnaire + /// + public async Task BatchAnalyze(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + // Get all text responses for the questionnaire + var responses = await _context.Responses + .Include(r => r.ResponseDetails) + .ThenInclude(rd => rd.Question) + .Where(r => r.QuestionnaireId == id) + .ToListAsync(); + + var analysisRequests = new List(); + + foreach (var response in responses) + { + foreach (var detail in response.ResponseDetails.Where(rd => !string.IsNullOrWhiteSpace(rd.TextResponse))) + { + analysisRequests.Add(new AnalysisRequest { - UserName = response.UserName, - UserEmail = response.UserEmail, - Question = detail.Question.Text, - Response = detail.TextResponse, - Sentiment = sentiment.Sentiment.ToString(), - PositiveScore = sentiment.ConfidenceScores.Positive, - NegativeScore = sentiment.ConfidenceScores.Negative, - NeutralScore = sentiment.ConfidenceScores.Neutral, - RiskAssessment = riskAssessment, - KeyPhrases = keyPhrases + ResponseId = response.Id, + QuestionId = detail.QuestionId, + ResponseText = detail.TextResponse, + QuestionText = detail.Question?.Text ?? "" }); } } + + if (!analysisRequests.Any()) + { + TempData["WarningMessage"] = "No text responses found to analyze."; + return RedirectToAction(nameof(AnalyzeQuestionnaire), new { id }); + } + + // Process batch analysis (this might take a while) + ViewBag.QuestionnaireName = questionnaire.Title; + ViewBag.QuestionnaireId = id; + ViewBag.TotalRequests = analysisRequests.Count; + + return View("BatchAnalysisProgress"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting batch analysis for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error starting batch analysis. Please try again."; + return RedirectToAction(nameof(Index)); } - - ViewBag.QuestionnaireName = responses.First().Questionnaire.Title; - return View(analysisResults); } - } -} + /// + /// AJAX endpoint for batch analysis progress + /// + [HttpPost] + public async Task ProcessBatchAnalysis(int questionnaireId) + { + try + { + var responses = await _context.Responses + .Include(r => r.ResponseDetails) + .ThenInclude(rd => rd.Question) + .Where(r => r.QuestionnaireId == questionnaireId) + .ToListAsync(); + + var analysisRequests = new List(); + + foreach (var response in responses) + { + foreach (var detail in response.ResponseDetails.Where(rd => !string.IsNullOrWhiteSpace(rd.TextResponse))) + { + analysisRequests.Add(new AnalysisRequest + { + ResponseId = response.Id, + QuestionId = detail.QuestionId, + ResponseText = detail.TextResponse, + QuestionText = detail.Question?.Text ?? "" + }); + } + } + + var results = await _aiAnalysisService.BatchAnalyzeResponsesAsync(analysisRequests); + + return Json(new + { + success = true, + processedCount = results.Count, + highRiskCount = results.Count(r => r.RiskAssessment?.RiskLevel >= RiskLevel.High), + message = $"Successfully analyzed {results.Count} responses." + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing batch analysis for questionnaire {QuestionnaireId}", questionnaireId); + return Json(new + { + success = false, + message = "Error processing batch analysis. Please try again." + }); + } + } + + #endregion + + #region Reporting + + /// + /// Generate detailed analysis report for management + /// + public async Task GenerateReport(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + var report = await _aiAnalysisService.GenerateDetailedAnalysisReportAsync(id); + + ViewBag.QuestionnaireName = questionnaire.Title; + ViewBag.QuestionnaireId = id; + ViewBag.Report = report; + ViewBag.GeneratedDate = DateTime.Now; + + return View(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating report for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error generating report. Please try again."; + return RedirectToAction(nameof(Index)); + } + } + + /// + /// Download report as text file + /// + public async Task DownloadReport(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + var report = await _aiAnalysisService.GenerateDetailedAnalysisReportAsync(id); + var bytes = Encoding.UTF8.GetBytes(report); + + var fileName = $"Mental_Health_Analysis_{questionnaire.Title}_{DateTime.Now:yyyy-MM-dd}.txt"; + + return File(bytes, "text/plain", fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading report for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error downloading report. Please try again."; + return RedirectToAction(nameof(Index)); + } + } + + /// + /// Export anonymized analysis data + /// + public async Task ExportAnalysis(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + var analysisData = await _aiAnalysisService.ExportAnonymizedAnalysisAsync(id); + + var json = System.Text.Json.JsonSerializer.Serialize(analysisData, new JsonSerializerOptions + { + WriteIndented = true + }); + + var bytes = Encoding.UTF8.GetBytes(json); + var fileName = $"Anonymized_Analysis_{questionnaire.Title}_{DateTime.Now:yyyy-MM-dd}.json"; + + return File(bytes, "application/json", fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting analysis for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error exporting analysis. Please try again."; + return RedirectToAction(nameof(Index)); + } + } + + #endregion + + #region Mental Health Trends + + /// + /// Analyze mental health trends over time periods + /// + public async Task AnalyzeTrends(int id, DateTime? fromDate = null, DateTime? toDate = null) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + // Default to last 6 months if no dates provided + var from = fromDate ?? DateTime.Now.AddMonths(-6); + var to = toDate ?? DateTime.Now; + + var trends = await _aiAnalysisService.AnalyzeMentalHealthTrendsAsync(id, from, to); + + ViewBag.QuestionnaireName = questionnaire.Title; + ViewBag.QuestionnaireId = id; + ViewBag.FromDate = from; + ViewBag.ToDate = to; + + return View(trends); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing trends for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error analyzing trends. Please try again."; + return RedirectToAction(nameof(Index)); + } + } + + #endregion + + #region Service Health and Testing + + /// + /// Check AI service health status + /// + public async Task ServiceHealth() + { + try + { + var healthStatus = await _aiAnalysisService.GetServiceHealthStatusAsync(); + return Json(new + { + success = true, + services = healthStatus, + message = "Service health check completed successfully" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking service health"); + return Json(new + { + success = false, + error = "Unable to check service health", + message = ex.Message + }); + } + } + + /// + /// Test AI analysis with sample text + /// + [HttpPost] + public async Task TestAnalysis(string sampleText) + { + try + { + if (string.IsNullOrWhiteSpace(sampleText)) + { + return Json(new { success = false, message = "Please provide sample text." }); + } + + var analysisRequest = new AnalysisRequest + { + ResponseId = 0, // Test request + QuestionId = 0, // Test request + ResponseText = sampleText, + QuestionText = "Test question: How are you feeling about your work environment?" + }; + + var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); + + return Json(new + { + success = true, + sentiment = analysis.SentimentAnalysis?.Sentiment, + riskLevel = analysis.RiskAssessment?.RiskLevel.ToString(), + keyPhrases = analysis.KeyPhrases?.KeyPhrases, + insights = analysis.Insights.Select(i => i.Category).ToList() + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in test analysis"); + return Json(new + { + success = false, + message = "Error performing test analysis. Please try again." + }); + } + } + + #endregion + + #region Management Dashboard + + /// + /// Executive dashboard for mental health overview + /// + public async Task Dashboard(int id) + { + try + { + var questionnaire = await _context.Questionnaires + .FirstOrDefaultAsync(q => q.Id == id); + + if (questionnaire == null) + { + TempData["ErrorMessage"] = "Questionnaire not found."; + return RedirectToAction(nameof(Index)); + } + + var dashboard = await _aiAnalysisService.GenerateManagementDashboardAsync(id); + + return View(dashboard); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading dashboard for questionnaire {QuestionnaireId}", id); + TempData["ErrorMessage"] = "Error loading dashboard. Please try again."; + return RedirectToAction(nameof(Index)); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/AiAnalysis.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/AiAnalysis.cshtml deleted file mode 100644 index 6e55342..0000000 --- a/Web/Areas/Admin/Views/SurveyAnalysis/AiAnalysis.cshtml +++ /dev/null @@ -1,71 +0,0 @@ -@model IEnumerable - -@{ - ViewData["Title"] = "AI Analysis Results"; -} - -
-
-
-

AI Analysis: @ViewBag.QuestionnaireName

-
-
- - @if (Model.Any()) - { -
- - - - - - - - - - - - - - @foreach (var item in Model) - { - - - - - - - - - - } - -
UserQuestionResponseSentimentRisk LevelKey PhrasesConfidence Scores
@item.UserName@item.Question@item.Response - - @item.Sentiment - - - - @item.RiskAssessment - - - @string.Join(", ", item.KeyPhrases) - - - Pos: @item.PositiveScore.ToString("F2")
- Neg: @item.NegativeScore.ToString("F2")
- Neu: @item.NeutralScore.ToString("F2") -
-
-
- } - else - { -

No text responses found to analyze.

- } -
-
-
\ No newline at end of file diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml deleted file mode 100644 index 7729ce7..0000000 --- a/Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml +++ /dev/null @@ -1,446 +0,0 @@ -@using Newtonsoft.Json -@model ResponseQuestionnaireWithUsersViewModel - -@{ - ViewData["Title"] = "Detailed Survey Analysis"; -} - - - -
- - -
-
- Survey Analyzer -
-
-
@Model.Title
-

@Html.Raw(Model.Description)

- -
- -
- -
- @foreach (var question in Model.Questions) - { -
- - -
- - - @if (question.Type == QuestionType.Image) - { -
- } -
- } -
- -
- -@* @section Scripts { - - -} - *@ - -@section Scripts { - - -} - - -@* @section Scripts { -// -// -// } - - - -@* @section Scripts { - - -} - *@ - - - - -@* @section Scripts { - - -} *@ diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/AnalyzeQuestionnaire.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/AnalyzeQuestionnaire.cshtml new file mode 100644 index 0000000..0469c15 --- /dev/null +++ b/Web/Areas/Admin/Views/SurveyAnalysis/AnalyzeQuestionnaire.cshtml @@ -0,0 +1,504 @@ + +@model Services.AIViewModel.QuestionnaireAnalysisOverview + +@{ + ViewData["Title"] = $"AI Analysis - {Model.QuestionnaireTitle}"; + +} + +
+ +
+
+
+
+ +

+ + AI Analysis Results +

+

Comprehensive mental health analysis powered by Azure AI

+
+ +
+
+
+ + +
+
+
+
+
+ +
+

@Model.TotalResponses

+ Total Responses +
+
+
+
+
+
+
+ +
+

@Model.AnalyzedResponses

+ AI Analyzed +
+
+
+
+
+
+
+ +
+

@Math.Round(Model.OverallPositiveSentiment * 100, 1)%

+ Positive Sentiment +
+
+
+
+
+
+
0 ? "text-warning" : "text-success") mb-2"> + +
+

@(Model.HighRiskResponses + Model.CriticalRiskResponses)

+ High/Critical Risk +
+
+
+
+ + +
+
+
+
+
+ + Mental Health Risk Distribution +
+
+
+ @if (Model.AnalyzedResponses > 0) + { +
+
+ + Low Risk + + @Model.LowRiskResponses +
+
+
+
+
+
+ +
+
+ + Moderate Risk + + @Model.ModerateRiskResponses +
+
+
+
+
+
+ +
+
+ + High Risk + + @Model.HighRiskResponses +
+
+
+
+
+
+ +
+
+ + Critical Risk + + @Model.CriticalRiskResponses +
+
+
+
+
+
+ + @if (Model.HighRiskResponses > 0 || Model.CriticalRiskResponses > 0) + { +
+ + @(Model.HighRiskResponses + Model.CriticalRiskResponses) responses require immediate attention. + View details +
+ } + } + else + { +
+ +

No risk assessment data available

+
+ } +
+
+
+ +
+
+
+
+ + Overall Sentiment Analysis +
+
+
+ @if (Model.AnalyzedResponses > 0) + { +
+
+ + Positive + + @Math.Round(Model.OverallPositiveSentiment * 100, 1)% +
+
+
+
+
+
+ +
+
+ + Neutral + + @Math.Round(Model.OverallNeutralSentiment * 100, 1)% +
+
+
+
+
+
+ +
+
+ + Negative + + @Math.Round(Model.OverallNegativeSentiment * 100, 1)% +
+
+
+
+
+
+ + + + string sentimentStatus = ""; + string sentimentColor = ""; + string sentimentIcon = ""; + + if (Model.OverallPositiveSentiment >= 0.6) + { + sentimentStatus = "Excellent mental health climate"; + sentimentColor = "text-success"; + sentimentIcon = "fa-thumbs-up"; + } + else if (Model.OverallPositiveSentiment >= 0.4) + { + sentimentStatus = "Moderate mental health climate"; + sentimentColor = "text-warning"; + sentimentIcon = "fa-balance-scale"; + } + else + { + sentimentStatus = "Concerning mental health climate"; + sentimentColor = "text-danger"; + sentimentIcon = "fa-exclamation-triangle"; + } + + +
+ + @sentimentStatus +
+ } + else + { +
+ +

No sentiment analysis data available

+
+ } +
+
+
+
+ + + @if (!string.IsNullOrEmpty(Model.ExecutiveSummary)) + { +
+
+
+
+
+ + Executive Summary +
+
+
+
+ @Html.Raw(Model.ExecutiveSummary.Replace("\n", "
")) +
+
+
+
+
+ } + + +
+
+
+
+
+ + Top Workplace Issues & Interventions +
+
+
+ @if (Model.TopWorkplaceIssues != null && Model.TopWorkplaceIssues.Any()) + { + @foreach (var issue in Model.TopWorkplaceIssues.Take(5)) + { +
+
+
@issue.Category
+ + Priority @issue.Priority + +
+

@issue.Issue

+
+ Recommended Intervention: +

@issue.RecommendedIntervention

+
+ @if (issue.AffectedAreas.Any()) + { +
+ @foreach (var area in issue.AffectedAreas) + { + @area + } +
+ } +
+ } + } + else + { +
+ +

No workplace issues identified in the analysis

+
+ } +
+
+
+ +
+
+
+
+ + Common Themes +
+
+
+ @if (Model.MostCommonKeyPhrases != null && Model.MostCommonKeyPhrases.Any()) + { +
+
MOST MENTIONED PHRASES
+ @foreach (var phrase in Model.MostCommonKeyPhrases.Take(8)) + { + @phrase + } +
+ + @if (Model.TopWorkplaceIssues.Any()) + { +
+
ISSUE CATEGORIES
+ @foreach (var category in Model.TopWorkplaceIssues.Select(i => i.Category).Distinct().Take(5)) + { + @category + } +
+ } + } + else + { +
+ +

No common themes identified

+
+ } +
+
+
+
+ + +
+
+
+
+
+
+ + + Analysis completed on @Model.LastAnalyzedAt.ToString("MMMM dd, yyyy 'at' HH:mm") + | @Model.AnalyzedResponses of @Model.TotalResponses responses analyzed + | Powered by Azure AI Services + +
+ +
+
+
+
+
+
+ +@functions { + private string GetPriorityBorderColor(int priority) + { + return priority switch + { + 5 => "border-danger", + 4 => "border-warning", + 3 => "border-primary", + 2 => "border-info", + _ => "border-secondary" + }; + } + + private string GetPriorityBadgeColor(int priority) + { + return priority switch + { + 5 => "bg-danger", + 4 => "bg-warning text-dark", + 3 => "bg-primary", + 2 => "bg-info", + _ => "bg-secondary" + }; + } +} + +@section Styles { + +} \ No newline at end of file diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/HighRiskResponses.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/HighRiskResponses.cshtml new file mode 100644 index 0000000..2b780e7 --- /dev/null +++ b/Web/Areas/Admin/Views/SurveyAnalysis/HighRiskResponses.cshtml @@ -0,0 +1,492 @@ +@* Views/Admin/SurveyAnalysis/HighRiskResponses.cshtml *@ +@model List + +@{ + ViewData["Title"] = $"High Risk Responses - {ViewBag.QuestionnaireName}"; + +} + +
+ +
+
+
+
+ +

+ + High Risk Mental Health Cases +

+

Employees requiring immediate attention and intervention

+
+ +
+
+
+ + + @if (Model != null && Model.Any()) + { + var criticalCount = Model.Count(r => r.RiskAssessment?.RiskLevel == Services.AIViewModel.RiskLevel.Critical); + var highCount = Model.Count(r => r.RiskAssessment?.RiskLevel == Services.AIViewModel.RiskLevel.High); + var immediateAttentionCount = Model.Count(r => r.RiskAssessment?.RequiresImmediateAttention == true); + +
+
+
+
+
+
+ + Mental Health Alert: @Model.Count Cases Requiring Attention +
+

+ @if (criticalCount > 0) + { + @criticalCount Critical + } + @if (highCount > 0) + { + @highCount High Risk + } + @if (immediateAttentionCount > 0) + { + @immediateAttentionCount Immediate Attention + } + | Professional intervention recommended +

+
+
+ +
+
+
+
+
+ } + + + @if (Model != null && Model.Any()) + { +
+ @foreach (var response in Model.OrderByDescending(r => r.RiskAssessment?.RiskScore ?? 0)) + { + var riskLevel = response.RiskAssessment?.RiskLevel ?? Services.AIViewModel.RiskLevel.Low; + var riskScore = response.RiskAssessment?.RiskScore ?? 0; + var requiresAttention = response.RiskAssessment?.RequiresImmediateAttention ?? false; + +
+
+ +
+
+
+
+ + @riskLevel Risk Level +
+ Response ID: #@response.ResponseId +
+
+
+ + Risk Score: @Math.Round(riskScore * 100, 0)% + +
+ @if (requiresAttention) + { +
+ + Immediate + +
+ } +
+
+
+ + +
+ +
+
+ Question Context +
+

@response.QuestionText

+
+ + +
+
+ Response (Privacy Protected) +
+
+

@(response.AnonymizedResponseText?.Length > 150 ? response.AnonymizedResponseText.Substring(0, 150) + "..." : response.AnonymizedResponseText)

+
+
+ + + @if (response.RiskAssessment?.RiskIndicators?.Any() == true) + { +
+
+ Risk Indicators +
+
+ @foreach (var indicator in response.RiskAssessment.RiskIndicators.Take(3)) + { + @indicator + } +
+
+ } + + + @if (!string.IsNullOrEmpty(response.RiskAssessment?.RecommendedAction)) + { +
+
+ Recommended Action +
+
+

@response.RiskAssessment.RecommendedAction

+
+
+ } + + + @if (response.Insights?.Any() == true) + { +
+
+ Key Insights +
+ @foreach (var insight in response.Insights.Take(2)) + { +
+ @insight.Category +

@insight.RecommendedIntervention

+
+ } +
+ } + + + @if (response.RiskAssessment?.ProtectiveFactors?.Any() == true) + { +
+
+ Protective Factors +
+
+ @foreach (var factor in response.RiskAssessment.ProtectiveFactors.Take(3)) + { + @factor + } +
+
+ } +
+ + + +
+
+ } +
+ + +
+
+
+
+
+ + Mental Health Professional Actions +
+
+
+
+
+
Immediate Actions
+
    +
  • Contact employees with Critical/High risk levels
  • +
  • Schedule follow-up conversations
  • +
  • Refer to mental health professionals if needed
  • +
+
+
+
Documentation
+
    +
  • Document interventions taken
  • +
  • Track response to interventions
  • +
  • Update risk assessments as needed
  • +
+
+
+
Organizational
+
    +
  • Alert management to workplace issues
  • +
  • Implement preventive measures
  • +
  • Schedule team interventions
  • +
+
+
+
+
+ + + + View Trends + +
+
+
+
+
+ } + else + { + +
+
+
+
+
+ +
+

Excellent Mental Health Status

+

+ No high-risk or critical mental health cases were identified in this survey analysis. + This indicates a generally positive workplace mental health environment. +

+ +
+
+
+
+ } +
+ +@functions { + private string GetRiskHeaderClass(Services.AIViewModel.RiskLevel riskLevel) + { + switch (riskLevel) + { + case Services.AIViewModel.RiskLevel.Critical: + return "bg-dark"; + case Services.AIViewModel.RiskLevel.High: + return "bg-danger"; + case Services.AIViewModel.RiskLevel.Moderate: + return "bg-warning"; + default: + return "bg-secondary"; + } + } + + private string GetRiskIcon(Services.AIViewModel.RiskLevel riskLevel) + { + switch (riskLevel) + { + case Services.AIViewModel.RiskLevel.Critical: + return "fa-exclamation-triangle"; + case Services.AIViewModel.RiskLevel.High: + return "fa-shield-alt"; + case Services.AIViewModel.RiskLevel.Moderate: + return "fa-info-circle"; + default: + return "fa-check-circle"; + } + } +} + +@section Scripts { + +} + +@section Styles { + +} \ No newline at end of file diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml index 189b876..2a93010 100644 --- a/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml +++ b/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml @@ -1,64 +1,446 @@ -@model IEnumerable +@* Views/Admin/SurveyAnalysis/Index.cshtml *@ +@model IEnumerable @{ - ViewData["Title"] = "Survey Analysis"; + ViewData["Title"] = "Mental Health Survey Analysis - Dashboard"; + } -
- +
+ +
+
+
+
+

+ + Mental Health Survey Analysis +

+

AI-powered analysis of workplace mental health surveys

+
+
+ + NVKN Nærværskonsulenterne + +
+
+
+
+ + @if (ViewBag.ServiceHealth != null) + { + var serviceHealth = ViewBag.ServiceHealth as Dictionary; +
+
+
+
+
+
+
+ AI Services Status +
+
+
+
+ @foreach (var service in serviceHealth) + { +
+ + + + @service.Key.Replace("Azure", "") +
+ } +
+
+
+
+
+
+
+ } -
-
Survey analysis
-
-

Survey analysis list

+ + @if (TempData["ErrorMessage"] != null) + { + + } + @if (TempData["SuccessMessage"] != null) + { + + } -
+ @if (TempData["WarningMessage"] != null) + { + + } - - - - - - - - - - - @foreach (var item in Model) - { - - - - + +
+
+
+
+
+
+
+ Quick Actions +
+ + +
+
+ + Last updated: @DateTime.Now.ToString("yyyy-MM-dd HH:mm") + +
+
+
+
+
+
- - - } - -
IdQuestionnaireAction
@item.Id@item.Title - Analyzer - AI Analysis -
+ +
+ @if (Model != null && Model.Any()) + { + @foreach (var questionnaire in Model) + { +
+
+
+
+
+
@questionnaire.Title
+ @questionnaire.Description +
+ +
+
- - +
+ +
+
+
+
@questionnaire.QuestionCount
+ Questions +
+
+
+
+
@questionnaire.ResponseCount
+ Responses +
+
+
+
@questionnaire.TextResponseCount
+ Text Answers +
+
+ + @if (questionnaire.LastResponse != null && questionnaire.LastResponse != DateTime.MinValue) + { +
+ + + Last response: @((DateTime)questionnaire.LastResponse).ToString("MMM dd, yyyy") + +
+ } + + + @if (questionnaire.TextResponseCount > 0) + { +
+ + Ready for Analysis + +
+ } + else + { +
+ + No Text Responses + +
+ } +
+ + +
+
+ } + } + else + { +
+
+
+ +
No Questionnaires Found
+

There are no questionnaires available for analysis at the moment.

+ + Create New Questionnaire + +
+
+
+ } +
+
+ + + - - @section Scripts { + function getRiskColor(riskLevel) { + switch(riskLevel?.toLowerCase()) { + case 'low': return 'success'; + case 'moderate': return 'warning'; + case 'high': return 'danger'; + case 'critical': return 'dark'; + default: return 'secondary'; + } + } + + // Auto-refresh service health every 5 minutes + setInterval(function() { + location.reload(); + }, 300000); + } + +@section Styles { + +} \ No newline at end of file diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/ViewHighRiskResponse.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/ViewHighRiskResponse.cshtml new file mode 100644 index 0000000..5b92967 --- /dev/null +++ b/Web/Areas/Admin/Views/SurveyAnalysis/ViewHighRiskResponse.cshtml @@ -0,0 +1,699 @@ + +@model List + +@{ + var response = ViewBag.Response as Model.Response; + ViewData["Title"] = $"High Risk Case Details - Response #{response?.Id}"; + +} + +
+ +
+
+
+
+ +

+ + High Risk Case Analysis +

+

Detailed mental health assessment requiring professional intervention

+
+
+
+ + + +
+
+
+
+
+ + + @if (Model.Any(r => r.RiskAssessment?.RequiresImmediateAttention == true)) + { +
+
+
+
+
+

+ + IMMEDIATE ATTENTION REQUIRED +

+

This employee's responses indicate significant mental health concerns that require urgent professional intervention.

+
+ HIGH PRIORITY + PROFESSIONAL INTERVENTION + CONFIDENTIAL +
+
+
+ +
+
+
+
+
+ } + + + @if (response != null) + { +
+
+
+
+
+ + Employee Response Overview +
+
+
+
+
+ Response ID:
+ #@response.Id +
+
+ Submission Date:
+ @response.SubmissionDate.ToString("MMMM dd, yyyy") +
+
+ Survey:
+ @response.Questionnaire?.Title +
+
+ +
+ + Privacy Notice: All personal information has been anonymized to protect employee privacy while enabling professional mental health assessment. +
+
+
+
+ +
+
+
+
+ + Action Checklist +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ } + + + @if (Model != null && Model.Any()) + { +
+ @foreach (var analysis in Model.OrderByDescending(a => a.RiskAssessment?.RiskScore ?? 0)) + { + var riskLevel = analysis.RiskAssessment?.RiskLevel ?? Services.AIViewModel.RiskLevel.Low; + var riskScore = analysis.RiskAssessment?.RiskScore ?? 0; + +
+
+ +
+
+
+
+ + Question Analysis +
+

@analysis.QuestionText

+
+
+
+ + + @riskLevel Risk + + + @Math.Round(riskScore * 100, 0)% Risk Score + +
+
+
+
+ +
+
+ +
+
+ Employee Response (Anonymized) +
+
+

@analysis.AnonymizedResponseText

+
+ + + @if (analysis.SentimentAnalysis != null) + { +
+
+ Emotional Sentiment +
+
+
+ Positive + @Math.Round(analysis.SentimentAnalysis.PositiveScore * 100, 1)% +
+
+
+
+ +
+ Neutral + @Math.Round(analysis.SentimentAnalysis.NeutralScore * 100, 1)% +
+
+
+
+ +
+ Negative + @Math.Round(analysis.SentimentAnalysis.NegativeScore * 100, 1)% +
+
+
+
+
+
+ } +
+ + +
+ + @if (analysis.RiskAssessment != null) + { +
+
+ Mental Health Risk Assessment +
+ + + @if (analysis.RiskAssessment.RiskIndicators?.Any() == true) + { +
+ RISK INDICATORS: +
+ @foreach (var indicator in analysis.RiskAssessment.RiskIndicators) + { + + @indicator + + } +
+
+ } + + + @if (!string.IsNullOrEmpty(analysis.RiskAssessment.RecommendedAction)) + { +
+ RECOMMENDED ACTION: +
+

@analysis.RiskAssessment.RecommendedAction

+
+
+ } + + + @if (analysis.RiskAssessment.ProtectiveFactors?.Any() == true) + { +
+ PROTECTIVE FACTORS: +
+ @foreach (var factor in analysis.RiskAssessment.ProtectiveFactors) + { + + @factor + + } +
+
+ } +
+ } + + + @if (analysis.KeyPhrases?.KeyPhrases?.Any() == true) + { +
+
+ Key Phrases Identified +
+
+ @foreach (var phrase in analysis.KeyPhrases.KeyPhrases.Take(6)) + { + @phrase + } +
+
+ } +
+
+
+ + + @if (analysis.Insights?.Any() == true) + { + + } +
+
+ } +
+ } + + +
+
+
+
+
+ + Mental Health Professional Action Plan +
+
+
+
+
+
+ Immediate Actions (Next 24 Hours) +
+
    +
  • + + Contact employee for confidential check-in +
  • +
  • + + Assess immediate safety and support needs +
  • +
  • + + Provide mental health resources and contacts +
  • +
  • + + Document initial intervention in confidential records +
  • +
+
+
+
+ Follow-up Actions (Next 7 Days) +
+
    +
  • + + Schedule follow-up conversation +
  • +
  • + + Review workplace factors with management +
  • +
  • + + Implement recommended workplace interventions +
  • +
  • + + Assess progress and adjust support plan +
  • +
+
+
+ +
+ +
+
+ + + + +
+
+
+
+
+
+
+ +@functions { + private string GetRiskHeaderClass(Services.AIViewModel.RiskLevel riskLevel) + { + switch (riskLevel) + { + case Services.AIViewModel.RiskLevel.Critical: + return "bg-dark"; + case Services.AIViewModel.RiskLevel.High: + return "bg-danger"; + case Services.AIViewModel.RiskLevel.Moderate: + return "bg-warning"; + default: + return "bg-secondary"; + } + } + + private string GetRiskIcon(Services.AIViewModel.RiskLevel riskLevel) + { + switch (riskLevel) + { + case Services.AIViewModel.RiskLevel.Critical: + return "fa-exclamation-triangle"; + case Services.AIViewModel.RiskLevel.High: + return "fa-shield-alt"; + case Services.AIViewModel.RiskLevel.Moderate: + return "fa-info-circle"; + default: + return "fa-check-circle"; + } + } + + private string GetPriorityBorderClass(int priority) + { + switch (priority) + { + case 5: + return "border-danger"; + case 4: + return "border-warning"; + case 3: + return "border-primary"; + case 2: + return "border-info"; + default: + return "border-secondary"; + } + } + + private string GetPriorityBadgeClass(int priority) + { + switch (priority) + { + case 5: + return "bg-danger"; + case 4: + return "bg-warning text-dark"; + case 3: + return "bg-primary"; + case 2: + return "bg-info"; + default: + return "bg-secondary"; + } + } +} + +@section Scripts { + +} + +@section Styles { + +} \ No newline at end of file diff --git a/Web/Extesions/ServicesExtesions.cs b/Web/Extesions/ServicesExtesions.cs index 7afb040..2224143 100644 --- a/Web/Extesions/ServicesExtesions.cs +++ b/Web/Extesions/ServicesExtesions.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Model; -using OpenAI_API; using Services.Implemnetation; using Services.Interaces; using Web.AIConfiguration; @@ -128,7 +127,7 @@ namespace Web.Extesions { services.Configure(configuration.GetSection("OpenAI")); - services.AddSingleton(); + diff --git a/Web/Web.csproj b/Web/Web.csproj index bcb22c3..5f6a874 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -24,7 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - +