From ac81927426f68827c8bd61933537b4c138376941 Mon Sep 17 00:00:00 2001 From: DHCross <45954119+DHCross@users.noreply.github.com> Date: Sun, 28 Jun 2026 20:41:40 -0500 Subject: [PATCH] Improve birth wheel response state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../__tests__/forecastIntent.test.ts | 11 ++++ vessel/src/app/api/raven-chat/dateUtils.ts | 15 ++++-- vessel/src/app/page.tsx | 54 +++++++++++++++++-- .../__tests__/symbolicMomentIntent.test.ts | 6 +++ vessel/src/lib/raven/symbolicMomentIntent.ts | 5 +- 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts b/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts index aff41bdff..e0c367079 100644 --- a/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts +++ b/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts @@ -341,4 +341,15 @@ test('cross-year symbolic weather declarations include explicit years', () => { }); assert.match(context.declaration, /2025/); assert.match(context.declaration, /2026/); +}); + +test('parses tonight, this evening, this morning, and this afternoon as today', () => { + const now = new Date('2026-06-04T12:00:00Z'); + const expected = { startDate: '2026-06-04', endDate: '2026-06-04' }; + + assert.deepEqual(parseSymbolicMomentDateRange('tell me about tonight', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('tell me about...tonight.', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('how is this evening looking?', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('what is active this morning?', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('read this afternoon', now), expected); }); \ No newline at end of file diff --git a/vessel/src/app/api/raven-chat/dateUtils.ts b/vessel/src/app/api/raven-chat/dateUtils.ts index ca7ee7f72..c74f97d6a 100644 --- a/vessel/src/app/api/raven-chat/dateUtils.ts +++ b/vessel/src/app/api/raven-chat/dateUtils.ts @@ -36,6 +36,9 @@ export function parseIsoDate(value: string): string | null { probe.getUTCDate() === day; return valid ? trimmed : null; } + +const SAME_DAY_PART_PATTERN = /\b(?:this\s+)?(?:tonight|evening|morning|afternoon)\b/i; + export function parseLooseDateToken(token: string, now: Date = new Date()): string | null { const trimmed = token .trim() @@ -58,7 +61,11 @@ export function parseLooseDateToken(token: string, now: Date = new Date()): stri } const normalized = trimmed.toLowerCase().replace(/,/g, ' ').replace(/\s+/g, ' ').trim(); const today = utcStartOfDay(now); - if (normalized === 'now' || normalized === 'today') { + if ( + normalized === 'now' + || normalized === 'today' + || SAME_DAY_PART_PATTERN.test(normalized) + ) { return toIsoDate(today); } if (normalized === 'tomorrow') { @@ -84,7 +91,7 @@ function extractLooseDateFromPhrase(phrase: string, now: Date = new Date()): str ...Array.from(phrase.matchAll(/\b\d{4}-\d{2}-\d{2}\b/g)).map((entry) => entry[0]), ...Array.from(phrase.matchAll(/\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4}|\s+\d{4})?\b/ig)).map((entry) => entry[0]), ...Array.from(phrase.matchAll(/\b\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?\b/g)).map((entry) => entry[0]), - ...Array.from(phrase.matchAll(/\b(?:now|today|tomorrow)\b/ig)).map((entry) => entry[0]), + ...Array.from(phrase.matchAll(/\b(?:now|today|tonight|tomorrow|(?:this\s+)?(?:evening|morning|afternoon))\b/ig)).map((entry) => entry[0]), ]; for (const candidate of candidates) { @@ -113,7 +120,7 @@ function buildForwardRange(startDate: string, spanDays: number = 14): SymbolicMo } const DATE_PHRASE_PREFIX = - String.raw`(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4}|\s+\d{4})?|\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|today|tomorrow`; + String.raw`(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4}|\s+\d{4})?|\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|today|tonight|tomorrow|(?:this\s+)?(?:evening|morning|afternoon)`; function daysBetweenIsoDates(startDate: string, endDate: string): number | null { const start = parseIsoDate(startDate); @@ -242,7 +249,7 @@ function parseSymbolicMomentDateRangeRaw(userMessage: string, now: Date = new Da endDate: toIsoDate(utcAddDays(today, span - 1)), }; } - if (/\btoday\b/.test(normalized)) { + if (/\btoday\b/.test(normalized) || SAME_DAY_PART_PATTERN.test(normalized)) { const iso = toIsoDate(today); return { startDate: iso, endDate: iso }; } diff --git a/vessel/src/app/page.tsx b/vessel/src/app/page.tsx index 8b323e885..6ef6fe61d 100644 --- a/vessel/src/app/page.tsx +++ b/vessel/src/app/page.tsx @@ -563,6 +563,30 @@ function buildRavenThinkingStatusText(input: { return 'Reading in flight…'; } +function WheelReadingResponseGlyph({ + isVisible, + statusText, +}: { + isVisible: boolean; + statusText: string; +}) { + if (!isVisible) return null; + + return ( +