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'))); // Check for Angry / Frustrated trigger keywords in speech text $lowerText = strtolower($speechText); $angryTriggers = ['angry', 'furious', 'unacceptable', 'ridiculous', 'terrible', 'waste of time', 'delay', 'broken', 'disaster', 'unhappy', 'frustrated', 'mad', 'annoyed', 'horrible', 'cancel', 'lawyer', 'refund', 'fail', 'incompetent', 'worst']; $sadTriggers = ['sad', 'disappointed', 'worried', 'concerned', 'nervous', 'scared', 'afraid', 'regret', 'loss', 'hopeless', 'sorry', 'doubt']; $happyTriggers = ['great', 'excellent', 'fantastic', 'awesome', 'good job', 'love it', 'happy', 'pleased', 'perfect', 'superb', 'wonderful', 'thank you', 'amazing', 'impressed']; $detectedAngryWords = []; foreach ($angryTriggers as $word) { if (str_contains($lowerText, $word)) { $detectedAngryWords[] = $word; } } $detectedSadWords = []; foreach ($sadTriggers as $word) { if (str_contains($lowerText, $word)) { $detectedSadWords[] = $word; } } $detectedHappyWords = []; foreach ($happyTriggers as $word) { if (str_contains($lowerText, $word)) { $detectedHappyWords[] = $word; } } if (!empty($detectedAngryWords)) { return [ 'emotion' => 'angry', 'confidence' => 0.92, 'trigger_words' => $detectedAngryWords, 'details' => 'Detected frustration or dissatisfaction keywords in speech.', 'label' => 'Frustrated / Angry 😡' ]; } if (!empty($detectedSadWords)) { return [ 'emotion' => 'sad', 'confidence' => 0.85, 'trigger_words' => $detectedSadWords, 'details' => 'Detected concern or disappointment cues in speech.', 'label' => 'Concerned / Sad 😟' ]; } if (!empty($detectedHappyWords)) { return [ 'emotion' => 'happy', 'confidence' => 0.88, 'trigger_words' => $detectedHappyWords, 'details' => 'Detected positive satisfaction keywords 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); $angryWords = ['angry', 'furious', 'unacceptable', 'ridiculous', 'terrible', 'waste of time', 'delay', 'broken', 'disaster', 'unhappy', 'frustrated', 'mad', 'annoyed', 'horrible', 'refund', 'cancel', 'fail', 'incompetent']; $sadWords = ['sad', 'disappointed', 'worried', 'concerned', 'nervous', 'scared', 'afraid', 'regret', 'doubt']; $happyWords = ['great', 'excellent', 'fantastic', 'awesome', 'good job', 'love it', 'happy', 'pleased', 'perfect', 'superb', 'wonderful', 'thank you', 'appreciate', 'impressed']; $foundAngry = array_values(array_filter($angryWords, fn($w) => str_contains($lower, $w))); $foundSad = array_values(array_filter($sadWords, fn($w) => str_contains($lower, $w))); $foundHappy = array_values(array_filter($happyWords, fn($w) => str_contains($lower, $w))); $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." ]; } } }