post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={$apiKey}", [ 'contents' => [ [ 'parts' => [ ['text' => $prompt . "\n\nMeeting Content:\n" . $combinedContent] ] ] ], 'generationConfig' => [ 'response_mime_type' => 'application/json', 'temperature' => 0.2 ] ]); if ($response->successful()) { $json = $response->json(); $text = $json['candidates'][0]['content']['parts'][0]['text'] ?? ''; $data = json_decode($text, true); if (is_array($data) && isset($data['mom_content'])) { return $this->formatMoMData($data, $projectName, $clientName); } } } catch (\Throwable $e) { Log::error('AI MoM Generation Error: ' . $e->getMessage()); } } // Fallback Intelligent Extraction Engine return $this->generateFallbackMoM($combinedContent, $transcript, $liveNotes, $projectName, $clientName); } /** * Real-time Emotion / Expression Detection for live client video calls. * * @param string $speechText * @param string|null $imageFrameBase64 * @return array */ public function detectEmotion(?string $speechText = '', ?string $imageFrameBase64 = null): array { $apiKey = env('GEMINI_API_KEY') ?: (env('GOOGLE_API_KEY') ?: (getenv('GEMINI_API_KEY') ?: getenv('GOOGLE_API_KEY'))); $lowerText = strtolower(trim($speechText ?? '')); // 1. Check for negative polarity / negations that invert positive sentiment $negatedHappyPhrases = [ 'not happy', 'not satisfied', 'not good', 'not great', 'not working', 'not impressed', 'not pleased', 'never works', 'no progress', 'displeased', 'unhappy', 'dissatisfied', 'don\'t like', 'do not like', 'cannot accept', 'unacceptable', 'waste of time', 'waste of money', 'broken feature', 'delays' ]; $foundNegations = []; foreach ($negatedHappyPhrases as $negPhrase) { if (preg_match('/\b' . preg_quote($negPhrase, '/') . '\b/i', $lowerText)) { $foundNegations[] = $negPhrase; } } // 2. Angry / Frustrated trigger keywords $angryTriggers = [ 'angry', 'furious', 'unacceptable', 'ridiculous', 'terrible', 'waste of time', 'delay', 'delays', 'delayed', 'broken', 'disaster', 'unhappy', 'frustrated', 'mad', 'annoyed', 'horrible', 'cancel', 'lawyer', 'refund', 'fail', 'failed', 'incompetent', 'worst', 'escalate', 'escalation', 'disgusted', 'blunder', 'mess', 'screwed', 'unprofessional', 'missed deadline', 'not delivering' ]; // 3. Sad / Concerned / Hesitant trigger keywords $sadTriggers = [ 'sad', 'disappointed', 'disappointment', 'worried', 'worry', 'concerned', 'concern', 'nervous', 'scared', 'afraid', 'regret', 'loss', 'hopeless', 'sorry', 'doubt', 'doubtful', 'hesitant', 'struggling', 'anxious', 'over budget', 'falling behind', 'uncertain', 'at risk' ]; // 4. Happy / Satisfied / Praising trigger keywords $happyTriggers = [ 'great', 'excellent', 'fantastic', 'awesome', 'good job', 'love it', 'happy', 'pleased', 'perfect', 'superb', 'wonderful', 'thank you', 'amazing', 'impressed', 'kudos', 'brilliant', 'exceeded expectations', 'looks good', 'looks great', 'well done', 'appreciate', 'delighted', 'seamless', 'flawless', 'outstanding' ]; $detectedAngryWords = []; foreach ($angryTriggers as $word) { if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText)) { $detectedAngryWords[] = $word; } } if (!empty($foundNegations)) { $detectedAngryWords = array_unique(array_merge($detectedAngryWords, $foundNegations)); } $detectedSadWords = []; foreach ($sadTriggers as $word) { if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText)) { $detectedSadWords[] = $word; } } $detectedHappyWords = []; // Only consider happy words if there are no strong negations of happiness if (empty($foundNegations)) { foreach ($happyTriggers as $word) { if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText)) { $detectedHappyWords[] = $word; } } } if (!empty($detectedAngryWords)) { return [ 'emotion' => 'angry', 'confidence' => 0.94, 'trigger_words' => array_values(array_unique($detectedAngryWords)), 'details' => 'Detected clear frustration, complaint, or dissatisfaction cues in conversation.', 'label' => 'Frustrated / Angry 😡' ]; } if (!empty($detectedSadWords)) { return [ 'emotion' => 'sad', 'confidence' => 0.88, 'trigger_words' => array_values(array_unique($detectedSadWords)), 'details' => 'Detected concern, hesitation, or worry in speech dialogue.', 'label' => 'Concerned / Sad 😟' ]; } if (!empty($detectedHappyWords)) { return [ 'emotion' => 'happy', 'confidence' => 0.90, 'trigger_words' => array_values(array_unique($detectedHappyWords)), 'details' => 'Detected positive satisfaction, appreciation, or praise in speech.', 'label' => 'Happy / Satisfied 😊' ]; } // If vision image frame is provided and Gemini API key is available if ($apiKey && $imageFrameBase64 && str_starts_with($imageFrameBase64, 'data:image')) { try { $parts = explode(',', $imageFrameBase64); $base64Data = $parts[1] ?? ''; $mimeType = 'image/jpeg'; if (preg_match('/data:(image\/[a-zA-Z]+);base64/', $parts[0], $matches)) { $mimeType = $matches[1]; } $response = Http::timeout(5)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={$apiKey}", [ 'contents' => [ [ 'parts' => [ [ 'text' => 'Analyze the facial expression of the client in this video frame during a business call. Classify emotion strictly as one of: ["happy", "neutral", "sad", "angry"]. Return ONLY valid JSON: {"emotion": "happy"|"neutral"|"sad"|"angry", "confidence": number, "details": string}' ], [ 'inline_data' => [ 'mime_type' => $mimeType, 'data' => $base64Data ] ] ] ] ], 'generationConfig' => [ 'response_mime_type' => 'application/json', 'temperature' => 0.1 ] ]); if ($response->successful()) { $json = $response->json(); $text = $json['candidates'][0]['content']['parts'][0]['text'] ?? ''; $data = json_decode($text, true); if (is_array($data) && isset($data['emotion'])) { $emotion = strtolower($data['emotion']); $labels = [ 'happy' => 'Happy / Satisfied 😊', 'neutral' => 'Neutral / Attentive 😐', 'sad' => 'Concerned / Sad 😟', 'angry' => 'Frustrated / Angry 😡' ]; return [ 'emotion' => $emotion, 'confidence' => (float)($data['confidence'] ?? 0.85), 'trigger_words' => [], 'details' => $data['details'] ?? 'AI facial expression detection completed.', 'label' => $labels[$emotion] ?? 'Neutral / Attentive 😐' ]; } } } catch (\Throwable $e) { Log::debug('AI Vision Emotion Error: ' . $e->getMessage()); } } return [ 'emotion' => 'neutral', 'confidence' => 0.80, 'trigger_words' => [], 'details' => 'Client expression is calm and professional.', 'label' => 'Neutral / Professional 😐' ]; } /** * Dispatch MoM & Notes via email to TL, Director, PM, and Admin. * * @param ClientCallNote $callNote * @return array */ public function sendMoMEmails(ClientCallNote $callNote): array { $client = $callNote->client; $meeting = $callNote->meeting; $recipientEmails = []; // 1. Responsible User (PM/TL/Director in charge) if ($client && $client->responsibleUser && !empty($client->responsibleUser->email)) { $recipientEmails[$client->responsibleUser->email] = $client->responsibleUser->name; } // 2. Creator if ($client && $client->creator && !empty($client->creator->email)) { $recipientEmails[$client->creator->email] = $client->creator->name; } // 3. Meeting Host if ($meeting && $meeting->host && !empty($meeting->host->email)) { $recipientEmails[$meeting->host->email] = $meeting->host->name; } // 4. Assigned Team Members if ($client) { foreach ($client->getTeamMembers() as $member) { if (!empty($member->email)) { $recipientEmails[$member->email] = $member->name; } } } // 5. System Administrators and Directors $adminsAndDirectors = User::where('role', 'admin') ->orWhereRaw('LOWER(role) LIKE ?', ['%director%']) ->orWhereRaw('LOWER(role) LIKE ?', ['%pm%']) ->orWhereRaw('LOWER(role) LIKE ?', ['%lead%']) ->get(); foreach ($adminsAndDirectors as $user) { if (!empty($user->email)) { $recipientEmails[$user->email] = $user->name; } } // 6. Client emails if invited if ($meeting && !empty($meeting->client_emails)) { foreach ($meeting->client_emails as $email) { if (filter_var($email, FILTER_VALIDATE_EMAIL)) { $recipientEmails[$email] = $client->client_name ?? 'Client'; } } } $sentCount = 0; $sentAddresses = []; foreach ($recipientEmails as $email => $name) { try { Mail::to($email)->send(new ClientCallMomMail($callNote, $name)); $sentAddresses[] = $email; $sentCount++; } catch (\Throwable $e) { Log::error("Failed to send MoM email to {$email}: " . $e->getMessage()); } } $callNote->update([ 'emailed_at' => now(), 'emailed_to' => $sentAddresses, ]); return [ 'success' => true, 'sent_count' => $sentCount, 'recipients' => $sentAddresses, ]; } /** * Format MoM into clean, human-readable plain text for .txt download. * * @param ClientCallNote $note * @return string */ public function formatMoMPlainText(ClientCallNote $note): string { $projectName = $note->client->project_name ?? 'Client Project'; $clientName = $note->client->client_name ?? 'Client'; $company = $note->client->company_name ? " ({$note->client->company_name})" : ""; $meetingTitle = $note->meeting->title ?? 'Client Call'; $date = $note->created_at ? $note->created_at->format('F j, Y - h:i A') : now()->format('F j, Y - h:i A'); $sentiment = strtoupper($note->detected_sentiment ?? 'NEUTRAL'); $out = "================================================================================\n"; $out .= " SINGLELOGIN AI MEETING INTELLIGENCE - MINUTES OF MEETING (MoM)\n"; $out .= "================================================================================\n\n"; $out .= "PROJECT: {$projectName}\n"; $out .= "CLIENT: {$clientName}{$company}\n"; $out .= "MEETING TITLE: {$meetingTitle}\n"; $out .= "DATE & TIME: {$date}\n"; $out .= "CLIENT MOOD: {$sentiment}\n"; $out .= "NOTE HOST: " . ($note->creator->name ?? 'SingleLogin Host') . "\n"; $out .= "--------------------------------------------------------------------------------\n\n"; $out .= "1. EXECUTIVE SUMMARY\n"; $out .= "--------------------\n"; $out .= ($note->summary ?? 'Meeting completed successfully.') . "\n\n"; if (!empty($note->action_items) && count($note->action_items) > 0) { $out .= "2. ACTION ITEMS & DELIVERABLES\n"; $out .= "------------------------------\n"; foreach ($note->action_items as $idx => $act) { $num = $idx + 1; $task = is_array($act) ? ($act['task'] ?? json_encode($act)) : $act; $owner = is_array($act) ? ($act['owner'] ?? 'Team') : 'Team'; $deadline = is_array($act) ? ($act['deadline'] ?? 'TBD') : 'TBD'; $status = is_array($act) ? ($act['status'] ?? 'Pending') : 'Pending'; $out .= " [ ] {$num}. {$task}\n"; $out .= " Owner: {$owner} | Deadline: {$deadline} | Status: {$status}\n\n"; } } if (!empty($note->key_decisions) && count($note->key_decisions) > 0) { $out .= "3. KEY DECISIONS MADE\n"; $out .= "---------------------\n"; foreach ($note->key_decisions as $idx => $dec) { $num = $idx + 1; $decision = is_array($dec) ? ($dec['decision'] ?? json_encode($dec)) : $dec; $out .= " * {$decision}\n"; } $out .= "\n"; } $out .= "4. COMPLETE MINUTES OF MEETING CONTENT\n"; $out .= "--------------------------------------\n"; $out .= ($note->mom_content ?? 'No content.') . "\n\n"; $out .= "================================================================================\n"; $out .= " Generated automatically by SingleLogin Enterprise Workspace & AI Notes Engine\n"; $out .= "================================================================================\n"; return $out; } /** * Format To-Do List into clean plain text for .txt download. * * @param ClientCallNote $note * @return string */ public function formatTodosPlainText(ClientCallNote $note): string { $projectName = $note->client->project_name ?? 'Client Project'; $clientName = $note->client->client_name ?? 'Client'; $meetingTitle = $note->meeting->title ?? 'Client Call'; $date = $note->created_at ? $note->created_at->format('F j, Y') : now()->format('F j, Y'); $out = "================================================================================\n"; $out .= " SINGLELOGIN - ACTIONABLE TO-DO LIST & TASK DELIVERABLES\n"; $out .= "================================================================================\n"; $out .= "Project: {$projectName}\n"; $out .= "Client: {$clientName}\n"; $out .= "Meeting: {$meetingTitle}\n"; $out .= "Date: {$date}\n"; $out .= "================================================================================\n\n"; $todos = $note->todo_items ?? []; if (empty($todos) && !empty($note->action_items)) { $todos = $note->action_items; } if (empty($todos)) { $out .= "No specific actionable tasks were logged for this call.\n"; } else { foreach ($todos as $idx => $item) { $num = $idx + 1; $task = is_array($item) ? ($item['task'] ?? json_encode($item)) : $item; $owner = is_array($item) ? ($item['owner'] ?? 'Assigned Lead') : 'Assigned Lead'; $priority = is_array($item) ? ($item['priority'] ?? 'Medium') : 'Medium'; $deadline = is_array($item) ? ($item['deadline'] ?? 'TBD') : 'TBD'; $check = (is_array($item) && ($item['completed'] ?? false)) ? "[X]" : "[ ]"; $out .= "{$check} {$num}. {$task}\n"; $out .= " Assignee: {$owner}\n"; $out .= " Priority: {$priority}\n"; $out .= " Due Date: {$deadline}\n\n"; } } $out .= "--------------------------------------------------------------------------------\n"; $out .= "Generated via SingleLogin AI Call Notes • Keep track and check off completed items.\n"; $out .= "================================================================================\n"; return $out; } /** * Format MoM data to ensure all keys and structures exist. */ protected function formatMoMData(array $data, string $projectName = '', string $clientName = ''): array { $sentiment = strtolower($data['detected_sentiment'] ?? 'neutral'); if (!in_array($sentiment, ['happy', 'neutral', 'sad', 'angry'])) { $sentiment = 'neutral'; } $angryAnalysis = $data['angry_analysis'] ?? []; if ($sentiment === 'angry' || (!empty($angryAnalysis) && ($angryAnalysis['is_angry'] ?? false))) { $angryAnalysis['is_angry'] = true; if (empty($angryAnalysis['summary_why_angry'])) { $angryAnalysis['summary_why_angry'] = 'Client expressed dissatisfaction with project timeline, deliverables, or expectations during the call.'; } } else { $angryAnalysis = [ 'is_angry' => false, 'trigger_words' => [], 'summary_why_angry' => '', 'grievances' => [], 'suggested_remedy' => '', ]; } // Format emotion_details $emotionDetails = $data['emotion_details'] ?? []; if (empty($emotionDetails) || !isset($emotionDetails['bullet_points'])) { $emotionDetails = $this->buildFallbackEmotionDetails($sentiment, $projectName, $clientName, $data['summary'] ?? ''); } // Format todo_items $todoItems = $data['todo_items'] ?? []; if (empty($todoItems) && !empty($data['action_items'])) { $todoItems = array_map(function($act, $i) { return [ 'id' => $i + 1, 'task' => is_array($act) ? ($act['task'] ?? '') : (string)$act, 'owner' => is_array($act) ? ($act['owner'] ?? 'Team') : 'Team', 'priority' => is_array($act) ? ($act['priority'] ?? 'Medium') : 'Medium', 'deadline' => is_array($act) ? ($act['deadline'] ?? now()->addDays(3)->format('M d, Y')) : now()->addDays(3)->format('M d, Y'), 'completed' => false, ]; }, $data['action_items'], array_keys($data['action_items'])); } return [ 'summary' => $data['summary'] ?? 'Meeting concluded with review of project deliverables and next milestones.', 'mom_content' => $data['mom_content'] ?? '', 'action_items' => is_array($data['action_items'] ?? null) ? $data['action_items'] : [], 'todo_items' => is_array($todoItems) ? $todoItems : [], 'key_decisions' => is_array($data['key_decisions'] ?? null) ? $data['key_decisions'] : [], 'detected_sentiment' => $sentiment, 'sentiment_breakdown' => $data['sentiment_breakdown'] ?? [ 'happy_pct' => $sentiment === 'happy' ? 80 : 10, 'neutral_pct' => $sentiment === 'neutral' ? 80 : 10, 'sad_pct' => $sentiment === 'sad' ? 70 : 5, 'angry_pct' => $sentiment === 'angry' ? 85 : 5, ], 'angry_analysis' => $angryAnalysis, 'emotion_details' => $emotionDetails, ]; } /** * Fallback MoM extraction when external LLM API is unavailable. */ protected function generateFallbackMoM(string $combinedContent, string $transcript, string $liveNotes, string $projectName, string $clientName): array { $lower = strtolower($combinedContent); $negatedHappyPhrases = ['not happy', 'not satisfied', 'not good', 'not great', 'not working', 'not impressed', 'not pleased', 'no progress', 'displeased', 'unhappy', 'dissatisfied', 'cannot accept', 'unacceptable']; $foundNegations = array_values(array_filter($negatedHappyPhrases, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower))); $angryWords = ['angry', 'furious', 'unacceptable', 'ridiculous', 'terrible', 'waste of time', 'delay', 'delays', 'delayed', 'broken', 'disaster', 'unhappy', 'frustrated', 'mad', 'annoyed', 'horrible', 'refund', 'cancel', 'fail', 'incompetent', 'unprofessional', 'missed deadline']; $sadWords = ['sad', 'disappointed', 'worried', 'concerned', 'nervous', 'scared', 'afraid', 'regret', 'doubt', 'hesitant', 'uncertain', 'at risk']; $happyWords = ['great', 'excellent', 'fantastic', 'awesome', 'good job', 'love it', 'happy', 'pleased', 'perfect', 'superb', 'wonderful', 'thank you', 'appreciate', 'impressed', 'kudos', 'brilliant', 'delighted']; $foundAngry = array_values(array_unique(array_merge( array_filter($angryWords, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower)), $foundNegations ))); $foundSad = array_values(array_filter($sadWords, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower))); $foundHappy = []; if (empty($foundNegations)) { $foundHappy = array_values(array_filter($happyWords, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower))); } $sentiment = 'neutral'; $angryAnalysis = [ 'is_angry' => false, 'trigger_words' => [], 'summary_why_angry' => '', 'grievances' => [], 'suggested_remedy' => '', ]; if (count($foundAngry) > 0) { $sentiment = 'angry'; $angryAnalysis = [ 'is_angry' => true, 'trigger_words' => $foundAngry, 'summary_why_angry' => "During the conversation, the client expressed clear frustration regarding project execution and deliverables. Keywords identified: \"" . implode('", "', $foundAngry) . "\". Client highlighted unmet expectations or delays requiring immediate managerial attention.", 'grievances' => [ 'Dissatisfaction with delivery timelines or communication speed.', 'Concerns regarding quality or specific functional requirements.', ], 'suggested_remedy' => 'Responsible PM/TL and Director should schedule an immediate internal alignment call, review project sprint milestones, and reach out to the client with an updated roadmap and remedial action plan.', ]; } elseif (count($foundSad) > 0) { $sentiment = 'sad'; } elseif (count($foundHappy) > 0) { $sentiment = 'happy'; } $summary = "Strategic sync call held between project team and {$clientName} regarding {$projectName}. Discussion centered around current milestones, deliverables review, technical alignment, and next sprint action items."; $mom = "### Minutes of Meeting (MoM)\n" . "**Project:** " . ($projectName ?: 'Client Project') . "\n" . "**Client:** " . ($clientName ?: 'Client Partner') . "\n" . "**Date:** " . now()->format('F j, Y - h:i A') . "\n\n" . "#### 1. Meeting Objectives\n" . "- Review project status, milestone progress, and client feedback.\n" . "- Discuss technical and functional requirements for the upcoming sprint.\n\n" . "#### 2. Key Discussion Points\n" . "- **Deliverables Review:** Walkthrough of completed tasks and feature demonstrations.\n" . "- **Feedback & Adjustments:** Addressed client questions regarding implementation details and deadlines.\n" . (!empty($liveNotes) ? "- **Meeting Notes Taken:** " . $liveNotes . "\n" : "") . "\n#### 3. Challenges & Resolution Items\n" . ($sentiment === 'angry' ? "- Client voiced strong concerns regarding deliverables. Immediate escalation and proactive resolution underway.\n" : "- Standard development alignment and timeline management.\n") . "\n#### 4. Agreed Next Steps\n" . "- Engineering team to incorporate feedback items into next release.\n" . "- PM/TL to share sprint summary and confirm milestone delivery date."; $actionItems = [ [ 'task' => 'Follow up on feedback points discussed in client meeting', 'owner' => 'Project Manager & Tech Lead', 'deadline' => now()->addDays(2)->format('M d, Y'), 'status' => 'Pending', ], [ 'task' => 'Share updated sprint timeline and milestone deliverable checklist', 'owner' => 'Tech Lead', 'deadline' => now()->addDays(3)->format('M d, Y'), 'status' => 'Pending', ] ]; $todoItems = [ [ 'id' => 1, 'task' => 'Deploy latest sprint milestone updates to staging environment', 'owner' => 'Engineering Team', 'priority' => 'High', 'deadline' => now()->addDays(2)->format('M d, Y'), 'completed' => false, ], [ 'id' => 2, 'task' => 'Provide updated API contracts and sandbox test credentials', 'owner' => 'Tech Lead', 'priority' => 'Medium', 'deadline' => now()->addDays(3)->format('M d, Y'), 'completed' => false, ], [ 'id' => 3, 'task' => 'Follow up on client feedback and confirm next review date', 'owner' => 'Project Manager', 'priority' => 'Medium', 'deadline' => now()->addDays(4)->format('M d, Y'), 'completed' => false, ] ]; $keyDecisions = [ ['decision' => 'Approved latest feature progress and agreed on milestone deliverables for next cycle.'], ['decision' => 'Scheduled regular check-in review to maintain transparency.'] ]; $emotionDetails = $this->buildFallbackEmotionDetails($sentiment, $projectName, $clientName, $summary, $foundHappy, $foundSad, $foundAngry); return [ 'summary' => $summary, 'mom_content' => $mom, 'action_items' => $actionItems, 'todo_items' => $todoItems, 'key_decisions' => $keyDecisions, 'detected_sentiment' => $sentiment, 'sentiment_breakdown' => [ 'happy_pct' => $sentiment === 'happy' ? 75 : 15, 'neutral_pct' => $sentiment === 'neutral' ? 70 : 15, 'sad_pct' => $sentiment === 'sad' ? 65 : 10, 'angry_pct' => $sentiment === 'angry' ? 80 : 5, ], 'angry_analysis' => $angryAnalysis, 'emotion_details' => $emotionDetails, ]; } /** * Build structured emotion breakdown for popups (Why happy, why sad, why angry). */ protected function buildFallbackEmotionDetails(string $sentiment, string $projectName, string $clientName, string $summary, array $happyWords = [], array $sadWords = [], array $angryWords = []): array { switch ($sentiment) { case 'happy': return [ 'emotion' => 'happy', 'headline' => "Client Expressed High Satisfaction & Praise", 'bullet_points' => [ "Expressed enthusiasm regarding completed sprint milestones and feature quality.", "Complimented speed of execution and transparency during the demo.", "Approved the proposed architecture and expressed confidence in the team's delivery." ], 'trigger_quotes' => $happyWords ?: ['Great progress', 'Looks fantastic', 'Appreciate the quick work'], 'suggested_action' => "Send a brief thank-you note, preserve current sprint momentum, and maintain regular demo cadence." ]; case 'sad': return [ 'emotion' => 'sad', 'headline' => "Client Expressed Concern / Hesitation", 'bullet_points' => [ "Expressed worries regarding upcoming production release deadlines.", "Voiced hesitation about scope changes or budget allocations.", "Hesitant on testing coverage and requested extra verification before launch." ], 'trigger_quotes' => $sadWords ?: ['Concerned about timeline', 'Worried about testing', 'Disappointed with pace'], 'suggested_action' => "Tech Lead and PM should proactively provide a detailed risk-mitigation checklist and schedule a reassuring technical walkthrough." ]; case 'angry': return [ 'emotion' => 'angry', 'headline' => "Client Frustration & Escalation Alert", 'bullet_points' => [ "Voiced intense dissatisfaction regarding milestone delivery delays.", "Highlighted broken modules or recurring bugs in staging environment.", "Complained about lack of proactive communication before sprint deadlines." ], 'trigger_quotes' => $angryWords ?: ['Unacceptable delay', 'Broken features', 'Extremely frustrated'], 'suggested_action' => "Director & PM must arrange an immediate executive check-in, assign dedicated senior engineers to unblock issues, and deliver hotfix within 24h." ]; default: return [ 'emotion' => 'neutral', 'headline' => "Professional & Standard Alignment", 'bullet_points' => [ "Meeting proceeded with standard corporate tone and routine inquiries.", "No major blockers or heightened emotional triggers detected." ], 'trigger_quotes' => [], 'suggested_action' => "Continue regular sprint progress updates." ]; } } }