fix issu
This commit is contained in:
parent
80b772bc8b
commit
4803f86074
@ -12,6 +12,28 @@
|
|||||||
|
|
||||||
class AiClientCallService
|
class AiClientCallService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Get configured Gemini API Key from settings or environment.
|
||||||
|
*/
|
||||||
|
protected function getApiKey(): ?string
|
||||||
|
{
|
||||||
|
$settingKey = null;
|
||||||
|
try {
|
||||||
|
if (class_exists(\App\Models\Setting::class)) {
|
||||||
|
$settingKey = \App\Models\Setting::get('gemini_api_key') ?: \App\Models\Setting::get('google_api_key');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {}
|
||||||
|
|
||||||
|
return $settingKey
|
||||||
|
?: (config('services.gemini.key')
|
||||||
|
?: (config('services.gemini.api_key')
|
||||||
|
?: (config('services.google.key')
|
||||||
|
?: (env('GEMINI_API_KEY')
|
||||||
|
?: (env('GOOGLE_API_KEY')
|
||||||
|
?: (getenv('GEMINI_API_KEY')
|
||||||
|
?: (getenv('GOOGLE_API_KEY') ?: null)))))));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate Comprehensive MoM, Action Items, To-Do List, Sentiment, and Emotion Breakdown.
|
* Generate Comprehensive MoM, Action Items, To-Do List, Sentiment, and Emotion Breakdown.
|
||||||
*
|
*
|
||||||
@ -23,28 +45,40 @@ class AiClientCallService
|
|||||||
*/
|
*/
|
||||||
public function generateMoM(?string $transcript = '', ?string $liveNotes = '', ?string $projectName = '', ?string $clientName = ''): array
|
public function generateMoM(?string $transcript = '', ?string $liveNotes = '', ?string $projectName = '', ?string $clientName = ''): array
|
||||||
{
|
{
|
||||||
$transcript = (string)($transcript ?? '');
|
$transcript = trim((string)($transcript ?? ''));
|
||||||
$liveNotes = (string)($liveNotes ?? '');
|
$liveNotes = trim((string)($liveNotes ?? ''));
|
||||||
$projectName = (string)($projectName ?? '');
|
$projectName = trim((string)($projectName ?? ''));
|
||||||
$clientName = (string)($clientName ?? '');
|
$clientName = trim((string)($clientName ?? ''));
|
||||||
|
|
||||||
$apiKey = env('GEMINI_API_KEY') ?: (env('GOOGLE_API_KEY') ?: (getenv('GEMINI_API_KEY') ?: getenv('GOOGLE_API_KEY')));
|
$apiKey = $this->getApiKey();
|
||||||
|
|
||||||
$combinedContent = "Project: {$projectName}\nClient: {$clientName}\n\n";
|
$combinedContent = "Project: " . ($projectName ?: 'Client Project') . "\nClient: " . ($clientName ?: 'Client Partner') . "\n\n";
|
||||||
if (!empty($liveNotes)) {
|
if (!empty($liveNotes)) {
|
||||||
$combinedContent .= "Live Notes Taken During Call:\n{$liveNotes}\n\n";
|
$combinedContent .= "Live Notes Taken During Call:\n{$liveNotes}\n\n";
|
||||||
}
|
}
|
||||||
$combinedContent .= "Call Speech Transcript:\n" . (!empty($transcript) ? $transcript : "No spoken transcript recorded. Live meeting notes provided.");
|
$combinedContent .= "Call Speech Transcript:\n" . (!empty($transcript) ? $transcript : "No spoken transcript recorded.");
|
||||||
|
|
||||||
if ($apiKey) {
|
if ($apiKey) {
|
||||||
try {
|
$prompt = <<<PROMPT
|
||||||
$prompt = <<<PROMPT
|
You are an expert Executive AI Note Taker and Meeting Intelligence Specialist for corporate client video calls.
|
||||||
You are an Executive AI Note Taker and Meeting Intelligence Specialist for corporate client video calls.
|
|
||||||
Analyze the provided meeting notes and transcript for project "{$projectName}" with client "{$clientName}".
|
Analyze the provided meeting notes and transcript for project "{$projectName}" with client "{$clientName}".
|
||||||
|
|
||||||
Generate a comprehensive meeting report with:
|
CRITICAL INSTRUCTIONS:
|
||||||
1. "summary": A concise executive summary of the call (2-4 sentences).
|
1. You MUST extract all summary points, MoM content, action items, to-do items, decisions, and blockers STRICTLY and ONLY from the provided "Meeting Content" (live notes and spoken transcript).
|
||||||
2. "mom_content": Formatted Minutes of Meeting (MoM) in clear sections: (1. Meeting Objectives, 2. Key Discussion Points, 3. Challenges / Blockers Raised, 4. Agreed Next Steps & Timeline).
|
2. DO NOT invent, hallucinate, or output generic software development tasks (such as "Deploy latest sprint updates", "Provide API contracts", "Follow up on feedback") unless those exact topics were explicitly discussed or noted in the meeting content.
|
||||||
|
3. If the participants discussed specific topics (e.g. updating a button color, fixing a login issue, revising a budget, shipping designs by Friday), your tasks and MoM points MUST specifically reflect those exact topics, names, deadlines, and requirements.
|
||||||
|
4. If only 1 or 2 specific tasks were discussed, return ONLY those 1 or 2 tasks. Do NOT pad with fake placeholder tasks.
|
||||||
|
5. If the meeting content is empty or contains no actionable dialogue, state in the summary that no discussion points were recorded and return empty arrays for tasks.
|
||||||
|
|
||||||
|
Generate a comprehensive meeting report in JSON with:
|
||||||
|
1. "summary": A concise executive summary strictly summarizing what was actually discussed (2-3 sentences).
|
||||||
|
2. "mom_content": Formatted Minutes of Meeting (MoM) in markdown sections:
|
||||||
|
### Minutes of Meeting (MoM)
|
||||||
|
**Project:** {$projectName} | **Client:** {$clientName} | **Date:** [Meeting Date]
|
||||||
|
#### 1. Meeting Objectives (Brief statement based on discussion topics)
|
||||||
|
#### 2. Key Discussion Points (Specific bullet points of actual topics discussed in notes/transcript)
|
||||||
|
#### 3. Challenges / Blockers Raised (Specific blockers raised, or "No critical blockers raised" if none)
|
||||||
|
#### 4. Agreed Next Steps & Timeline (Specific action items and deliverables agreed upon)
|
||||||
3. "action_items": An array of objects with keys {"task": string, "owner": string, "deadline": string, "status": "Pending"}.
|
3. "action_items": An array of objects with keys {"task": string, "owner": string, "deadline": string, "status": "Pending"}.
|
||||||
4. "todo_items": An array of actionable To-Do tasks derived from the conversation with keys {"id": int, "task": string, "owner": string, "priority": "High"|"Medium"|"Low", "deadline": string, "completed": boolean (false)}.
|
4. "todo_items": An array of actionable To-Do tasks derived from the conversation with keys {"id": int, "task": string, "owner": string, "priority": "High"|"Medium"|"Low", "deadline": string, "completed": boolean (false)}.
|
||||||
5. "key_decisions": An array of objects with key {"decision": string}.
|
5. "key_decisions": An array of objects with key {"decision": string}.
|
||||||
@ -53,8 +87,8 @@ public function generateMoM(?string $transcript = '', ?string $liveNotes = '', ?
|
|||||||
8. "emotion_details": An object explaining the client's emotional state with:
|
8. "emotion_details": An object explaining the client's emotional state with:
|
||||||
{
|
{
|
||||||
"emotion": "happy"|"sad"|"angry"|"neutral",
|
"emotion": "happy"|"sad"|"angry"|"neutral",
|
||||||
"headline": string (e.g. "Client Delighted with Sprint 2 Deliverables" / "Client Expressed Timeline Concerns" / "Client Frustrated Over Delay"),
|
"headline": string (e.g. "Client Delighted with Deliverables" / "Client Expressed Timeline Concerns" / "Client Frustrated Over Delay"),
|
||||||
"bullet_points": array of strings (bullet points explaining specifically WHY the client is happy, why sad, or why angry. If neutral, return 1-2 bullet points on professional alignment),
|
"bullet_points": array of strings (bullet points explaining specifically WHY the client is happy, why sad, or why angry based on actual discussion),
|
||||||
"trigger_quotes": array of strings (exact phrases spoken by client reflecting this emotion),
|
"trigger_quotes": array of strings (exact phrases spoken by client reflecting this emotion),
|
||||||
"suggested_action": string (recommended action for PM, TL, and Director)
|
"suggested_action": string (recommended action for PM, TL, and Director)
|
||||||
}
|
}
|
||||||
@ -71,35 +105,41 @@ public function generateMoM(?string $transcript = '', ?string $liveNotes = '', ?
|
|||||||
Respond ONLY with valid JSON matching this schema.
|
Respond ONLY with valid JSON matching this schema.
|
||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
$response = Http::timeout(15)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={$apiKey}", [
|
// Attempt generation across available models
|
||||||
'contents' => [
|
$models = ['gemini-1.5-flash', 'gemini-2.0-flash', 'gemini-1.5-pro'];
|
||||||
[
|
|
||||||
'parts' => [
|
foreach ($models as $model) {
|
||||||
['text' => $prompt . "\n\nMeeting Content:\n" . $combinedContent]
|
try {
|
||||||
|
$response = Http::timeout(20)->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
|
||||||
]
|
]
|
||||||
],
|
]);
|
||||||
'generationConfig' => [
|
|
||||||
'response_mime_type' => 'application/json',
|
|
||||||
'temperature' => 0.2
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
$json = $response->json();
|
$json = $response->json();
|
||||||
$text = $json['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
$text = $json['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||||
$data = json_decode($text, true);
|
$data = json_decode($text, true);
|
||||||
|
|
||||||
if (is_array($data) && isset($data['mom_content'])) {
|
if (is_array($data) && isset($data['mom_content'])) {
|
||||||
return $this->formatMoMData($data, $projectName, $clientName);
|
return $this->formatMoMData($data, $projectName, $clientName, $transcript, $liveNotes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning("AI MoM Model {$model} error: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('AI MoM Generation Error: ' . $e->getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback Intelligent Extraction Engine
|
// Intelligent Local NLP Extraction Fallback Engine (strictly grounded in actual transcript and live notes)
|
||||||
return $this->generateFallbackMoM($combinedContent, $transcript, $liveNotes, $projectName, $clientName);
|
return $this->generateFallbackMoM($combinedContent, $transcript, $liveNotes, $projectName, $clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,7 +526,7 @@ public function formatTodosPlainText(ClientCallNote $note): string
|
|||||||
/**
|
/**
|
||||||
* Format MoM data to ensure all keys and structures exist.
|
* Format MoM data to ensure all keys and structures exist.
|
||||||
*/
|
*/
|
||||||
protected function formatMoMData(array $data, string $projectName = '', string $clientName = ''): array
|
protected function formatMoMData(array $data, string $projectName = '', string $clientName = '', string $transcript = '', string $liveNotes = ''): array
|
||||||
{
|
{
|
||||||
$sentiment = strtolower($data['detected_sentiment'] ?? 'neutral');
|
$sentiment = strtolower($data['detected_sentiment'] ?? 'neutral');
|
||||||
if (!in_array($sentiment, ['happy', 'neutral', 'sad', 'angry'])) {
|
if (!in_array($sentiment, ['happy', 'neutral', 'sad', 'angry'])) {
|
||||||
@ -515,27 +555,72 @@ protected function formatMoMData(array $data, string $projectName = '', string $
|
|||||||
$emotionDetails = $this->buildFallbackEmotionDetails($sentiment, $projectName, $clientName, $data['summary'] ?? '');
|
$emotionDetails = $this->buildFallbackEmotionDetails($sentiment, $projectName, $clientName, $data['summary'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format todo_items
|
// Format action_items
|
||||||
$todoItems = $data['todo_items'] ?? [];
|
$rawActionItems = is_array($data['action_items'] ?? null) ? $data['action_items'] : [];
|
||||||
if (empty($todoItems) && !empty($data['action_items'])) {
|
$actionItems = [];
|
||||||
$todoItems = array_map(function($act, $i) {
|
foreach ($rawActionItems as $act) {
|
||||||
return [
|
$taskText = is_array($act) ? ($act['task'] ?? $act['title'] ?? '') : (string)$act;
|
||||||
'id' => $i + 1,
|
if (!empty(trim($taskText))) {
|
||||||
'task' => is_array($act) ? ($act['task'] ?? '') : (string)$act,
|
$actionItems[] = [
|
||||||
'owner' => is_array($act) ? ($act['owner'] ?? 'Team') : 'Team',
|
'task' => trim($taskText),
|
||||||
'priority' => is_array($act) ? ($act['priority'] ?? 'Medium') : 'Medium',
|
'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'),
|
'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,
|
'completed' => false,
|
||||||
];
|
];
|
||||||
}, $data['action_items'], array_keys($data['action_items']));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 [
|
return [
|
||||||
'summary' => $data['summary'] ?? 'Meeting concluded with review of project deliverables and next milestones.',
|
'summary' => $data['summary'] ?? 'Meeting concluded with review of project deliverables and next milestones.',
|
||||||
'mom_content' => $data['mom_content'] ?? '',
|
'mom_content' => $data['mom_content'] ?? '',
|
||||||
'action_items' => is_array($data['action_items'] ?? null) ? $data['action_items'] : [],
|
'action_items' => $actionItems,
|
||||||
'todo_items' => is_array($todoItems) ? $todoItems : [],
|
'todo_items' => $todoItems,
|
||||||
'key_decisions' => is_array($data['key_decisions'] ?? null) ? $data['key_decisions'] : [],
|
'key_decisions' => $keyDecisions,
|
||||||
'detected_sentiment' => $sentiment,
|
'detected_sentiment' => $sentiment,
|
||||||
'sentiment_breakdown' => $data['sentiment_breakdown'] ?? [
|
'sentiment_breakdown' => $data['sentiment_breakdown'] ?? [
|
||||||
'happy_pct' => $sentiment === 'happy' ? 80 : 10,
|
'happy_pct' => $sentiment === 'happy' ? 80 : 10,
|
||||||
@ -549,12 +634,16 @@ protected function formatMoMData(array $data, string $projectName = '', string $
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback MoM extraction when external LLM API is unavailable.
|
* 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
|
protected function generateFallbackMoM(string $combinedContent, string $transcript, string $liveNotes, string $projectName, string $clientName): array
|
||||||
{
|
{
|
||||||
$lower = strtolower($combinedContent);
|
$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'];
|
$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)));
|
$foundNegations = array_values(array_filter($negatedHappyPhrases, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower)));
|
||||||
|
|
||||||
@ -574,6 +663,262 @@ protected function generateFallbackMoM(string $combinedContent, string $transcri
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sentiment = 'neutral';
|
$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 = [
|
$angryAnalysis = [
|
||||||
'is_angry' => false,
|
'is_angry' => false,
|
||||||
'trigger_words' => [],
|
'trigger_words' => [],
|
||||||
@ -582,91 +927,20 @@ protected function generateFallbackMoM(string $combinedContent, string $transcri
|
|||||||
'suggested_remedy' => '',
|
'suggested_remedy' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (count($foundAngry) > 0) {
|
if ($sentiment === 'angry') {
|
||||||
$sentiment = 'angry';
|
|
||||||
$angryAnalysis = [
|
$angryAnalysis = [
|
||||||
'is_angry' => true,
|
'is_angry' => true,
|
||||||
'trigger_words' => $foundAngry,
|
'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.",
|
'summary_why_angry' => "Client expressed dissatisfaction during the conversation regarding project deliverables or timeline. Identified cues: \"" . implode('", "', $foundAngry) . "\".",
|
||||||
'grievances' => [
|
'grievances' => !empty($extractedBlockers) ? array_slice($extractedBlockers, 0, 4) : [
|
||||||
'Dissatisfaction with delivery timelines or communication speed.',
|
'Concerns regarding delivery milestones or response timeliness.',
|
||||||
'Concerns regarding quality or specific functional requirements.',
|
'Specific functional requirements requiring escalation.'
|
||||||
],
|
],
|
||||||
'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.',
|
'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.',
|
||||||
];
|
];
|
||||||
} 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.";
|
$emotionDetails = $this->buildFallbackEmotionDetails($sentiment, $proj, $client, $summary, $foundHappy, $foundSad, $foundAngry, $extractedBlockers, $extractedDiscussionPoints);
|
||||||
|
|
||||||
$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 [
|
return [
|
||||||
'summary' => $summary,
|
'summary' => $summary,
|
||||||
@ -689,59 +963,73 @@ protected function generateFallbackMoM(string $combinedContent, string $transcri
|
|||||||
/**
|
/**
|
||||||
* Build structured emotion breakdown for popups (Why happy, why sad, why angry).
|
* 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
|
protected function buildFallbackEmotionDetails(string $sentiment, string $projectName, string $clientName, string $summary, array $happyWords = [], array $sadWords = [], array $angryWords = [], array $blockers = [], array $topics = []): array
|
||||||
{
|
{
|
||||||
switch ($sentiment) {
|
switch ($sentiment) {
|
||||||
case 'happy':
|
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 [
|
return [
|
||||||
'emotion' => 'happy',
|
'emotion' => 'happy',
|
||||||
'headline' => "Client Expressed High Satisfaction & Praise",
|
'headline' => "Client Expressed High Satisfaction & Positive Feedback",
|
||||||
'bullet_points' => [
|
'bullet_points' => $happyPoints,
|
||||||
"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'],
|
'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."
|
'suggested_action' => "Send a brief thank-you follow-up and maintain current sprint delivery cadence."
|
||||||
];
|
];
|
||||||
|
|
||||||
case 'sad':
|
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 [
|
return [
|
||||||
'emotion' => 'sad',
|
'emotion' => 'sad',
|
||||||
'headline' => "Client Expressed Concern / Hesitation",
|
'headline' => "Client Expressed Concern / Hesitation",
|
||||||
'bullet_points' => [
|
'bullet_points' => $sadPoints,
|
||||||
"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'],
|
'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."
|
'suggested_action' => "Tech Lead and PM should proactively provide a detailed risk-mitigation checklist and schedule a clarifying walkthrough."
|
||||||
];
|
];
|
||||||
|
|
||||||
case 'angry':
|
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 [
|
return [
|
||||||
'emotion' => 'angry',
|
'emotion' => 'angry',
|
||||||
'headline' => "Client Frustration & Escalation Alert",
|
'headline' => "Client Frustration & Escalation Alert",
|
||||||
'bullet_points' => [
|
'bullet_points' => $angryPoints,
|
||||||
"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'],
|
'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."
|
'suggested_action' => "Director & PM must arrange an immediate internal alignment and reach out with an expedited resolution."
|
||||||
];
|
];
|
||||||
|
|
||||||
default:
|
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 [
|
return [
|
||||||
'emotion' => 'neutral',
|
'emotion' => 'neutral',
|
||||||
'headline' => "Professional & Standard Alignment",
|
'headline' => "Professional & Standard Alignment",
|
||||||
'bullet_points' => [
|
'bullet_points' => $neutralPoints,
|
||||||
"Meeting proceeded with standard corporate tone and routine inquiries.",
|
|
||||||
"No major blockers or heightened emotional triggers detected."
|
|
||||||
],
|
|
||||||
'trigger_quotes' => [],
|
'trigger_quotes' => [],
|
||||||
'suggested_action' => "Continue regular sprint progress updates."
|
'suggested_action' => "Continue regular milestone updates and sprint reviews."
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -444,10 +444,14 @@ function initSpeechRecognition() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
speechRecognizer.onend = () => {
|
speechRecognizer.onend = () => {
|
||||||
// Auto restart recognition
|
// Auto restart recognition with debounce
|
||||||
try {
|
setTimeout(() => {
|
||||||
speechRecognizer.start();
|
try {
|
||||||
} catch (err) {}
|
if (speechRecognizer) {
|
||||||
|
speechRecognizer.start();
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
speechRecognizer.start();
|
speechRecognizer.start();
|
||||||
|
|||||||
@ -959,4 +959,51 @@ public function test_signaling_heartbeat_and_chat_messages(): void
|
|||||||
$this->assertNotEmpty($meeting->chat_messages);
|
$this->assertNotEmpty($meeting->chat_messages);
|
||||||
$this->assertEquals('Hello everyone!', $meeting->chat_messages[0]['text']);
|
$this->assertEquals('Hello everyone!', $meeting->chat_messages[0]['text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_actual_discussed_tasks_and_mom_are_extracted_from_notes_and_transcript(): void
|
||||||
|
{
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$client = Client::create([
|
||||||
|
'client_name' => 'Michael Scott',
|
||||||
|
'client_email' => 'michael@dunder.com',
|
||||||
|
'project_name' => 'Paper Ordering Portal',
|
||||||
|
'responsible_user_id' => $this->pm->id,
|
||||||
|
'created_by' => $this->admin->id,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$meeting = ClientMeeting::create([
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'title' => 'Feature Review Call',
|
||||||
|
'meeting_code' => 'meet-paper-portal-77',
|
||||||
|
'host_user_id' => $this->pm->id,
|
||||||
|
'status' => 'in_progress',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->pm)
|
||||||
|
->postJson(route('client-calls.end-call', $meeting->meeting_code), [
|
||||||
|
'transcript' => "[10:01 AM] Michael: We need to change the logo to navy blue and add PayPal checkout by Friday.\n[10:02 AM] Alex Dev: I will implement PayPal checkout by Friday and update the logo.",
|
||||||
|
'live_notes' => "- Fix shopping cart checkout discount code bug\n- Sarah will send updated invoice by tomorrow\n- Client approved the new homepage layout",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson(['success' => true]);
|
||||||
|
|
||||||
|
$note = ClientCallNote::where('client_meeting_id', $meeting->id)->first();
|
||||||
|
$this->assertNotNull($note);
|
||||||
|
|
||||||
|
// Verify that extracted tasks contain the actual discussed items
|
||||||
|
$tasks = collect($note->todo_items)->pluck('task')->implode(' ');
|
||||||
|
$this->assertStringContainsString('shopping cart checkout discount code bug', strtolower($tasks));
|
||||||
|
$this->assertStringContainsString('invoice by tomorrow', strtolower($tasks));
|
||||||
|
|
||||||
|
// Verify MoM content contains the actual discussion points
|
||||||
|
$this->assertStringContainsString('shopping cart checkout discount code bug', strtolower($note->mom_content));
|
||||||
|
$this->assertStringContainsString('paypal', strtolower($note->mom_content));
|
||||||
|
|
||||||
|
// Verify that generic placeholder tasks are NOT present
|
||||||
|
$this->assertStringNotContainsString('deploy latest sprint milestone updates to staging environment', strtolower($tasks));
|
||||||
|
$this->assertStringNotContainsString('provide updated api contracts and sandbox test credentials', strtolower($tasks));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user