diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/bun.lock b/bun.lock index 63c0b77..522c683 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "gregorlohaus.com", "dependencies": { + "@ai-sdk/openai": "^3.0.41", + "@ai-sdk/react": "^3.0.118", "@clerk/nextjs": "^7.0.2", "@electric-sql/pglite": "^0.3.16", "@fortawesome/fontawesome-svg-core": "^7.2.0", @@ -50,6 +52,7 @@ "@trpc/server": "^11.12.0", "@uiw/react-md-editor": "^4.0.11", "@uploadthing/react": "^7.3.3", + "ai": "^6.0.116", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -59,6 +62,7 @@ "drizzle-zod": "^0.8.3", "embla-carousel-react": "^8.6.0", "glazejs": "^2.0.1", + "googleapis": "^171.4.0", "gsap": "^3.14.2", "input-otp": "^1.4.2", "lucide-react": "^0.577.0", @@ -120,6 +124,16 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], + + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], + + "@ai-sdk/react": ["@ai-sdk/react@3.0.118", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.19", "ai": "6.0.116", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-fBAix8Jftxse6/2YJnOFkwW1/O6EQK4DK68M9DlFmZGAzBmsaHXEPVS77sVIlkaOWCy11bE7434NAVXRY+3OsQ=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], @@ -538,6 +552,8 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], @@ -962,6 +978,8 @@ "@uploadthing/shared": ["@uploadthing/shared@7.1.10", "", { "dependencies": { "@uploadthing/mime-types": "0.3.6", "effect": "3.17.7", "sqids": "^0.3.0" } }, "sha512-R/XSA3SfCVnLIzFpXyGaKPfbwlYlWYSTuGjTFHuJhdAomuBuhopAHLh2Ois5fJibAHzi02uP1QCKbgTAdmArqg=="], + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="], @@ -988,6 +1006,8 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -1030,12 +1050,16 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -1048,6 +1072,8 @@ "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -1234,6 +1260,8 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "eciesjs": ["eciesjs@0.4.18", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -1364,6 +1392,10 @@ "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], + "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -1396,6 +1428,14 @@ "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + "google-auth-library": ["google-auth-library@10.6.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "7.1.3", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "googleapis": ["googleapis@171.4.0", "", { "dependencies": { "google-auth-library": "^10.2.0", "googleapis-common": "^8.0.0" } }, "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg=="], + + "googleapis-common": ["googleapis-common@8.0.1", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^7.0.0-rc.4", "google-auth-library": "^10.1.0", "qs": "^6.7.0", "url-template": "^2.0.8" } }, "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -1620,8 +1660,12 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -1636,6 +1680,10 @@ "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], @@ -2076,6 +2124,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -2086,6 +2136,8 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], @@ -2186,6 +2238,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], @@ -2202,6 +2256,8 @@ "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -2284,6 +2340,8 @@ "uploadthing": ["uploadthing@7.7.4", "", { "dependencies": { "@effect/platform": "0.90.3", "@standard-schema/spec": "1.0.0-beta.4", "@uploadthing/mime-types": "0.3.6", "@uploadthing/shared": "7.1.10", "effect": "3.17.7" }, "peerDependencies": { "express": "*", "h3": "*", "tailwindcss": "^3.0.0 || ^4.0.0-beta.0" }, "optionalPeers": ["express", "h3", "tailwindcss"] }, "sha512-rlK/4JWHW5jP30syzWGBFDDXv3WJDdT8gn9OoxRJmXLoXi94hBmyyjxihGlNrKhBc81czyv8TkzMioe/OuKGfA=="], + "url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], diff --git a/package.json b/package.json index bb65e74..e587fa5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "test": "vitest --typecheck" }, "dependencies": { + "@ai-sdk/openai": "^3.0.41", + "@ai-sdk/react": "^3.0.118", "@clerk/nextjs": "^7.0.2", "@electric-sql/pglite": "^0.3.16", "@fortawesome/fontawesome-svg-core": "^7.2.0", @@ -64,6 +66,7 @@ "@trpc/server": "^11.12.0", "@uiw/react-md-editor": "^4.0.11", "@uploadthing/react": "^7.3.3", + "ai": "^6.0.116", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -73,6 +76,7 @@ "drizzle-zod": "^0.8.3", "embla-carousel-react": "^8.6.0", "glazejs": "^2.0.1", + "googleapis": "^171.4.0", "gsap": "^3.14.2", "input-otp": "^1.4.2", "lucide-react": "^0.577.0", diff --git a/src/app/@modal/(.)assistant/_components/ChatModal.tsx b/src/app/@modal/(.)assistant/_components/ChatModal.tsx new file mode 100644 index 0000000..820100c --- /dev/null +++ b/src/app/@modal/(.)assistant/_components/ChatModal.tsx @@ -0,0 +1,31 @@ +'use client' +import { useRouter } from 'next/navigation' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog' +import ChatInterface from '~/app/chat/_components/ChatInterface' +import { useMessages } from '~/app/_providers/MessagesProvider'; +import { Spinner } from '~/components/ui/spinner'; + +export default function ChatModal() { + const router = useRouter() + const {messages,session,isLoading,error} = useMessages() + return ( + router.back()}> + + + Talk To My AI-Assistant + +
+ {!isLoading && + + } + {isLoading && + <> Loading Messages... + } + {error && +
{error}
+ } +
+
+
+ ) +} diff --git a/src/app/@modal/(.)assistant/page.tsx b/src/app/@modal/(.)assistant/page.tsx new file mode 100644 index 0000000..a0b0235 --- /dev/null +++ b/src/app/@modal/(.)assistant/page.tsx @@ -0,0 +1,8 @@ +'use client' +import ChatModal from './_components/ChatModal' + +export default function AssistantModalPage() { + return ( + + ) +} diff --git a/src/app/_components/Animated/AnimateIn.tsx b/src/app/_components/Animated/AnimateIn.tsx index aaa9c07..054f2ff 100644 --- a/src/app/_components/Animated/AnimateIn.tsx +++ b/src/app/_components/Animated/AnimateIn.tsx @@ -7,32 +7,58 @@ import { cn } from "~/lib/utils"; const AnimateTextIn = ({ children, animation = "type", - position, + position = 0, + tlId = undefined, + speed = 1, + scrollOnly = false, className }: { children: ReactNode, animation?: "type" | "slide", - position: gsap.Position, - className?:HTMLAttributes['className'] + position?: gsap.Position, + tlId?: string, + scrollOnly?: boolean, + speed?: number, + className?: HTMLAttributes['className'] }) => { const el = useRef(null) const gsapContext = useGsapContext(); useGSAP(() => { const rect = el.current?.getBoundingClientRect() - const isInView = rect && rect.top < window.innerHeight + const scroller = gsapContext?.getScroller() + console.log(scroller) + let viewportTop = 0 + let viewportBottom = window.innerHeight + if (scroller && scroller instanceof Element) { + const scrollerRect = scroller.getBoundingClientRect() + viewportTop = scrollerRect.top + viewportBottom = scrollerRect.top + scrollerRect.height + } + const isInView = rect && rect.bottom > viewportTop && rect.top < viewportBottom + console.log(isInView) const chars = new SplitText(el.current, { type: 'chars' }) - gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0) + gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0, tlId) const fromVars = animation === "slide" - ? { opacity: 0, x: -10, duration: 0.2, stagger: { each: 0.08 }, ease: 'bounce.inOut', onComplete: () => chars.revert() } - : { opacity: 0, duration: 0.01, stagger: { each: 0.04 }, ease: 'bounce.inOut', onComplete: () => chars.revert() } - if (isInView) { - gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position) + ? { opacity: 0, x: -10, duration: 0.2 * speed, stagger: { each: 0.08 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() } + : { opacity: 0, duration: 0.01 * speed, stagger: { each: 0.04 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() } + if (isInView && !scrollOnly) { + gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position, tlId) } else { - gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } }) + gsap.from(chars.chars, + { + ...fromVars, + scrollTrigger: { + trigger: el.current, + start: 'top bottom', + end: 'bottom top', + toggleActions: "play reverse play reverse", + scroller + } + }) } }, { dependencies: [] }) return ( -
+
{children}
) diff --git a/src/app/_components/ChatFAB.tsx b/src/app/_components/ChatFAB.tsx new file mode 100644 index 0000000..b080507 --- /dev/null +++ b/src/app/_components/ChatFAB.tsx @@ -0,0 +1,22 @@ +'use client' +import Link from 'next/link' +import { MessageCircle } from 'lucide-react' +import { Button } from '~/components/ui/button' +import { usePathname } from 'next/navigation' +export default function ChatFAB() { + const pathName = usePathname() + const isChat = pathName.indexOf('\/chat') > -1 + return ( + <> + {!isChat && +
+ +
+ } + + ) +} diff --git a/src/app/_components/TopNav.tsx b/src/app/_components/TopNav.tsx index 30e326a..9890f18 100644 --- a/src/app/_components/TopNav.tsx +++ b/src/app/_components/TopNav.tsx @@ -22,6 +22,11 @@ export default function TopNav() { + + +
@@ -47,7 +52,13 @@ export default function TopNav() { diff --git a/src/app/_providers/GsapProvicer.tsx b/src/app/_providers/GsapProvicer.tsx index b7c79cd..cc9f12d 100644 --- a/src/app/_providers/GsapProvicer.tsx +++ b/src/app/_providers/GsapProvicer.tsx @@ -3,7 +3,7 @@ import { useGSAP } from '@gsap/react' import gsap from 'gsap' import { SplitText } from 'gsap/SplitText' import { ScrollTrigger, GSDevTools } from 'gsap/all' -import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react' +import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, type ReactNode } from 'react' gsap.registerPlugin(useGSAP) gsap.registerPlugin(ScrollTrigger) @@ -38,7 +38,7 @@ export const useTimeLine = (dep:any,all?:boolean) => { } } },[dep]) - useEffect(() => { + useLayoutEffect(() => { return () => { gsapContext?.resetTimeline() } @@ -49,18 +49,30 @@ export default function GsapProvider({ children }: { children: ReactNode }) { const tl = useRef(null) const scrollerRef = useRef(null) const getScroller = useCallback(() => { - const cached = scrollerRef.current - if (!cached || (cached instanceof Element && !document.contains(cached))) { - scrollerRef.current = document.querySelector('[data-slot="scroll-area-viewport"]') ?? window - } + // const cached = scrollerRef.current + // if (!cached || (cached instanceof Element && !document.contains(cached))) { + let scrollers = document.querySelectorAll('[data-slot="scroll-area-viewport"]') + if (scrollers.length < 1) { + scrollerRef.current = window + } else { + let scrollerArray = Array.from(scrollers.values()).sort((a,b) => { + const s1 = a as HTMLDivElement; + const s2 = b as HTMLDivElement; + // using bitwise not (~~) to coerce NaN values to 0 + const aPriority = ~~Number(s1.dataset?.scrollerPriority) + const bPriority = ~~Number(s2.dataset?.scrollerPriority) + return aPriority - bPriority; + }) + let prioScroller = scrollerArray.pop(); + scrollerRef.current = prioScroller || window; + } + + // } return scrollerRef.current }, []) useGSAP(() => { if (!tl.current) { - tl.current = gsap.timeline({ paused: true, id:'mainTimeline', onComplete: ()=>{ - console.log('timeline revert') - gsap.getById('mainTimeline')?.revert() - } }) + tl.current = gsap.timeline({ paused: true }) } return () => { console.log("gsap cleanup") } }) diff --git a/src/app/_providers/MessagesProvider.tsx b/src/app/_providers/MessagesProvider.tsx new file mode 100644 index 0000000..d5b9cec --- /dev/null +++ b/src/app/_providers/MessagesProvider.tsx @@ -0,0 +1,95 @@ +'use client' +import type { inferRouterOutputs } from '@trpc/server'; +import { useUser } from '@clerk/nextjs' +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' +import { trpc } from '~/app/_trpc/Client' +import { type ChatRouter } from '~/server/routers/chat' +const MessageContext = createContext<{ + session?: inferRouterOutputs['getSession'] + messages?: inferRouterOutputs['getMessages'] + refetchMessages: () => void + clearChat: (callback?: () => void) => void + error: string|null + isLoading: boolean + clearingChat: boolean + clearedChat: boolean +}>({ + session: undefined, + messages: undefined, + refetchMessages: () => undefined, + clearChat: () => undefined, + error: null, + isLoading: true, + clearingChat: false, + clearedChat: false +}) +export const useMessages = () => useContext(MessageContext) +export const MessagesProvider = ({children}:{children:ReactNode}) => { + const [error,setError] = useState(null) + const [isLoading,setIsLoading] = useState(true) + const { isLoaded, isSignedIn } = useUser() + const { data: session,error:sessionError,isLoading:sessionLoading} = trpc.chat.getSession.useQuery(undefined, { + enabled: isSignedIn === true, + }) + const { data: messages, refetch, error:messageError, isLoading:messagesLoading } = trpc.chat.getMessages.useQuery(session?.id ? session.id : "", { + enabled: isSignedIn === true && session?.id != undefined, + }) + const { mutate ,isPending:clearingChat,isSuccess:clearedChat } = trpc.chat.clearChat.useMutation() + const utils = trpc.useUtils() + const refetchMessages = () => { + if (!isSignedIn) { + return; + } + utils.chat.getMessages.invalidate() + refetch() + } + const clearChat = (callback?: () => void) => { + if (!isSignedIn) { + if (callback) { + callback() + } + return; + } + mutate(undefined,{onSuccess: () => { + if (callback) { + callback() + } + utils.chat.getMessages.invalidate() + }}) + } + useEffect(() => { + if (isSignedIn !== true) { + setError(null) + return; + } + messageError && setError(messageError.message) + sessionError && setError(sessionError.message) + },[messageError,sessionError,isSignedIn]) + useEffect(() => { + if (!isLoaded) { + setIsLoading(true) + return; + } + if (isSignedIn !== true) { + setIsLoading(false) + return; + } + setIsLoading(sessionLoading || messagesLoading) + },[isLoaded,isSignedIn,sessionLoading,messagesLoading]) + return ( + + {children} + + ) +} diff --git a/src/app/actions/currentTime.ts b/src/app/actions/currentTime.ts new file mode 100644 index 0000000..9b8ae6d --- /dev/null +++ b/src/app/actions/currentTime.ts @@ -0,0 +1,8 @@ +export default function currentTime() { + let now = Date.now(); + console.log(now); + return { + success: true, + time: now + } +} diff --git a/src/app/actions/scheduleMeeting.ts b/src/app/actions/scheduleMeeting.ts new file mode 100644 index 0000000..f642083 --- /dev/null +++ b/src/app/actions/scheduleMeeting.ts @@ -0,0 +1,77 @@ +'use server' +import { clerkClient, auth } from '@clerk/nextjs/server' +import { google } from 'googleapis' +import { env } from '~/env' + +export async function scheduleMeeting({ + title, + description, + dateTime, + durationMinutes, + attendeeEmail, + attendeeName, +}: { + title: string + description: string + dateTime: string + durationMinutes: number + attendeeEmail?: string + attendeeName?: string +}) { + try { + const clerk = await clerkClient() + const userAuth = await auth() + const user = await clerk.users.getUser(userAuth.userId?userAuth.userId:"") + // Get admin's Google OAuth token to create the event on Gregor's calendar + const adminTokenResponse = await clerk.users.getUserOauthAccessToken( + env.ADMIN_USER_CLERK_ID, + 'oauth_google', + ) + const adminToken = adminTokenResponse.data[0] + + if (!adminToken?.token) { + return { success: false, error: 'Admin Google Calendar not connected. Ensure the admin account is linked with Google and has calendar scope enabled.' } + } + + // Try to resolve visitor's Google email for the invite + let visitorEmail: string | undefined = attendeeEmail + if (!visitorEmail) { + visitorEmail = user?.emailAddresses.at(0)?.emailAddress ?? undefined + } + + const oAuth2Client = new google.auth.OAuth2() + oAuth2Client.setCredentials({ access_token: adminToken.token }) + const calendar = google.calendar({ version: 'v3', auth: oAuth2Client }) + + const startTime = new Date(dateTime) + const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000) + + const attendees: { email: string; displayName?: string }[] = [] + if (visitorEmail) { + attendees.push({ email: visitorEmail, displayName: attendeeName }) + } + + const event = await calendar.events.insert({ + calendarId: 'primary', + sendUpdates: 'all', + requestBody: { + summary: title, + description, + start: { dateTime: startTime.toISOString(), timeZone: 'UTC' }, + end: { dateTime: endTime.toISOString(), timeZone: 'UTC' }, + attendees, + }, + sendNotifications: true + }) + + return { + success: true, + eventId: event.data.id, + htmlLink: event.data.htmlLink, + message: `Meeting "${title}" scheduled for ${startTime.toLocaleString()}${visitorEmail ? `. Invite sent to ${visitorEmail}.` : '.'}`, + } + } catch (error) { + console.error('Failed to schedule meeting:', error) + return { success: false, error: 'Failed to schedule meeting. Please try again.' } + } +} diff --git a/src/app/admin/_components/AdminSideBar.tsx b/src/app/admin/_components/AdminSideBar.tsx index ddca0bc..9e473a7 100644 --- a/src/app/admin/_components/AdminSideBar.tsx +++ b/src/app/admin/_components/AdminSideBar.tsx @@ -26,6 +26,9 @@ export default function AdminSideBar() { Some Blog Action + + System Prompt + diff --git a/src/app/admin/chat/_components/SystemPromptForm.tsx b/src/app/admin/chat/_components/SystemPromptForm.tsx new file mode 100644 index 0000000..f2d2f28 --- /dev/null +++ b/src/app/admin/chat/_components/SystemPromptForm.tsx @@ -0,0 +1,39 @@ +'use client' +import { useState } from 'react' +import { Textarea } from '~/components/ui/textarea' +import { Button } from '~/components/ui/button' +import { trpc } from '~/app/_trpc/Client' + +export default function SystemPromptForm({ initialValue }: { initialValue: string }) { + const [value, setValue] = useState(initialValue) + const [saved, setSaved] = useState(false) + + const mutation = trpc.chat.updateSystemPrompt.useMutation({ + onSuccess: () => setSaved(true), + }) + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setSaved(false) + mutation.mutate({ prompt: value }) + } + + return ( +
+