From 1a30f26beaedc97c94c789b28d94e1ce42330572 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Fri, 22 Mar 2024 07:50:11 -0600 Subject: [PATCH 01/24] Add title for tool tip on hover over DetailedDayForecast tiles --- src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index 328f83c..d02cdbc 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -22,6 +22,7 @@ const DetailedDayForecast: React.FC = ({ period }) => { onClick={() => { setHourlyForecastParams(hourlyParams); }} + title={`Click for ${period.name}'s hourly forecast`} >
{/* Using img here, had issues with loading using Image component */} From f20575f56767385caebd04a7d7abaa27d9d79f82 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 10:01:44 -0600 Subject: [PATCH 02/24] Disable all SW functions for testing network caching --- public/serviceWorker.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/public/serviceWorker.js b/public/serviceWorker.js index b9fc593..42bab25 100644 --- a/public/serviceWorker.js +++ b/public/serviceWorker.js @@ -1,19 +1,19 @@ -self.addEventListener("install", () => { +// self.addEventListener("install", () => { // console.log("sw installed"); -}); +// }); -self.addEventListener("activate", () => { +// self.addEventListener("activate", () => { // console.log("sw activated"); -}); +// }); // Added to trigger browser add to home screen pop-up // Changed to allow network requests in standalone -self.addEventListener('fetch', event => { - event.respondWith( - fetch(event.request) - .catch((err) => { - alert(`An error occurred with the network. - Please try your request again. ${err}`) - }) - ); -}); \ No newline at end of file +// self.addEventListener('fetch', event => { +// event.respondWith( +// fetch(event.request) +// .catch((err) => { +// alert(`An error occurred with the network. +// Please try your request again. ${err}`) +// }) +// ); +// }); \ No newline at end of file From f6e89a4a0a72caf85b95ed2fae54b22de7e793c1 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 10:23:17 -0600 Subject: [PATCH 03/24] Remove serviceWorker.js and install useEffect, was causing issues with Next caching and not needed --- public/serviceWorker.js | 19 ------------------- src/app/page.tsx | 19 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 public/serviceWorker.js diff --git a/public/serviceWorker.js b/public/serviceWorker.js deleted file mode 100644 index 42bab25..0000000 --- a/public/serviceWorker.js +++ /dev/null @@ -1,19 +0,0 @@ -// self.addEventListener("install", () => { - // console.log("sw installed"); -// }); - -// self.addEventListener("activate", () => { - // console.log("sw activated"); -// }); - -// Added to trigger browser add to home screen pop-up -// Changed to allow network requests in standalone -// self.addEventListener('fetch', event => { -// event.respondWith( -// fetch(event.request) -// .catch((err) => { -// alert(`An error occurred with the network. -// Please try your request again. ${err}`) -// }) -// ); -// }); \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index d4fd66d..b1fcf64 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -37,25 +37,6 @@ export default function Home() { }; }, [setPageLoaded]); - // Register service worker in production - useEffect(() => { - if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { - window.addEventListener("load", () => { - navigator.serviceWorker - .register("/serviceWorker.js") - .then((registration) => { - console.log( - "Service Worker registered with scope:", - registration.scope - ); - }) - .catch((error) => { - console.error("Service Worker registration failed:", error); - }); - }); - } - }, []); - // SetScreen width with throttling useEffect(() => { const setWindowWidthState = throttle(() => { From 492fd44b53240d7abecabde789dabaedb543cf13 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 10:30:07 -0600 Subject: [PATCH 04/24] Add useEffect to unregister service worker in production --- src/app/page.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/app/page.tsx b/src/app/page.tsx index b1fcf64..5e0a9ea 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -37,6 +37,29 @@ export default function Home() { }; }, [setPageLoaded]); + // Unregister service worker in production, no longer used. + // Was causing issues with caching network requests + useEffect(() => { + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .getRegistrations() + .then((registrations) => { + for (let registration of registrations) { + registration.unregister().then((res) => { + if (res === true) { + console.log("Service Worker unregistered successfully"); + } + }); + } + }) + .catch((error) => { + console.error("Service Worker unregistration failed:", error); + }); + }); + } + }, []); + // SetScreen width with throttling useEffect(() => { const setWindowWidthState = throttle(() => { From def192945ab388b6d37b7092e7201c19d19d6ed6 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 10:47:11 -0600 Subject: [PATCH 05/24] Create humidity string on end of detailed day forecast to move from header --- .../Components/DetailedDayForecast/DetailedDayForecast.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index d02cdbc..dee9edd 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -47,7 +47,11 @@ const DetailedDayForecast: React.FC = ({ period }) => { ) : null}
-

{period.detailedForecast}

+

{`${ + period.detailedForecast + } Humidity ${minRH.toLocaleString()}% to ${ + period.relativeHumidity.value + }% RH.`}

); } From d6757ed4353dde14e88fa2631f8dcd756fc0b556 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 11:18:52 -0600 Subject: [PATCH 06/24] Create initial send_score POST endpoint --- package-lock.json | 232 +++++++++++++++++++++++- package.json | 1 + src/app/api/open_ai/send_score/route.ts | 19 ++ 3 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 src/app/api/open_ai/send_score/route.ts diff --git a/package-lock.json b/package-lock.json index f7e9858..7b2b49f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "lodash": "^4.17.21", "next": "^14.0.4", "next-auth": "^4.24.5", + "openai": "^4.31.0", "react": "^18", "react-dom": "^18" }, @@ -452,6 +453,28 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/pg": { "version": "8.6.6", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.6.tgz", @@ -646,6 +669,17 @@ "node": ">=14.6" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -667,6 +701,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -972,8 +1017,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -1035,6 +1079,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1227,6 +1276,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -1336,7 +1393,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1396,6 +1452,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -1556,7 +1620,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -1570,6 +1633,15 @@ "node": ">=6" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2182,6 +2254,14 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -2437,6 +2517,31 @@ "node": ">= 0.12" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -2799,6 +2904,14 @@ "node": ">=8.12.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2958,6 +3071,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -3623,6 +3741,16 @@ "node": ">=10" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3655,7 +3783,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -3664,7 +3791,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -3705,8 +3831,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -3804,6 +3929,43 @@ } } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz", @@ -3989,6 +4151,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.31.0.tgz", + "integrity": "sha512-JebkRnRGEGLnJt3+bJ5B7au8nBeZvJjs9baVxDmUZ5+BgafAdy6KDxJGSuyaw/IA+ErqY3jmOH5cDC2mCDJF2w==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.28.tgz", + "integrity": "sha512-J5cOGD9n4x3YGgVuaND6khm5x07MMdAKkRyXnjVR6KFhLMNh2yONGiP7Z+4+tBOt5mK+GvDTiacTOVGGpqiecw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/openid-client": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", @@ -5059,6 +5248,11 @@ "node": ">= 4.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -5311,6 +5505,28 @@ "node": ">=10.13.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9962063..642022d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lodash": "^4.17.21", "next": "^14.0.4", "next-auth": "^4.24.5", + "openai": "^4.31.0", "react": "^18", "react-dom": "^18" }, diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts new file mode 100644 index 0000000..20d411e --- /dev/null +++ b/src/app/api/open_ai/send_score/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; +import OpenAI from "openai"; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +export async function POST(request: NextRequest) { + try { + const reqBody = await request.json(); + + return NextResponse.json("Successful POST to api/open_ai/send_score", { + status: 200, + }); + } catch (error) { + console.error(error); + return NextResponse.json({ error }, { status: 500 }); + } +} From 08ff2a746712f8d95f9fe1758f5144a46f60c57b Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 11:24:50 -0600 Subject: [PATCH 07/24] Create initial api call for posting forecast and retrieving send score from OpenAI --- src/app/Util/OpenAiApiCalls.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/app/Util/OpenAiApiCalls.ts diff --git a/src/app/Util/OpenAiApiCalls.ts b/src/app/Util/OpenAiApiCalls.ts new file mode 100644 index 0000000..585c809 --- /dev/null +++ b/src/app/Util/OpenAiApiCalls.ts @@ -0,0 +1,21 @@ +export async function postForecastForSendScores(aiForecastData: any) { + try { + const response = await fetch("/api/open_ai/send_score", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(aiForecastData), + credentials: "include", + }); + + if (response.ok) { + return await response.json(); + } else { + const errorData = await response.json(); + throw new Error(`Error response postNewUserLocation: ${errorData}`); + } + } catch (error) { + throw error; + } +} From 896c1a8849e49cf0d9fb3c292497cdee758d1ebf Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 11:36:16 -0600 Subject: [PATCH 08/24] Create interface for forecast data to sent to OpenAI endpoint --- src/app/Interfaces/interfaces.ts | 9 +++++++++ src/app/Util/OpenAiApiCalls.ts | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/Interfaces/interfaces.ts b/src/app/Interfaces/interfaces.ts index 5d9e80b..7f057b3 100644 --- a/src/app/Interfaces/interfaces.ts +++ b/src/app/Interfaces/interfaces.ts @@ -235,3 +235,12 @@ export interface GoogleMapPoint { lng: number; }; } + +export interface OpenAIForecastData { + sport: string; + forecastPeriods: { + name: string; + detailedForecast: string; + humidity: { min: number; max: number }; + }[]; +} diff --git a/src/app/Util/OpenAiApiCalls.ts b/src/app/Util/OpenAiApiCalls.ts index 585c809..ddfc676 100644 --- a/src/app/Util/OpenAiApiCalls.ts +++ b/src/app/Util/OpenAiApiCalls.ts @@ -1,4 +1,6 @@ -export async function postForecastForSendScores(aiForecastData: any) { +import { OpenAIForecastData } from "../Interfaces/interfaces"; + +export async function postForecastForSendScores(aiForecastData: OpenAIForecastData) { try { const response = await fetch("/api/open_ai/send_score", { method: "POST", From 8f182e44aa811f0ea1bc9a9bf5bba590e693d201 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 12:12:38 -0600 Subject: [PATCH 09/24] Create OpenAIForecastData class --- src/app/Classes/OpenAIForecastData.ts | 23 +++++++++++++++++++++++ src/app/Interfaces/interfaces.ts | 9 --------- src/app/Util/OpenAiApiCalls.ts | 6 ++++-- 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 src/app/Classes/OpenAIForecastData.ts diff --git a/src/app/Classes/OpenAIForecastData.ts b/src/app/Classes/OpenAIForecastData.ts new file mode 100644 index 0000000..5d592b3 --- /dev/null +++ b/src/app/Classes/OpenAIForecastData.ts @@ -0,0 +1,23 @@ +import { Forecast } from "./Forecast"; + +export class OpenAIForecastData { + sport: string; + forecastPeriods: { + name: string; + detailedForecast: string; + }[]; + constructor(sport: string, forecast: Forecast) { + this.sport = sport; + this.forecastPeriods = this.createForecastPeriods(forecast); + } + + createForecastPeriods(data: Forecast) { + const periods = data.periods.map((per) => { + return { + name: per.name, + detailedForecast: per.detailedForecast, + }; + }); + return periods; + } +} diff --git a/src/app/Interfaces/interfaces.ts b/src/app/Interfaces/interfaces.ts index 7f057b3..5d9e80b 100644 --- a/src/app/Interfaces/interfaces.ts +++ b/src/app/Interfaces/interfaces.ts @@ -235,12 +235,3 @@ export interface GoogleMapPoint { lng: number; }; } - -export interface OpenAIForecastData { - sport: string; - forecastPeriods: { - name: string; - detailedForecast: string; - humidity: { min: number; max: number }; - }[]; -} diff --git a/src/app/Util/OpenAiApiCalls.ts b/src/app/Util/OpenAiApiCalls.ts index ddfc676..85607fb 100644 --- a/src/app/Util/OpenAiApiCalls.ts +++ b/src/app/Util/OpenAiApiCalls.ts @@ -1,6 +1,8 @@ -import { OpenAIForecastData } from "../Interfaces/interfaces"; +import { OpenAIForecastData } from "../Classes/OpenAIForecastData"; -export async function postForecastForSendScores(aiForecastData: OpenAIForecastData) { +export async function postForecastForSendScores( + aiForecastData: OpenAIForecastData +) { try { const response = await fetch("/api/open_ai/send_score", { method: "POST", From 3e9e6cbc54e3a586fc4fe8abbd0b0f5b1cfb5329 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 12:27:54 -0600 Subject: [PATCH 10/24] Invole OpenAI api call in HomeControl --- .../Components/HomeControl/HomeControl.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index ea5ae5a..8f2904d 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -10,6 +10,8 @@ import { import { Gridpoint } from "@/app/Classes/Gridpoint"; import { Forecast } from "@/app/Classes/Forecast"; import { HourlyForecast } from "@/app/Classes/HourlyForecast"; +import { postForecastForSendScores } from "@/app/Util/OpenAiApiCalls"; +import { OpenAIForecastData } from "@/app/Classes/OpenAIForecastData"; export default function HomeControl() { const { @@ -103,6 +105,26 @@ export default function HomeControl() { forecastData, ]); + useEffect(() => { + // Add global state for AI res, with && here + if (forecastData) { + const aiForecastData = new OpenAIForecastData( + selectedLocType, + forecastData + ); + const fetchAiScores = async () => { + try { + const res = await postForecastForSendScores(aiForecastData); + console.log(res); + return res; + } catch (error) { + console.log("An error occurred fetching OpenAI send scores", error); + } + }; + fetchAiScores(); + } + }, [forecastData, locationDetails, selectedLocType]); + // Ask for user location if Current Location selected useEffect(() => { if (selectedLocType === "Current Location") { From c6232b9a8d111e6f136566261f3cfa4657a8081e Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 13:27:35 -0600 Subject: [PATCH 11/24] Create successful post to OpenAI to generate sendScores --- src/app/api/open_ai/send_score/route.ts | 54 ++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index 20d411e..e76150f 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -8,10 +8,60 @@ const openai = new OpenAI({ export async function POST(request: NextRequest) { try { const reqBody = await request.json(); + let sportString = ""; + let sportPrompt = ""; - return NextResponse.json("Successful POST to api/open_ai/send_score", { - status: 200, + switch (reqBody.sport) { + case "climb": + sportString = "rock climbing"; + sportPrompt = + "Optimal conditions for most climbers would be between 50 and 75 degrees, clear skies and low wind. Snow and rain are extremely undesirable, making it impossible to rock climb. Snowfall within a few days prior or heavy rain the day prior could leave the rock wet if not sunny and breezy."; + break; + case "mtb": + sportString = "mountain biking"; + sportPrompt = + "Optimal conditions for most mountain bikers is between 50 and 85 degrees. If it is colder, sunny skies and low wind makes it more favorable. If the temperature is above 85, cloudy skies and a light breeze are desirable"; + break; + case "ski": + sportString = "skiing"; + sportPrompt = + "Optimal conditions for most skiers is between 15 and 45 degrees with little wind and sunny skies. Factors that make the day more desirable include new snowfall forecasted the night or day before the day the user would engage in skiing. High wind and low wind chill values are the least desirable conditions for skiing, especially if it is also cloudy. Ski conditions are better the day after snow is forecasted as skiing while it is snowing can be cold and hard to see. Rain and freezing rain are very unfavorable conditions for skiing."; + } + + const aiPrompt = `Your job is to create "sendScore" value between 0 and 10 that represents how favorable it is for the user to participate in ${sportString} based on the below information and return a JSON response. The days are represented by objects in the "forecastPeriods" array. Any period that is a "Night" should be scored with a lower "sendScore" as all sports are less favorable to engage in at night. Only return a JSON response with this structure: + { + "forecastPeriods" : [ + {"name" : same as name for each period, "sendScore": number representing how favorable that period is to engage in the "sport" provided} ... return a response object for each period + ] + } + ${sportPrompt}`; + + const aiResponse = await openai.chat.completions.create({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: JSON.stringify(aiPrompt), + }, + { + role: "user", + content: JSON.stringify(reqBody.forecastPeriods), + }, + ], + temperature: 0.5, + max_tokens: 448, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, }); + + if (aiResponse?.choices[0]?.message?.content) { + return NextResponse.json(aiResponse?.choices[0]?.message?.content, { + status: 200, + }); + } else { + throw new Error("OpenAI response undefined"); + } } catch (error) { console.error(error); return NextResponse.json({ error }, { status: 500 }); From 93e6d17ed80af9a9540d28abf254c8e144e84cb8 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 13:34:23 -0600 Subject: [PATCH 12/24] Add forecastSendScores to global state --- src/app/Contexts/HomeContext.tsx | 28 ++++++++++++++++++++-------- src/app/Interfaces/interfaces.ts | 4 ++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/app/Contexts/HomeContext.tsx b/src/app/Contexts/HomeContext.tsx index ab3ca76..bd62b45 100644 --- a/src/app/Contexts/HomeContext.tsx +++ b/src/app/Contexts/HomeContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useState } from "react"; import { Coords, + ForecastSendScores, HourlyForecastParams, } from "../Interfaces/interfaces"; import { Gridpoint } from "../Classes/Gridpoint"; @@ -20,15 +21,19 @@ interface HomeContextType { React.SetStateAction >; forecastData: Forecast | undefined; - setForecastData: React.Dispatch< - React.SetStateAction - >; + setForecastData: React.Dispatch>; hourlyForecastData: HourlyForecast | undefined; setHourlyForecastData: React.Dispatch< React.SetStateAction >; hourlyForecastParams: HourlyForecastParams | undefined; - setHourlyForecastParams: React.Dispatch> + setHourlyForecastParams: React.Dispatch< + React.SetStateAction + >; + forecastSendScores: ForecastSendScores | undefined; + setForecastSendScores: React.Dispatch< + React.SetStateAction + >; screenWidth: number; setScreenWidth: React.Dispatch>; isLoading: boolean; @@ -54,6 +59,8 @@ export const HomeContext = createContext({ setHourlyForecastData: () => {}, hourlyForecastParams: undefined, setHourlyForecastParams: () => {}, + forecastSendScores: undefined, + setForecastSendScores: () => {}, screenWidth: 0, setScreenWidth: () => {}, isLoading: false, @@ -71,12 +78,15 @@ interface HomeProviderProps { export const HomeProvider: React.FC = ({ children }) => { const [currentGPSCoords, setCurrentGPSCoords] = useState(); const [selectedLocCoords, setSelectedLocCoords] = useState(""); - const [selectedLocType, setSelectedLocType] = - useState(""); + const [selectedLocType, setSelectedLocType] = useState(""); const [locationDetails, setLocationDetails] = useState(); const [forecastData, setForecastData] = useState(); - const [hourlyForecastData, setHourlyForecastData] = useState(); - const [hourlyForecastParams, setHourlyForecastParams] = useState(); + const [hourlyForecastData, setHourlyForecastData] = + useState(); + const [hourlyForecastParams, setHourlyForecastParams] = + useState(); + const [forecastSendScores, setForecastSendScores] = + useState(); const [screenWidth, setScreenWidth] = useState(0); const [isLoading, setIsLoading] = useState(false); const [pageLoaded, setPageLoaded] = useState(false); @@ -98,6 +108,8 @@ export const HomeProvider: React.FC = ({ children }) => { setHourlyForecastData, hourlyForecastParams, setHourlyForecastParams, + forecastSendScores, + setForecastSendScores, screenWidth, setScreenWidth, isLoading, diff --git a/src/app/Interfaces/interfaces.ts b/src/app/Interfaces/interfaces.ts index 5d9e80b..ab16502 100644 --- a/src/app/Interfaces/interfaces.ts +++ b/src/app/Interfaces/interfaces.ts @@ -235,3 +235,7 @@ export interface GoogleMapPoint { lng: number; }; } + +export interface ForecastSendScores { + forecastPeriods: { name: string; sendScore: number }[]; +} From f92e92060969221b0ad9a8f3afb0c94e5f2f9875 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 1 Apr 2024 13:53:24 -0600 Subject: [PATCH 13/24] Set state, clear state for forecastSendScores, fix JSON return from API --- src/app/Components/HomeControl/HomeControl.tsx | 18 ++++++++++++++---- .../LocationSelect/LocationSelect.tsx | 2 ++ src/app/Components/TypeSelect/TypeSelect.tsx | 2 ++ src/app/api/open_ai/send_score/route.ts | 4 +++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index 8f2904d..2031a7f 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -25,6 +25,8 @@ export default function HomeControl() { forecastData, setForecastData, setHourlyForecastData, + forecastSendScores, + setForecastSendScores, setIsLoading, setError, } = useContext(HomeContext); @@ -107,7 +109,7 @@ export default function HomeControl() { useEffect(() => { // Add global state for AI res, with && here - if (forecastData) { + if (forecastData && !forecastSendScores) { const aiForecastData = new OpenAIForecastData( selectedLocType, forecastData @@ -115,15 +117,23 @@ export default function HomeControl() { const fetchAiScores = async () => { try { const res = await postForecastForSendScores(aiForecastData); - console.log(res); - return res; + if (res) { + console.log(res); + setForecastSendScores(res); + } } catch (error) { console.log("An error occurred fetching OpenAI send scores", error); } }; fetchAiScores(); } - }, [forecastData, locationDetails, selectedLocType]); + }, [ + forecastData, + locationDetails, + selectedLocType, + forecastSendScores, + setForecastSendScores, + ]); // Ask for user location if Current Location selected useEffect(() => { diff --git a/src/app/Components/LocationSelect/LocationSelect.tsx b/src/app/Components/LocationSelect/LocationSelect.tsx index e0952c7..7d1a0a7 100644 --- a/src/app/Components/LocationSelect/LocationSelect.tsx +++ b/src/app/Components/LocationSelect/LocationSelect.tsx @@ -32,6 +32,7 @@ export default function LocationSelect() { setForecastData, setHourlyForecastData, setHourlyForecastParams, + setForecastSendScores, setIsLoading, setError, } = useContext(HomeContext); @@ -82,6 +83,7 @@ export default function LocationSelect() { setForecastData(undefined); setHourlyForecastData(undefined); setHourlyForecastParams(undefined); + setForecastSendScores(undefined); setSelectedLocCoords(e.target.value); // If coordinates for new selection, fetch location details diff --git a/src/app/Components/TypeSelect/TypeSelect.tsx b/src/app/Components/TypeSelect/TypeSelect.tsx index 9bc38d7..06aae4f 100644 --- a/src/app/Components/TypeSelect/TypeSelect.tsx +++ b/src/app/Components/TypeSelect/TypeSelect.tsx @@ -12,6 +12,7 @@ export default function TypeSelect() { setForecastData, setHourlyForecastData, setHourlyForecastParams, + setForecastSendScores, } = useContext(HomeContext); const { userInfo } = useContext(UserContext); @@ -23,6 +24,7 @@ export default function TypeSelect() { setForecastData(undefined); setHourlyForecastData(undefined); setHourlyForecastParams(undefined); + setForecastSendScores(undefined); setSelectedLocType(e.target.value); }; diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index e76150f..bda4ea1 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -53,10 +53,12 @@ export async function POST(request: NextRequest) { top_p: 1, frequency_penalty: 0, presence_penalty: 0, + response_format: { type: "json_object" }, }); if (aiResponse?.choices[0]?.message?.content) { - return NextResponse.json(aiResponse?.choices[0]?.message?.content, { + const content = JSON.parse(aiResponse.choices[0].message.content); + return NextResponse.json(content, { status: 200, }); } else { From 645498e1119403f1cc7110c6a5cb0cb0fe61db42 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Tue, 2 Apr 2024 05:58:38 -0600 Subject: [PATCH 14/24] Add initial display elements of AI features, adjust conditional for request to be made --- .../DetailedDayForecast/DetailedDayForecast.tsx | 8 +++++--- src/app/Components/HomeControl/HomeControl.tsx | 8 ++++---- src/app/Interfaces/interfaces.ts | 1 + src/app/api/open_ai/send_score/route.ts | 3 ++- src/app/page.tsx | 2 ++ 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index dee9edd..b1c2b8f 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -7,7 +7,7 @@ interface Props { } const DetailedDayForecast: React.FC = ({ period }) => { - const { setHourlyForecastParams, hourlyForecastData } = + const { setHourlyForecastParams, hourlyForecastData, forecastSendScores } = useContext(HomeContext); if (period && hourlyForecastData) { const hourlyParams = { @@ -16,6 +16,9 @@ const DetailedDayForecast: React.FC = ({ period }) => { end: period.endTime, }; const minRH = hourlyForecastData.getMinRHForTimePeriod(hourlyParams); + const sendScore = forecastSendScores?.forecastPeriods.find( + (score) => score.name === period.name + ); return (
= ({ period }) => {
{period.relativeHumidity ? ( <> -

Max {period.relativeHumidity.value}% RH

-

Min {minRH.toLocaleString()}% RH

+

SendScore {sendScore?.sendScore}

) : null}
diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index 2031a7f..6316109 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -108,13 +108,13 @@ export default function HomeControl() { ]); useEffect(() => { - // Add global state for AI res, with && here - if (forecastData && !forecastSendScores) { + // Fetch AI weather analysis + if (forecastData && !forecastSendScores && selectedLocType !== "other") { const aiForecastData = new OpenAIForecastData( selectedLocType, forecastData ); - const fetchAiScores = async () => { + const fetchAiWeatherAnalysis = async () => { try { const res = await postForecastForSendScores(aiForecastData); if (res) { @@ -125,7 +125,7 @@ export default function HomeControl() { console.log("An error occurred fetching OpenAI send scores", error); } }; - fetchAiScores(); + fetchAiWeatherAnalysis(); } }, [ forecastData, diff --git a/src/app/Interfaces/interfaces.ts b/src/app/Interfaces/interfaces.ts index ab16502..2c63f61 100644 --- a/src/app/Interfaces/interfaces.ts +++ b/src/app/Interfaces/interfaces.ts @@ -237,5 +237,6 @@ export interface GoogleMapPoint { } export interface ForecastSendScores { + summary: string; forecastPeriods: { name: string; sendScore: number }[]; } diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index bda4ea1..87a7d00 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -30,8 +30,9 @@ export async function POST(request: NextRequest) { const aiPrompt = `Your job is to create "sendScore" value between 0 and 10 that represents how favorable it is for the user to participate in ${sportString} based on the below information and return a JSON response. The days are represented by objects in the "forecastPeriods" array. Any period that is a "Night" should be scored with a lower "sendScore" as all sports are less favorable to engage in at night. Only return a JSON response with this structure: { + "summary": a 2-4 sentence summary, telling the user which day it would be best to engage in ${sportString} "forecastPeriods" : [ - {"name" : same as name for each period, "sendScore": number representing how favorable that period is to engage in the "sport" provided} ... return a response object for each period + {"name" : same as name for each period, "sendScore": number representing how favorable that period is to engage in ${sportString}} ... return a response object for each period ] } ${sportPrompt}`; diff --git a/src/app/page.tsx b/src/app/page.tsx index 5e0a9ea..2f15ba5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,6 +14,7 @@ export default function Home() { const { forecastData, hourlyForecastParams, + forecastSendScores, screenWidth, setScreenWidth, isLoading, @@ -134,6 +135,7 @@ export default function Home() { {hourlyForecastParams && } {forecastData && !hourlyForecastParams ? ( <> + {forecastSendScores?.summary} {!hasSeenHourlyForecast && (

Click on a day for an hourly forecast! From 915e6c861de42c6c4ebbd4d41b87953227421aae Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Tue, 2 Apr 2024 06:50:14 -0600 Subject: [PATCH 15/24] Update AI prompt and rendering on Home --- .../Components/HomeControl/HomeControl.tsx | 2 +- src/app/api/open_ai/send_score/route.ts | 19 +++++++++++-------- src/app/page.tsx | 6 +++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index 6316109..e75b829 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -118,11 +118,11 @@ export default function HomeControl() { try { const res = await postForecastForSendScores(aiForecastData); if (res) { - console.log(res); setForecastSendScores(res); } } catch (error) { console.log("An error occurred fetching OpenAI send scores", error); + // Set summary with error and make forecastPeriods null? } }; fetchAiWeatherAnalysis(); diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index 87a7d00..a1094da 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -15,28 +15,31 @@ export async function POST(request: NextRequest) { case "climb": sportString = "rock climbing"; sportPrompt = - "Optimal conditions for most climbers would be between 50 and 75 degrees, clear skies and low wind. Snow and rain are extremely undesirable, making it impossible to rock climb. Snowfall within a few days prior or heavy rain the day prior could leave the rock wet if not sunny and breezy."; + "Optimal rock climbing conditions are 50-75°F with sunny skies and and wind under 15mph. Wind gusts above 30mph are unfavorable and above 50mph should be avoided. Snow and rain are extremely undesirable, making it impossible to rock climb. Snowfall within a few days prior or heavy rain the day prior could leave the rock wet if not sunny and breezy."; break; case "mtb": sportString = "mountain biking"; sportPrompt = - "Optimal conditions for most mountain bikers is between 50 and 85 degrees. If it is colder, sunny skies and low wind makes it more favorable. If the temperature is above 85, cloudy skies and a light breeze are desirable"; + "Optimal mountain biking conditions are 50-85°F with sunny skies and wind under 15mph. Below 50°F, sunny and calm conditions are preferable. Above 85°F, seek cloudy skies with a light breeze (up to 15mph). Adjust sendScore for wind speeds above 15mph, visibility issues, and trail conditions from recent heavy rain or snow."; break; case "ski": sportString = "skiing"; sportPrompt = - "Optimal conditions for most skiers is between 15 and 45 degrees with little wind and sunny skies. Factors that make the day more desirable include new snowfall forecasted the night or day before the day the user would engage in skiing. High wind and low wind chill values are the least desirable conditions for skiing, especially if it is also cloudy. Ski conditions are better the day after snow is forecasted as skiing while it is snowing can be cold and hard to see. Rain and freezing rain are very unfavorable conditions for skiing."; + "Optimal skiing conditions are 15-45°F with sunny skies and wind speed under 20mph. New snowfall forecasted the night or day before the day the user would engage in skiing is highly desirable. High wind and/or low wind chill values are the least desirable conditions for skiing, especially if it is also cloudy. Ski conditions are better the day after snow is forecasted as skiing while it is snowing can be cold and hard to see. Rain and freezing rain are very unfavorable conditions for skiing."; } - const aiPrompt = `Your job is to create "sendScore" value between 0 and 10 that represents how favorable it is for the user to participate in ${sportString} based on the below information and return a JSON response. The days are represented by objects in the "forecastPeriods" array. Any period that is a "Night" should be scored with a lower "sendScore" as all sports are less favorable to engage in at night. Only return a JSON response with this structure: + const aiPrompt = `Your task is to compute a "sendScore" between 1 and 10 for each forecast period, reflecting the suitability for ${sportString}. Each day and night's forecast is represented by an object in the "forecastPeriods" array. All temperatures are in degrees Fahrenheit and winds in MPH. Only return a JSON response with this structure: { - "summary": a 2-4 sentence summary, telling the user which day it would be best to engage in ${sportString} - "forecastPeriods" : [ - {"name" : same as name for each period, "sendScore": number representing how favorable that period is to engage in ${sportString}} ... return a response object for each period + "summary": "A brief summary indicating the best day, and also the next best options, for ${sportString} based on the forecast periods. Do not reference sendScore values.", + "forecastPeriods": [ + {"name": "The same name as each period", "sendScore": "A score representing the suitability of that period for ${sportString}"} + ... ] } ${sportPrompt}`; + console.log(aiPrompt); + const aiResponse = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ @@ -49,7 +52,7 @@ export async function POST(request: NextRequest) { content: JSON.stringify(reqBody.forecastPeriods), }, ], - temperature: 0.5, + temperature: 1, max_tokens: 448, top_p: 1, frequency_penalty: 0, diff --git a/src/app/page.tsx b/src/app/page.tsx index 2f15ba5..dcd57e7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -135,7 +135,11 @@ export default function Home() { {hourlyForecastParams && } {forecastData && !hourlyForecastParams ? ( <> - {forecastSendScores?.summary} + {forecastSendScores?.summary ? ( +

{forecastSendScores?.summary}

+ ) : ( +

Loading analysis

+ )} {!hasSeenHourlyForecast && (

Click on a day for an hourly forecast! From 2b3b87e4159fd7465a8f2d94ba221ca56a237164 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Wed, 10 Apr 2024 10:43:07 -0600 Subject: [PATCH 16/24] Change hasSeenHourlyForecast to use localStorage instead of SS --- src/app/api/open_ai/send_score/route.ts | 4 +--- src/app/page.tsx | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index a1094da..f6b1b6b 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -38,8 +38,6 @@ export async function POST(request: NextRequest) { } ${sportPrompt}`; - console.log(aiPrompt); - const aiResponse = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ @@ -52,7 +50,7 @@ export async function POST(request: NextRequest) { content: JSON.stringify(reqBody.forecastPeriods), }, ], - temperature: 1, + temperature: 0.75, max_tokens: 448, top_p: 1, frequency_penalty: 0, diff --git a/src/app/page.tsx b/src/app/page.tsx index dcd57e7..73b6210 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -86,7 +86,7 @@ export default function Home() { // Get sessionStorage item and set state indicating if user // has seen the new hourly forecast feature useEffect(() => { - const hasSeenHourly = window.sessionStorage.getItem("hasSeenHourly"); + const hasSeenHourly = window.localStorage.getItem("hasSeenHourly"); if (!hasSeenHourly || hasSeenHourly === "false") { setHasSeenHourlyForecast(false); } else if (hasSeenHourly === "true") { @@ -98,7 +98,7 @@ export default function Home() { useEffect(() => { if (hourlyForecastParams && !hasSeenHourlyForecast) { setHasSeenHourlyForecast(true); - window.sessionStorage.setItem("hasSeenHourly", "true"); + window.localStorage.setItem("hasSeenHourly", "true"); } }, [hourlyForecastParams, hasSeenHourlyForecast]); From 3b5305dab57b2330cece62d8b84ee1d0e3532375 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Wed, 10 Apr 2024 11:04:35 -0600 Subject: [PATCH 17/24] Style sendScore features --- .../DetailedDayForecast/DetailedDayForecast.tsx | 8 +++----- src/app/home.css | 17 +++++++++++------ src/app/page.tsx | 6 +++--- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index b1c2b8f..e4f063e 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -42,11 +42,9 @@ const DetailedDayForecast: React.FC = ({ period }) => {

{period.name}

- {period.relativeHumidity ? ( - <> -

SendScore {sendScore?.sendScore}

- - ) : null} + {sendScore?.sendScore ? ( +

SendScore {sendScore.sendScore}

+ ) :

Loading...

}

{`${ diff --git a/src/app/home.css b/src/app/home.css index 7fa4c53..4a935d5 100644 --- a/src/app/home.css +++ b/src/app/home.css @@ -306,12 +306,9 @@ select { } .day-header-details { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + min-width: 20%; font-weight: 600; - font-size: 0.8rem; + font-size: 1rem; position: relative; } @@ -353,6 +350,13 @@ select { text-align: center; } +.send-score-summary { + font-size: 1.25rem; + color: #ef8354; + margin: 0.5rem 0rem; + text-align: center; +} + /* Desktop */ @media screen and (min-width: 769px) { @@ -481,7 +485,8 @@ select { } .day-header-details { - font-size: 0.9rem; + min-width: 18%; + font-size: 1.2rem; } .hourly-forecast-container { diff --git a/src/app/page.tsx b/src/app/page.tsx index 73b6210..550886d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -83,7 +83,7 @@ export default function Home() { } }, [isLoading]); - // Get sessionStorage item and set state indicating if user + // Get localStorage item and set state indicating if user // has seen the new hourly forecast feature useEffect(() => { const hasSeenHourly = window.localStorage.getItem("hasSeenHourly"); @@ -136,9 +136,9 @@ export default function Home() { {forecastData && !hourlyForecastParams ? ( <> {forecastSendScores?.summary ? ( -

{forecastSendScores?.summary}

+

{forecastSendScores?.summary}

) : ( -

Loading analysis

+

Loading analysis

)} {!hasSeenHourlyForecast && (

From 333733474a5beb9923d99a7e2888081e854f9be6 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Wed, 10 Apr 2024 11:22:11 -0600 Subject: [PATCH 18/24] Tweaks to AI system prompts --- src/app/api/open_ai/send_score/route.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index f6b1b6b..aac85d7 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -25,12 +25,12 @@ export async function POST(request: NextRequest) { case "ski": sportString = "skiing"; sportPrompt = - "Optimal skiing conditions are 15-45°F with sunny skies and wind speed under 20mph. New snowfall forecasted the night or day before the day the user would engage in skiing is highly desirable. High wind and/or low wind chill values are the least desirable conditions for skiing, especially if it is also cloudy. Ski conditions are better the day after snow is forecasted as skiing while it is snowing can be cold and hard to see. Rain and freezing rain are very unfavorable conditions for skiing."; + "Optimal skiing conditions are 15-45°F with sunny skies and wind speed under 20mph. New snowfall forecasted the night or day before, or on the day the user would engage in skiing is highly desirable. Snowfall amounts above 2 inches are most desireable and increases exponentially with higher snowfall forecasts. High wind and/or low wind chill values are the least desirable conditions for skiing, especially if it is also cloudy. Rain and freezing rain are very unfavorable conditions for skiing."; } - const aiPrompt = `Your task is to compute a "sendScore" between 1 and 10 for each forecast period, reflecting the suitability for ${sportString}. Each day and night's forecast is represented by an object in the "forecastPeriods" array. All temperatures are in degrees Fahrenheit and winds in MPH. Only return a JSON response with this structure: + const aiPrompt = `Your task is to compute a "sendScore" between 1 and 10 for each forecast period and a text summary, reflecting the suitability for ${sportString}. Each day and night's forecast is represented by an object in the "forecastPeriods" array. All temperatures are in degrees Fahrenheit and winds in MPH. Night forecast periods are less desireable to participate in ${sportString} and should be scored significantly lower. Only return a JSON response with this structure: { - "summary": "A brief summary indicating the best day, and also the next best options, for ${sportString} based on the forecast periods. Do not reference sendScore values.", + "summary": "A brief summary indicating the best day, and also the next best options, for ${sportString} based on the forecast periods. Do not reference sendScore values. It should be 1 to 3 sentences.", "forecastPeriods": [ {"name": "The same name as each period", "sendScore": "A score representing the suitability of that period for ${sportString}"} ... From 3f795c2eaafd782c671e5f8cedf11bdc24a16f14 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Wed, 10 Apr 2024 11:48:02 -0600 Subject: [PATCH 19/24] Create error display and conditional rendering for errors with AI api call --- .../DetailedDayForecast/DetailedDayForecast.tsx | 16 +++++++++++----- src/app/Components/HomeControl/HomeControl.tsx | 7 +++++-- src/app/home.css | 2 +- src/app/page.tsx | 8 +++++--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index e4f063e..9d244c5 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -7,18 +7,24 @@ interface Props { } const DetailedDayForecast: React.FC = ({ period }) => { - const { setHourlyForecastParams, hourlyForecastData, forecastSendScores } = + const { setHourlyForecastParams, hourlyForecastData, forecastSendScores, error } = useContext(HomeContext); + if (period && hourlyForecastData) { const hourlyParams = { name: period.name, start: period.startTime, end: period.endTime, }; + const minRH = hourlyForecastData.getMinRHForTimePeriod(hourlyParams); - const sendScore = forecastSendScores?.forecastPeriods.find( - (score) => score.name === period.name - ); + + let sendScore; + if (forecastSendScores?.forecastPeriods) { + sendScore = forecastSendScores?.forecastPeriods.find( + (score) => score.name === period.name + ); + } return (

= ({ period }) => {
{sendScore?.sendScore ? (

SendScore {sendScore.sendScore}

- ) :

Loading...

} + ) : !error &&

Loading...

}

{`${ diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index e75b829..61c6176 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -12,6 +12,7 @@ import { Forecast } from "@/app/Classes/Forecast"; import { HourlyForecast } from "@/app/Classes/HourlyForecast"; import { postForecastForSendScores } from "@/app/Util/OpenAiApiCalls"; import { OpenAIForecastData } from "@/app/Classes/OpenAIForecastData"; +import { ForecastSendScores } from "@/app/Interfaces/interfaces"; export default function HomeControl() { const { @@ -121,8 +122,9 @@ export default function HomeControl() { setForecastSendScores(res); } } catch (error) { - console.log("An error occurred fetching OpenAI send scores", error); - // Set summary with error and make forecastPeriods null? + console.error(error); + // Set error + setError("An error occurred while creating SendScores."); } }; fetchAiWeatherAnalysis(); @@ -133,6 +135,7 @@ export default function HomeControl() { selectedLocType, forecastSendScores, setForecastSendScores, + setError, ]); // Ask for user location if Current Location selected diff --git a/src/app/home.css b/src/app/home.css index 4a935d5..b1a4e81 100644 --- a/src/app/home.css +++ b/src/app/home.css @@ -339,7 +339,7 @@ select { font-size: 1.3rem; color: #f18f01; text-align: center; - padding: 1rem; + padding: 1rem 1rem 0rem 1rem; } .hour-forecast-tip { diff --git a/src/app/page.tsx b/src/app/page.tsx index 550886d..5c7e319 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -136,11 +136,13 @@ export default function Home() { {forecastData && !hourlyForecastParams ? ( <> {forecastSendScores?.summary ? ( -

{forecastSendScores?.summary}

+

+ {forecastSendScores?.summary} +

) : ( -

Loading analysis

+ !error &&

Loading SendScore™ analysis...

)} - {!hasSeenHourlyForecast && ( + {!hasSeenHourlyForecast && !error && (

Click on a day for an hourly forecast!

From 3cd003d6e61f0331016ba10853b900ca4097a949 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Wed, 10 Apr 2024 12:21:33 -0600 Subject: [PATCH 20/24] Styling changes to SendScore --- .../DetailedDayForecast/DetailedDayForecast.tsx | 15 +++++++++++---- src/app/Components/HomeControl/HomeControl.tsx | 1 - src/app/api/open_ai/send_score/route.ts | 2 +- src/app/home.css | 11 ++++++++--- src/app/page.tsx | 8 +++++++- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index 9d244c5..e06431e 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -7,8 +7,13 @@ interface Props { } const DetailedDayForecast: React.FC = ({ period }) => { - const { setHourlyForecastParams, hourlyForecastData, forecastSendScores, error } = - useContext(HomeContext); + const { + selectedLocType, + setHourlyForecastParams, + hourlyForecastData, + forecastSendScores, + error, + } = useContext(HomeContext); if (period && hourlyForecastData) { const hourlyParams = { @@ -49,8 +54,10 @@ const DetailedDayForecast: React.FC = ({ period }) => {

{period.name}

{sendScore?.sendScore ? ( -

SendScore {sendScore.sendScore}

- ) : !error &&

Loading...

} +

SendScore: {sendScore.sendScore}

+ ) : ( + !error && selectedLocType !== "other" &&

Loading...

+ )}

{`${ diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index 61c6176..321e604 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -123,7 +123,6 @@ export default function HomeControl() { } } catch (error) { console.error(error); - // Set error setError("An error occurred while creating SendScores."); } }; diff --git a/src/app/api/open_ai/send_score/route.ts b/src/app/api/open_ai/send_score/route.ts index aac85d7..aa59458 100644 --- a/src/app/api/open_ai/send_score/route.ts +++ b/src/app/api/open_ai/send_score/route.ts @@ -30,7 +30,7 @@ export async function POST(request: NextRequest) { const aiPrompt = `Your task is to compute a "sendScore" between 1 and 10 for each forecast period and a text summary, reflecting the suitability for ${sportString}. Each day and night's forecast is represented by an object in the "forecastPeriods" array. All temperatures are in degrees Fahrenheit and winds in MPH. Night forecast periods are less desireable to participate in ${sportString} and should be scored significantly lower. Only return a JSON response with this structure: { - "summary": "A brief summary indicating the best day, and also the next best options, for ${sportString} based on the forecast periods. Do not reference sendScore values. It should be 1 to 3 sentences.", + "summary": "A brief summary indicating the best day, and also the next best options, for ${sportString} at the user's selected location based on the forecast periods. Do not reference sendScore values. It should be 1 to 3 sentences and does not need to explain that nights are less favorable.", "forecastPeriods": [ {"name": "The same name as each period", "sendScore": "A score representing the suitability of that period for ${sportString}"} ... diff --git a/src/app/home.css b/src/app/home.css index b1a4e81..c3242c3 100644 --- a/src/app/home.css +++ b/src/app/home.css @@ -306,10 +306,11 @@ select { } .day-header-details { - min-width: 20%; + min-width: 67px; font-weight: 600; font-size: 1rem; position: relative; + text-align: right; } .home-welcome-header { @@ -352,7 +353,7 @@ select { .send-score-summary { font-size: 1.25rem; - color: #ef8354; + color: #00b2ff; margin: 0.5rem 0rem; text-align: center; } @@ -458,6 +459,10 @@ select { .forecast-section { max-width: 1500px; } + + .send-score-summary { + margin: 0.5rem 1rem; + } .day-forecast-container { display: grid; @@ -485,7 +490,7 @@ select { } .day-header-details { - min-width: 18%; + min-width: 5.542rem; font-size: 1.2rem; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 5c7e319..6785113 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,6 +12,7 @@ import HourlyForecastContainer from "./Components/HourlyForecastContainer/Hourly export default function Home() { const { + selectedLocType, forecastData, hourlyForecastParams, forecastSendScores, @@ -140,7 +141,12 @@ export default function Home() { {forecastSendScores?.summary}

) : ( - !error &&

Loading SendScore™ analysis...

+ !error && + selectedLocType !== "other" && ( +

+ Loading SendScore™ analysis... +

+ ) )} {!hasSeenHourlyForecast && !error && (

From 4af83ec5c41ef29dfc89ca94af1d3f22cad2cb40 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Wed, 10 Apr 2024 13:41:59 -0600 Subject: [PATCH 21/24] Update test specs and create new tests for AI SendScores --- cypress/e2e/dailyForecast.cy.ts | 28 ++++++++---- cypress/e2e/forecastFetchErrors.cy.ts | 41 ++++++++++++++++++ cypress/e2e/hourlyForecast.cy.ts | 3 ++ cypress/fixtures/sendscore.json | 61 +++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 cypress/fixtures/sendscore.json diff --git a/cypress/e2e/dailyForecast.cy.ts b/cypress/e2e/dailyForecast.cy.ts index 24ec9d2..3331d92 100644 --- a/cypress/e2e/dailyForecast.cy.ts +++ b/cypress/e2e/dailyForecast.cy.ts @@ -26,6 +26,9 @@ describe("daily forecast display", () => { } ); + // Intercept OpenAI AI call + cy.intercept("/api/open_ai/send_score", { fixture: "sendscore.json" }); + cy.visit("/"); // Select Climbing in TypeSelect @@ -44,19 +47,26 @@ describe("daily forecast display", () => { .find("p.day-forecast-text") .should( "have.text", - "Sunny. High near 57, with temperatures falling to around 52 in the afternoon. West wind 30 to 36 mph, with gusts as high as 54 mph." + "Sunny. High near 57, with temperatures falling to around 52 in the afternoon. West wind 30 to 36 mph, with gusts as high as 54 mph. Humidity 17% to 19% RH." ); }); - it("should display the humidity details if available", () => { - cy.get("@todayForecast") - .find("div.day-header-details>p") - .eq(0) - .should("have.text", "Max 19% RH"); + it("should display a summary from AI data", () => { + cy.get("p.send-score-summary").should( + "have.text", + "Thursday is the best day for rock climbing with sunny skies, a high near 51°F, and light west winds. Friday is also a good option with a high near 61°F and light southwest winds. Saturday could work as well with a high near 59°F and mostly clear skies." + ); + }); - cy.get("@todayForecast") - .find("div.day-header-details>p") + it("should display the SendScore on forecast tiles", () => { + cy.get("article.detailed-day-forecast") .eq(1) - .should("have.text", "Min 17% RH"); + .as("tonightForecast") + .find("div.day-header-details>p") + .should("have.text", "SendScore: 1"); + }); + + it("should display a tip to click on forecast tiles for hourly forecasts", () => { + cy.get("p.hour-forecast-tip").should("be.visible"); }); }); diff --git a/cypress/e2e/forecastFetchErrors.cy.ts b/cypress/e2e/forecastFetchErrors.cy.ts index c5900ee..acda74b 100644 --- a/cypress/e2e/forecastFetchErrors.cy.ts +++ b/cypress/e2e/forecastFetchErrors.cy.ts @@ -61,6 +61,9 @@ describe("daily forecast display errors", () => { } ); + // Intercept OpenAI AI call + cy.intercept("/api/open_ai/send_score", { fixture: "sendscore.json" }); + cy.wait(10000); cy.get("div.loading-msg-div") @@ -95,6 +98,9 @@ describe("daily forecast display errors", () => { } ); + // Intercept OpenAI AI call + cy.intercept("/api/open_ai/send_score", { fixture: "sendscore.json" }); + cy.wait(10000); cy.get("div.loading-msg-div") @@ -104,4 +110,39 @@ describe("daily forecast display errors", () => { "Oh, no! All hourly forecast fetch attempts failed. Please reload the page and try again." ); }); + + it("should display an error when the OpenAI fetch fails", () => { + // Intercept Climbing - Lower Boulder Canyon fetchNoaaGridLocation call + cy.intercept("https://api.weather.gov/points/40.004482,-105.355800", { + fixture: "location_details.json", + }); + + // Intercept Climbing - Lower Boulder Canyon redirected fetchNoaaGridLocation call + cy.intercept("https://api.weather.gov/points/40.0045,-105.3558", { + fixture: "location_details.json", + }); + + // Intercept Lower Boulder Canyon Detailed daily forecast + cy.intercept("https://api.weather.gov/gridpoints/BOU/51,74/forecast", { + fixture: "detailed_forecast.json", + }); + + // Intercept Lower Boulder Canyon hourly forecast + cy.intercept( + "https://api.weather.gov/gridpoints/BOU/51,74/forecast/hourly", + { + fixture: "hourly_forecast.json", + } + ); + + // Intercept OpenAI AI call + cy.intercept("/api/open_ai/send_score", { statusCode: 500 }); + + cy.get("div.loading-msg-div") + .find("p.error-msg") + .should( + "have.text", + "Oh, no! An error occurred while creating SendScores." + ); + }); }); diff --git a/cypress/e2e/hourlyForecast.cy.ts b/cypress/e2e/hourlyForecast.cy.ts index 7aabaf4..d28ba08 100644 --- a/cypress/e2e/hourlyForecast.cy.ts +++ b/cypress/e2e/hourlyForecast.cy.ts @@ -26,6 +26,9 @@ describe("hourly forecast display", () => { } ); + // Intercept OpenAI AI call + cy.intercept("/api/open_ai/send_score", { fixture: "sendscore.json" }); + cy.visit("/"); // Select Climbing in TypeSelect diff --git a/cypress/fixtures/sendscore.json b/cypress/fixtures/sendscore.json new file mode 100644 index 0000000..9b40a65 --- /dev/null +++ b/cypress/fixtures/sendscore.json @@ -0,0 +1,61 @@ +{ + "summary": "Thursday is the best day for rock climbing with sunny skies, a high near 51°F, and light west winds. Friday is also a good option with a high near 61°F and light southwest winds. Saturday could work as well with a high near 59°F and mostly clear skies.", + "forecastPeriods": [ + { + "name": "This Afternoon", + "sendScore": 3 + }, + { + "name": "Tonight", + "sendScore": 1 + }, + { + "name": "Thursday", + "sendScore": 8 + }, + { + "name": "Thursday Night", + "sendScore": 2 + }, + { + "name": "Friday", + "sendScore": 7 + }, + { + "name": "Friday Night", + "sendScore": 3 + }, + { + "name": "Saturday", + "sendScore": 6 + }, + { + "name": "Saturday Night", + "sendScore": 2 + }, + { + "name": "Sunday", + "sendScore": 4 + }, + { + "name": "Sunday Night", + "sendScore": 1 + }, + { + "name": "Monday", + "sendScore": 3 + }, + { + "name": "Monday Night", + "sendScore": 1 + }, + { + "name": "Tuesday", + "sendScore": 3 + }, + { + "name": "Tuesday Night", + "sendScore": 1 + } + ] +} From 59f975962fe7cfe89f6c1ce32d9b54917e7a23c4 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 15 Apr 2024 06:54:43 -0600 Subject: [PATCH 22/24] Update readme --- README.md | 2 +- .../DetailedDayForecast.tsx | 18 ++++++------------ src/app/Components/HomeControl/HomeControl.tsx | 1 - 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3e2d165..c131db5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ SendTemps is a full-stack Next.js 14 application designed for Colorado Front Ran - **Seamless User Account Management:** Integrates Google OAuth and NextAuth for a secure, streamlined sign-in experience with persistent sessions, enabling easy access to location customization features. - **Interactive Mapping:** Features a point-and-click Google Maps interface for easy creation of backcountry locations, with an auto-updating, user-friendly UI. - **Robust Location Management:** Employs a Vercel PostgreSQL database for durable storage of custom locations, supporting full CRUD operations through Next.js APIs for dynamic user interaction. +- **AI-Powered Recommendations:** Using prompt engineering, integrated AI to analyze forecasts and recommend optimal days to engage in the sports that the user selected location is associated with. - **Optimized User Experience:** Implements intelligent error handling and automatic retry mechanisms for NOAA forecast API requests, ensuring smooth and informative user interactions. - **Standalone Display:** Designed for optimal performance on mobile devices, with capabilities for home screen addition, paving the way for full Progressive Web App functionality in the future. - **Hourly Forecast Display:** New as of 2.26.24 - Provides detailed hourly weather forecasts for more precise activity planning. @@ -26,7 +27,6 @@ SendTemps is a full-stack Next.js 14 application designed for Colorado Front Ran - **Multiple Sport Association:** Enable users to associate multiple sports with a single location for enhanced flexibility. - **Customizable Default Views:** Allow users to personalize the locations around the Front Range that are loaded by default. This would allow a user to further personalize the application, increasing engagement by reducing irrelevant information. - **Enhanced Search Functionality:** Implement text-based search with autocomplete to streamline location finding. This would be especially useful if a user has many locations and the current select inputs become too cumbersome. -- **AI-Powered Recommendations:** Using prompt engineering, integrate AI to analyze forecasts and recommend optimal days to engage in the sports that the location is associated with. ## Technical Challenges - Google OAuth / NextAuth diff --git a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx index e06431e..99ac12f 100644 --- a/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx +++ b/src/app/Components/DetailedDayForecast/DetailedDayForecast.tsx @@ -8,11 +8,9 @@ interface Props { const DetailedDayForecast: React.FC = ({ period }) => { const { - selectedLocType, setHourlyForecastParams, hourlyForecastData, forecastSendScores, - error, } = useContext(HomeContext); if (period && hourlyForecastData) { @@ -24,12 +22,10 @@ const DetailedDayForecast: React.FC = ({ period }) => { const minRH = hourlyForecastData.getMinRHForTimePeriod(hourlyParams); - let sendScore; - if (forecastSendScores?.forecastPeriods) { - sendScore = forecastSendScores?.forecastPeriods.find( - (score) => score.name === period.name - ); - } + const sendScoreData = forecastSendScores?.forecastPeriods.find( + (score) => score.name === period.name + ); + return (

= ({ period }) => {

{period.name}

- {sendScore?.sendScore ? ( -

SendScore: {sendScore.sendScore}

- ) : ( - !error && selectedLocType !== "other" &&

Loading...

+ {sendScoreData?.sendScore && ( +

SendScore: {sendScoreData.sendScore}

)}
diff --git a/src/app/Components/HomeControl/HomeControl.tsx b/src/app/Components/HomeControl/HomeControl.tsx index 321e604..1939df1 100644 --- a/src/app/Components/HomeControl/HomeControl.tsx +++ b/src/app/Components/HomeControl/HomeControl.tsx @@ -12,7 +12,6 @@ import { Forecast } from "@/app/Classes/Forecast"; import { HourlyForecast } from "@/app/Classes/HourlyForecast"; import { postForecastForSendScores } from "@/app/Util/OpenAiApiCalls"; import { OpenAIForecastData } from "@/app/Classes/OpenAIForecastData"; -import { ForecastSendScores } from "@/app/Interfaces/interfaces"; export default function HomeControl() { const { From 536f197ca8d84867a1f8646300bba17be22030b2 Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 15 Apr 2024 07:17:30 -0600 Subject: [PATCH 23/24] Add OpenAI API key to secrets on yaml --- .github/workflows/main.yml | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85534ae..67ef706 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,3 +65,4 @@ jobs: POSTGRES_URL: ${{ secrets.POSTGRES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/README.md b/README.md index c131db5..f8dfaad 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ SendTemps is a full-stack Next.js 14 application designed for Colorado Front Ran - Google OAuth - NextAuth - Google Maps API +- OpenAI API - Vercel Storage - Cypress E2E Testing From c0d7b02d7e7c5dc4269b6caf63c56ca6682b254e Mon Sep 17 00:00:00 2001 From: Rick Vermeil Date: Mon, 15 Apr 2024 07:30:50 -0600 Subject: [PATCH 24/24] Update yaml with env var --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 67ef706..a5149ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,8 @@ jobs: with: runTests: false build: npm run build + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Save .next folder uses: actions/upload-artifact@v4