getApiKey(); $combinedContent = "Project: " . ($projectName ?: 'Client Project') . "\nClient: " . ($clientName ?: 'Client Partner') . "\n\n"; if (!empty($liveNotes)) { $combinedContent .= "Live Notes Taken During Call:\n{$liveNotes}\n\n"; } $combinedContent .= "Call Speech Transcript:\n" . (!empty($transcript) ? $transcript : "No spoken transcript recorded."); if ($apiKey) { $prompt = <<post("https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}", [ 'contents' => [ [ 'parts' => [ ['text' => $prompt . "\n\nMeeting Content:\n" . $combinedContent] ] ] ], '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['mom_content'])) { return $this->formatMoMData($data, $projectName, $clientName, $transcript, $liveNotes); } } } catch (\Throwable $e) { Log::warning("AI MoM Model {$model} error: " . $e->getMessage()); } } } // Intelligent Local NLP Extraction Fallback Engine (strictly grounded in actual transcript and live notes) 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 = '', string $transcript = '', string $liveNotes = ''): 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 action_items $rawActionItems = is_array($data['action_items'] ?? null) ? $data['action_items'] : []; $actionItems = []; foreach ($rawActionItems as $act) { $taskText = is_array($act) ? ($act['task'] ?? $act['title'] ?? '') : (string)$act; if (!empty(trim($taskText))) { $actionItems[] = [ 'task' => trim($taskText), 'owner' => is_array($act) ? ($act['owner'] ?? 'Assigned Lead') : 'Assigned Lead', 'deadline' => is_array($act) ? ($act['deadline'] ?? now()->addDays(3)->format('M d, Y')) : now()->addDays(3)->format('M d, Y'), 'status' => is_array($act) ? ($act['status'] ?? 'Pending') : 'Pending', ]; } } // Format todo_items $rawTodoItems = is_array($data['todo_items'] ?? null) ? $data['todo_items'] : []; $todoItems = []; if (!empty($rawTodoItems)) { foreach ($rawTodoItems as $i => $item) { $taskText = is_array($item) ? ($item['task'] ?? $item['title'] ?? '') : (string)$item; if (!empty(trim($taskText))) { $priority = is_array($item) ? ($item['priority'] ?? 'Medium') : 'Medium'; if (!in_array($priority, ['High', 'Medium', 'Low'])) { $priority = 'Medium'; } $todoItems[] = [ 'id' => is_array($item) && isset($item['id']) ? (int)$item['id'] : ($i + 1), 'task' => trim($taskText), 'owner' => is_array($item) ? ($item['owner'] ?? 'Assigned Lead') : 'Assigned Lead', 'priority' => $priority, 'deadline' => is_array($item) ? ($item['deadline'] ?? now()->addDays(3)->format('M d, Y')) : now()->addDays(3)->format('M d, Y'), 'completed' => is_array($item) ? (bool)($item['completed'] ?? false) : false, ]; } } } elseif (!empty($actionItems)) { foreach ($actionItems as $i => $act) { $todoItems[] = [ 'id' => $i + 1, 'task' => $act['task'], 'owner' => $act['owner'], 'priority' => 'Medium', 'deadline' => $act['deadline'], 'completed' => false, ]; } } // Format key decisions $rawDecisions = is_array($data['key_decisions'] ?? null) ? $data['key_decisions'] : []; $keyDecisions = []; foreach ($rawDecisions as $dec) { $decText = is_array($dec) ? ($dec['decision'] ?? '') : (string)$dec; if (!empty(trim($decText))) { $keyDecisions[] = ['decision' => trim($decText)]; } } return [ 'summary' => $data['summary'] ?? 'Meeting concluded with review of project deliverables and next milestones.', 'mom_content' => $data['mom_content'] ?? '', 'action_items' => $actionItems, 'todo_items' => $todoItems, 'key_decisions' => $keyDecisions, '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, ]; } /** * Intelligent Local NLP Extraction Engine when external LLM API is unavailable. * Extracts actual discussion items, action items, blockers, decisions, and emotions from live notes and speech. */ protected function generateFallbackMoM(string $combinedContent, string $transcript, string $liveNotes, string $projectName, string $clientName): array { $lower = strtolower($combinedContent); $proj = $projectName ?: 'Client Project'; $client = $clientName ?: 'Client Partner'; // 1. Sentiment Keyword Extraction $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'; if (count($foundAngry) > 0) { $sentiment = 'angry'; } elseif (count($foundSad) > 0) { $sentiment = 'sad'; } elseif (count($foundHappy) > 0) { $sentiment = 'happy'; } // 2. Parse Raw Lines from Live Notes and Transcript $rawLines = []; if (!empty($liveNotes)) { $splitNotes = preg_split('/\r\n|\r|\n/', $liveNotes); foreach ($splitNotes as $noteLine) { $cleaned = trim(preg_replace('/^(\s*[-*•#\d\.\)\(\[\]xX]+\s*)+/', '', $noteLine)); if (!empty($cleaned) && strlen($cleaned) >= 3) { $rawLines[] = [ 'source' => 'notes', 'speaker' => 'Meeting Notes', 'text' => $cleaned, ]; } } } if (!empty($transcript)) { $splitTranscript = preg_split('/\r\n|\r|\n/', $transcript); foreach ($splitTranscript as $tLine) { $tLine = trim($tLine); if (empty($tLine)) continue; // Match format: [10:00 AM] Speaker: Spoken text OR Speaker: Spoken text $speaker = 'Speaker'; $text = $tLine; if (preg_match('/^(?:\[[^\]]+\]\s*)?([^:]+):\s*(.+)$/i', $tLine, $m)) { $speaker = trim($m[1]); $text = trim($m[2]); } else { $text = preg_replace('/^\[[^\]]+\]\s*/', '', $tLine); } if (!empty($text) && strlen($text) >= 3) { $rawLines[] = [ 'source' => 'transcript', 'speaker' => $speaker, 'text' => $text, ]; } } } // 3. Extract Tasks & Action Items from Actual Discussion Lines $extractedTasks = []; $extractedDecisions = []; $extractedBlockers = []; $extractedDiscussionPoints = []; $taskTriggers = [ 'fix', 'update', 'implement', 'deploy', 'create', 'build', 'send', 'email', 'share', 'provide', 'review', 'check', 'test', 'verify', 'resolve', 'prepare', 'design', 'refactor', 'migrate', 'setup', 'configure', 'integrate', 'optimize', 'schedule', 'add', 'remove', 'delete', 'change', 'modify', 'finalize', 'follow up', 'deliver', 'submit', 'contact', 'call', 'arrange', 'investigate', 'complete', 'write', 'publish', 'document', 'rework', 'todo', 'task', 'need to', 'needs to', 'have to', 'has to', 'will', 'shall', 'must', 'should', 'action item' ]; $decisionTriggers = ['agreed', 'decided', 'approved', 'confirmed', 'finalized', 'settled on', 'chosen', 'selected', 'proceed with', 'sign off', 'signed off', 'accepted', 'aligned on', 'consensus']; $blockerTriggers = ['issue', 'bug', 'error', 'broken', 'problem', 'blocker', 'delay', 'delayed', 'failed', 'struggling', 'challenge', 'difficulty', 'risk', 'concern', 'complaint', 'stuck', 'bottleneck', 'not working']; $seenTasks = []; foreach ($rawLines as $lineObj) { $lineText = $lineObj['text']; $speaker = $lineObj['speaker']; $lowerLine = strtolower($lineText); // Add to general discussion points list $extractedDiscussionPoints[] = $lineText; // Check if this line is a Blocker / Challenge foreach ($blockerTriggers as $bTrig) { if (preg_match('/\b' . preg_quote($bTrig, '/') . '\b/i', $lowerLine)) { $extractedBlockers[] = $lineText; break; } } // Check if this line is a Key Decision foreach ($decisionTriggers as $dTrig) { if (preg_match('/\b' . preg_quote($dTrig, '/') . '\b/i', $lowerLine)) { $cleanDec = preg_replace('/^(we|team|client)\s+/i', '', $lineText); $extractedDecisions[] = ucfirst(trim($cleanDec)); break; } } // Check if this line is an Action Item / Task $isTask = false; if ($lineObj['source'] === 'notes') { // Any note typed manually by the user during call is an actionable point or task $isTask = true; } else { foreach ($taskTriggers as $tTrig) { if (preg_match('/\b' . preg_quote($tTrig, '/') . '\b/i', $lowerLine)) { $isTask = true; break; } } } if ($isTask) { // Clean task text $cleanTask = trim(preg_replace('/^(todo|task|action item|action):\s*/i', '', $lineText)); $cleanTask = ucfirst(trim(preg_replace('/^(we need to|need to|we will|i will|please|team should|client requested to)\s+/i', '', $cleanTask))); $taskKey = strtolower(substr($cleanTask, 0, 40)); if (isset($seenTasks[$taskKey])) { continue; } $seenTasks[$taskKey] = true; // Extract Owner $owner = 'Assigned Lead'; if ($speaker !== 'Meeting Notes' && $speaker !== 'Speaker') { $owner = $speaker; } if (preg_match('/(?:assigned to|owner:?|lead:?)\s*([a-zA-Z\s]+)/i', $lineText, $mOwner)) { $owner = trim($mOwner[1]); } elseif (preg_match('/\b([A-Z][a-z]+)\s+(?:will|to|should)\b/', $lineText, $mOwner2)) { $owner = trim($mOwner2[1]); } elseif (preg_match('/\b(PM|TL|Tech Lead|Designer|Developer|QA|Frontend|Backend|Engineering)\b/i', $lineText, $mOwner3)) { $owner = trim($mOwner3[1]); } // Extract Deadline $deadline = now()->addDays(3)->format('M d, Y'); if (preg_match('/\bby\s+(tomorrow|today|tonight|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next week|end of day|eod|5 pm|noon)\b/i', $lineText, $mDead)) { $deadline = 'By ' . ucfirst(strtolower($mDead[1])); } elseif (preg_match('/\bby\s+([a-zA-Z]+\s+\d{1,2}(?:st|nd|rd|th)?|\d{1,2}(?:st|nd|rd|th)?\s+[a-zA-Z]+)\b/i', $lineText, $mDead2)) { $deadline = 'By ' . trim($mDead2[1]); } elseif (preg_match('/\bwithin\s+(\d+\s+(?:days|weeks|hours))\b/i', $lineText, $mDead3)) { $deadline = 'Within ' . trim($mDead3[1]); } // Extract Priority $priority = 'Medium'; if (preg_match('/\b(urgent|asap|critical|high priority|blocker|immediately|emergency|broken)\b/i', $lineText)) { $priority = 'High'; } elseif (preg_match('/\b(low priority|optional|nice to have|eventually|backlog|future|later)\b/i', $lineText)) { $priority = 'Low'; } $extractedTasks[] = [ 'task' => $cleanTask, 'owner' => $owner, 'deadline' => $deadline, 'priority' => $priority, ]; } } // 4. Format Action Items and To-Do Items $actionItems = []; $todoItems = []; foreach ($extractedTasks as $idx => $t) { $num = $idx + 1; $actionItems[] = [ 'task' => $t['task'], 'owner' => $t['owner'], 'deadline' => $t['deadline'], 'status' => 'Pending', ]; $todoItems[] = [ 'id' => $num, 'task' => $t['task'], 'owner' => $t['owner'], 'priority' => $t['priority'], 'deadline' => $t['deadline'], 'completed' => false, ]; } // 5. Format Key Decisions $keyDecisions = []; $uniqueDecisions = array_unique($extractedDecisions); foreach ($uniqueDecisions as $d) { $keyDecisions[] = ['decision' => $d]; } // 6. Build Grounded Minutes of Meeting (MoM) Content $dateStr = now()->format('F j, Y - h:i A'); $mom = "### Minutes of Meeting (MoM)\n" . "**Project:** {$proj}\n" . "**Client:** {$client}\n" . "**Date:** {$dateStr}\n\n"; // Section 1: Objectives $mom .= "#### 1. Meeting Objectives\n"; if (!empty($extractedDiscussionPoints)) { $firstTopic = $extractedDiscussionPoints[0]; $mom .= "- Review and sync on {$proj} progress, address client requirements, and align on deliverables regarding: {$firstTopic}.\n\n"; } else { $mom .= "- Sync meeting between project stakeholders and {$client}.\n\n"; } // Section 2: Discussion Points $mom .= "#### 2. Key Discussion Points\n"; if (!empty($extractedDiscussionPoints)) { $distinctPoints = array_slice(array_unique($extractedDiscussionPoints), 0, 10); foreach ($distinctPoints as $pt) { $mom .= "- " . ucfirst($pt) . "\n"; } $mom .= "\n"; } else { $mom .= "- No discussion points or live notes recorded during this session.\n\n"; } // Section 3: Challenges / Blockers $mom .= "#### 3. Challenges / Blockers Raised\n"; if (!empty($extractedBlockers)) { $distinctBlockers = array_slice(array_unique($extractedBlockers), 0, 5); foreach ($distinctBlockers as $b) { $mom .= "- " . ucfirst($b) . "\n"; } $mom .= "\n"; } elseif ($sentiment === 'angry') { $mom .= "- Client voiced dissatisfaction regarding deliverables and timeline. Proactive resolution underway.\n\n"; } else { $mom .= "- No critical blockers or unresolved obstacles raised during this sync.\n\n"; } // Section 4: Agreed Next Steps $mom .= "#### 4. Agreed Next Steps & Timeline\n"; if (!empty($actionItems)) { foreach ($actionItems as $act) { $mom .= "- **{$act['owner']}:** {$act['task']} (Target: {$act['deadline']})\n"; } } else { $mom .= "- Continue standard project roadmap and schedule follow-up check-in as needed.\n"; } // 7. Build Grounded Executive Summary if (!empty($extractedDiscussionPoints)) { $topicsSlice = array_slice(array_unique($extractedDiscussionPoints), 0, 3); $summaryTopics = implode('; ', $topicsSlice); $taskCount = count($todoItems); $summary = "Strategic sync call held for {$proj} with {$client}. Discussion centered around: {$summaryTopics}. The team agreed on {$taskCount} specific action item(s) and aligned on next milestones."; } else { $summary = "Meeting session concluded for {$proj} with {$client}. No live notes or spoken speech transcript were recorded during this call."; } // 8. Build Emotion Details & Angry Analysis $angryAnalysis = [ 'is_angry' => false, 'trigger_words' => [], 'summary_why_angry' => '', 'grievances' => [], 'suggested_remedy' => '', ]; if ($sentiment === 'angry') { $angryAnalysis = [ 'is_angry' => true, 'trigger_words' => $foundAngry, 'summary_why_angry' => "Client expressed dissatisfaction during the conversation regarding project deliverables or timeline. Identified cues: \"" . implode('", "', $foundAngry) . "\".", 'grievances' => !empty($extractedBlockers) ? array_slice($extractedBlockers, 0, 4) : [ 'Concerns regarding delivery milestones or response timeliness.', 'Specific functional requirements requiring escalation.' ], 'suggested_remedy' => 'Responsible PM/TL and Director should schedule an internal alignment call and reach out to the client with a remedial action plan within 24h.', ]; } $emotionDetails = $this->buildFallbackEmotionDetails($sentiment, $proj, $client, $summary, $foundHappy, $foundSad, $foundAngry, $extractedBlockers, $extractedDiscussionPoints); 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 $blockers = [], array $topics = []): array { switch ($sentiment) { case 'happy': $happyPoints = [ "Expressed satisfaction with project deliverables and discussed progress.", "Positive alignment on current milestones and next sprint schedule.", ]; if (!empty($topics)) { $happyPoints[] = "Constructive discussion on: " . $topics[0]; } return [ 'emotion' => 'happy', 'headline' => "Client Expressed High Satisfaction & Positive Feedback", 'bullet_points' => $happyPoints, 'trigger_quotes' => $happyWords ?: ['Great progress', 'Looks fantastic', 'Appreciate the quick work'], 'suggested_action' => "Send a brief thank-you follow-up and maintain current sprint delivery cadence." ]; case 'sad': $sadPoints = [ "Expressed concern or hesitation regarding upcoming milestones or scope.", "Requested additional verification and testing before final acceptance." ]; if (!empty($blockers)) { $sadPoints[] = "Raised concern regarding: " . $blockers[0]; } return [ 'emotion' => 'sad', 'headline' => "Client Expressed Concern / Hesitation", 'bullet_points' => $sadPoints, '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 clarifying walkthrough." ]; case 'angry': $angryPoints = [ "Voiced dissatisfaction regarding project deliverables or milestone delays.", "Requested immediate attention and prompt remedial resolution." ]; if (!empty($blockers)) { $angryPoints = array_slice($blockers, 0, 3); } return [ 'emotion' => 'angry', 'headline' => "Client Frustration & Escalation Alert", 'bullet_points' => $angryPoints, 'trigger_quotes' => $angryWords ?: ['Unacceptable delay', 'Broken features', 'Extremely frustrated'], 'suggested_action' => "Director & PM must arrange an immediate internal alignment and reach out with an expedited resolution." ]; default: $neutralPoints = [ "Meeting proceeded with standard professional tone and constructive collaboration.", "No major blockers or heightened emotional triggers detected." ]; if (!empty($topics)) { $neutralPoints[] = "Discussed items: " . implode(', ', array_slice($topics, 0, 2)); } return [ 'emotion' => 'neutral', 'headline' => "Professional & Standard Alignment", 'bullet_points' => $neutralPoints, 'trigger_quotes' => [], 'suggested_action' => "Continue regular milestone updates and sprint reviews." ]; } } }