From cfdbd57bce7c7f0bb700acccf7fa0afb5c8d2285 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 31 May 2026 19:47:41 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20task/appt=20voice=20creation=20=E2=80=94?= =?UTF-8?q?=20non-greedy=20trigger=20strip,=20bare-date=20extraction,=20no?= =?UTF-8?q?on/midnight,=20book=20trigger,=20900ms=20TTS=20mic=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/endpoints/chat.php | 78 +++++++++++++++++++++++++++++++----------- public_html/index.html | 4 +-- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index be82628..8245476 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -666,20 +666,31 @@ if (!$reply) { // ── Add task ────────────────────────────────────────────────────────── if (!$reply && preg_match('/\b(add task|remind me to|todo:|to do:|i need to|don.?t forget to|create task|new task|put.*task|add.*to.*list)\b/i', $message)) { - $title = preg_replace('/^.*(add task|remind me to|todo:|to do:|i need to|don.?t forget to|create task|new task|put.*task|add.*to.*list)\s*/i', '', $message); + // Strip trigger at START only (non-greedy anchor prevents stripping mid-phrase words) + $title = preg_replace('/^\s*(?:jarvis\s+)?(?:add task|remind me to|todo:|to do:|i need to|don.?t forget to|create task|new task|put\s+\w+\s+on\s+(?:my\s+)?(?:task\s+)?list|add\s+(?:this\s+)?to\s+(?:my\s+)?list)\s*/i', '', $message); $title = trim($title, '. '); $dueDate = null; - if (preg_match('/\b(?:by|on|due|before)\s+(.+)$/i', $title, $dm)) { - $ts = strtotime($dm[1]); - if ($ts !== false) { + // Extract date — "by Friday", "on Monday", "due tomorrow", OR bare date at end of phrase + $datePattern = '((?:next\s+\w+|this\s+(?:monday|tuesday|wednesday|thursday|friday)|tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?)(?:\s+at\s+\d{1,2}(?::\d{2})?\s*(?:am|pm)?)?)'; + if (preg_match('/\s+(?:by|on|due|before|this|next)\s+' . $datePattern . '\s*$/i', $title, $dm)) { + $ts = strtotime(trim($dm[1])); + if ($ts !== false && $ts > time() - 86400) { $dueDate = date('Y-m-d', $ts); - $title = trim(preg_replace('/\s+(?:by|on|due|before)\s+.+$/i', '', $title)); + $title = trim(preg_replace('/\s+(?:by|on|due|before)\s+.*$/i', '', $title)); + } + } elseif (preg_match('/\s+' . $datePattern . '\s*$/i', $title, $dm)) { + // Bare date at end: "call dentist tomorrow", "pay bills Monday" + $ts = strtotime(trim($dm[1])); + if ($ts !== false && $ts > time() - 86400 && $ts < time() + 365*86400) { + $dueDate = date('Y-m-d', $ts); + $title = trim(substr($title, 0, strrpos($title, $dm[0]))); } } - $category = preg_match('/\b(work|meeting|project|client|office)\b/i', $title) ? 'work' : 'personal'; + $category = preg_match('/\b(work|meeting|project|client|office|report|email)\b/i', $title) ? 'work' : 'personal'; $priority = 'normal'; if (preg_match('/\b(urgent|asap|emergency|critical)\b/i', $title)) $priority = 'urgent'; elseif (preg_match('/\b(important|high priority)\b/i', $title)) $priority = 'high'; + $title = trim($title); if ($title) { JarvisDB::execute('INSERT INTO tasks (title,category,priority,due_date) VALUES (?,?,?,?)', [$title, $category, $priority, $dueDate]); $duePart = $dueDate ? ', due ' . date('l, M j', strtotime($dueDate)) : ''; @@ -770,28 +781,55 @@ if (!$reply) { } // ── Add appointment ─────────────────────────────────────────────────── - if (!$reply && preg_match('/\b(schedule|appointment|add.*calendar|book|set up.*meeting|add.*meeting)\b/i', $message) - && !preg_match('/\b(my calendar|upcoming|list|show|what)\b/i', $message)) { - $raw = preg_replace('/^.*(schedule|appointment|add.*calendar|book|set up.*meeting|add.*meeting)\s*/i', '', $message); + if (!$reply && preg_match('/\b(schedule|add\s+(?:an?\s+)?appointment|book\s+(?:an?\s+)?|set\s+up\s+(?:an?\s+)?meeting|add\s+(?:an?\s+)?meeting|add\s+to\s+(?:my\s+)?calendar)\b/i', $message) + && !preg_match('/\b(my calendar|upcoming|list|show|what|from email)\b/i', $message)) { + // Strip trigger at START only — prevents eating words like "dentist appointment" + $raw = preg_replace('/^\s*(?:jarvis\s+)?(?:schedule|add\s+(?:an?\s+)?appointment|book\s+(?:a?n?\s+)?|set\s+up\s+(?:an?\s+)?meeting|add\s+(?:an?\s+)?meeting|add\s+to\s+(?:my\s+)?calendar)\s*/i', '', $message); $raw = trim($raw); + + // Normalize natural time words + $raw = preg_replace('/\bnoon\b/i', '12:00 pm', $raw); + $raw = preg_replace('/\bmidnight\b/i', '12:00 am', $raw); + $raw = preg_replace('/\bmidday\b/i', '12:00 pm', $raw); + + // Step 1: extract time ("at 2pm", "at 2:30pm", "2pm", "14:00") + $timeStr = null; + if (preg_match('/\bat\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\b/i', $raw, $tm)) { + $timeStr = $tm[1]; + $raw = trim(str_ireplace($tm[0], '', $raw)); + } elseif (preg_match('/\b(\d{1,2}:\d{2}\s*(?:am|pm)?)\b/i', $raw, $tm)) { + $timeStr = $tm[1]; + $raw = trim(str_ireplace($tm[0], '', $raw)); + } + + // Step 2: extract date (day names, "tomorrow", "next X", month+day) + $dateStr = null; + if (preg_match('/\b(tomorrow|today|next\s+\w+|this\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)|monday|tuesday|wednesday|thursday|friday|saturday|sunday|(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2})\b/i', $raw, $dm)) { + $dateStr = $dm[1]; + $raw = trim(str_ireplace($dm[0], '', $raw)); + } + + // Step 3: build datetime $dtParsed = null; - $title = $raw; - if (preg_match('/\b(tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next\s+\w+|jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?).{0,30}\b(\d{1,2}(?::\d{2})?\s*(?:am|pm)?)\b/i', $raw, $dm)) { - $ts = strtotime($dm[0]); - if ($ts !== false && $ts > time()) { $dtParsed = date('Y-m-d H:i:s', $ts); $title = trim(str_replace($dm[0], '', $raw)); } + if ($dateStr || $timeStr) { + $dtInput = trim(($dateStr ?: 'today') . ' ' . ($timeStr ?: '09:00 am')); + $ts = strtotime($dtInput); + if ($ts !== false && $ts > time() - 3600) { + $dtParsed = date('Y-m-d H:i:s', $ts); + } } - if (!$dtParsed && preg_match('/\b(tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next\s+\w+)\b/i', $raw, $dm)) { - $ts = strtotime($dm[0]); - if ($ts !== false && $ts >= strtotime($today)) { $dtParsed = date('Y-m-d 09:00:00', $ts); $title = trim(str_replace($dm[0], '', $raw)); } - } - $title = trim($title, ' .'); + + // Step 4: clean up title (remove leftover filler words) + $title = trim(preg_replace('/\s+/', ' ', preg_replace('/\b(on|at|the|a|an)\b\s*/i', ' ', $raw))); + $title = trim($title, ' .,'); + if ($title && $dtParsed) { - $category = preg_match('/\b(work|meeting|office|client|project|call)\b/i', $title) ? 'work' : 'personal'; + $category = preg_match('/\b(work|meeting|office|client|project|call|conference|interview)\b/i', $title) ? 'work' : 'personal'; JarvisDB::execute('INSERT INTO appointments (title,category,start_at) VALUES (?,?,?)', [$title, $category, $dtParsed]); $reply = "Scheduled: \"{$title}\" on " . date('l, M j \a\t g:i A', strtotime($dtParsed)) . ", {$userAddr}."; $source = 'planner:appt_add'; } elseif ($title) { - $reply = "I can add that, {$userAddr} — what date and time?"; + $reply = "I can schedule that, {$userAddr} — what date and time?"; $source = 'planner:appt_need_time'; } } diff --git a/public_html/index.html b/public_html/index.html index 2dc691e..89d4526 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -1938,7 +1938,7 @@ async function speak(text) { isSpeaking = false; reactor?.classList.remove('speaking'); // onend will fire from the abort we did before TTS, and restart cleanly - if (isListening) _scheduleRecStart(400); + if (isListening) _scheduleRecStart(900); }; try { const res = await fetch('/api/tts', { @@ -1971,7 +1971,7 @@ function _speakFallback(text) { utter.onend = () => { reactor?.classList.remove('speaking'); isSpeaking = false; - if (isListening) _scheduleRecStart(400); + if (isListening) _scheduleRecStart(900); }; synth.speak(utter); }