Compare commits
20 Commits
musicpage
...
c5b3ee3875
| Author | SHA1 | Date | |
|---|---|---|---|
| c5b3ee3875 | |||
| 2b5c105abb | |||
| e25fc39bac | |||
| e481fa66cd | |||
| 009d2b8d60 | |||
| d567fa3e02 | |||
| 399d78e508 | |||
| bfc2bb1501 | |||
| d7a9e53d9a | |||
| 18abc4b3f7 | |||
| 34dc53a8e9 | |||
| 10b3f989c8 | |||
| 348ed790e2 | |||
| c7de58a4b8 | |||
| a51b313aba | |||
| 363a91dd7d | |||
| dfaba3a24e | |||
| 9c5aec01e0 | |||
| 03399de14f | |||
| 9b48661a6a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ yarn-error.log*
|
|||||||
# clerk configuration (can include secrets)
|
# clerk configuration (can include secrets)
|
||||||
/.clerk/
|
/.clerk/
|
||||||
.worktrees
|
.worktrees
|
||||||
|
.claudesession
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -1,29 +1,13 @@
|
|||||||
# Create T3 App
|
# My Personal Website
|
||||||
|
|
||||||
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
|
## Using:
|
||||||
|
|
||||||
## What's next? How do I make an app with this?
|
- nextjs
|
||||||
|
- trpc
|
||||||
|
- neon
|
||||||
|
- uploadthing
|
||||||
|
- drizzle
|
||||||
|
- gsap
|
||||||
|
- openai
|
||||||
|
|
||||||
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
|
|
||||||
|
|
||||||
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
|
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org)
|
|
||||||
- [NextAuth.js](https://next-auth.js.org)
|
|
||||||
- [Prisma](https://prisma.io)
|
|
||||||
- [Drizzle](https://orm.drizzle.team)
|
|
||||||
- [Tailwind CSS](https://tailwindcss.com)
|
|
||||||
- [tRPC](https://trpc.io)
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
|
|
||||||
|
|
||||||
- [Documentation](https://create.t3.gg/)
|
|
||||||
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
|
|
||||||
|
|
||||||
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## How do I deploy this?
|
|
||||||
|
|
||||||
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
|
|
||||||
|
|||||||
58
bun.lock
58
bun.lock
@@ -5,6 +5,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "gregorlohaus.com",
|
"name": "gregorlohaus.com",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/openai": "^3.0.41",
|
||||||
|
"@ai-sdk/react": "^3.0.118",
|
||||||
"@clerk/nextjs": "^7.0.2",
|
"@clerk/nextjs": "^7.0.2",
|
||||||
"@electric-sql/pglite": "^0.3.16",
|
"@electric-sql/pglite": "^0.3.16",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
"@trpc/server": "^11.12.0",
|
"@trpc/server": "^11.12.0",
|
||||||
"@uiw/react-md-editor": "^4.0.11",
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
"@uploadthing/react": "^7.3.3",
|
"@uploadthing/react": "^7.3.3",
|
||||||
|
"ai": "^6.0.116",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -59,6 +62,7 @@
|
|||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"glazejs": "^2.0.1",
|
"glazejs": "^2.0.1",
|
||||||
|
"googleapis": "^171.4.0",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.577.0",
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"@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/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=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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": ["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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
"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=="],
|
"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=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"test": "vitest --typecheck"
|
"test": "vitest --typecheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/openai": "^3.0.41",
|
||||||
|
"@ai-sdk/react": "^3.0.118",
|
||||||
"@clerk/nextjs": "^7.0.2",
|
"@clerk/nextjs": "^7.0.2",
|
||||||
"@electric-sql/pglite": "^0.3.16",
|
"@electric-sql/pglite": "^0.3.16",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
"@trpc/server": "^11.12.0",
|
"@trpc/server": "^11.12.0",
|
||||||
"@uiw/react-md-editor": "^4.0.11",
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
"@uploadthing/react": "^7.3.3",
|
"@uploadthing/react": "^7.3.3",
|
||||||
|
"ai": "^6.0.116",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"glazejs": "^2.0.1",
|
"glazejs": "^2.0.1",
|
||||||
|
"googleapis": "^171.4.0",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
|||||||
32
src/app/@modal/(.)assistant/_components/ChatModal.tsx
Normal file
32
src/app/@modal/(.)assistant/_components/ChatModal.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
'use client'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
|
||||||
|
import ChatInterface from '~/app/chat/_components/ChatInterface'
|
||||||
|
|
||||||
|
type DBMessage = {
|
||||||
|
id: string
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatModalProps {
|
||||||
|
sessionId?: string
|
||||||
|
// initialMessages: DBMessage[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatModal({ sessionId }: ChatModalProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog modal={true} open onOpenChange={() => router.back()}>
|
||||||
|
<DialogContent className="w-full max-w-full rounded-none sm:max-w-full h-[100svh] lg:max-w-3xl lg:rounded-xl lg:h-[80vh] flex flex-col p-0 gap-0">
|
||||||
|
<DialogHeader className="p-4 border-b shrink-0">
|
||||||
|
<DialogTitle>Talk To My AI-Assistant</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-hidden min-h-0">
|
||||||
|
<ChatInterface sessionId={sessionId} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
src/app/@modal/(.)assistant/page.tsx
Normal file
16
src/app/@modal/(.)assistant/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
import { Skeleton } from '~/components/ui/skeleton';
|
||||||
|
import ChatModal from './_components/ChatModal'
|
||||||
|
import { trpc } from '~/app/_trpc/Client'
|
||||||
|
import { useTimeLine } from '~/app/_providers/GsapProvicer';
|
||||||
|
|
||||||
|
export default function AssistantModalPage() {
|
||||||
|
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
&& <ChatModal sessionId={session?.id} />
|
||||||
|
{error && <div>{error.message}</div>}
|
||||||
|
{isLoading && <Skeleton />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,27 +1,64 @@
|
|||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
import { useRef, type ReactNode } from "react";
|
import { useRef, type HTMLAttributes, type ReactNode } from "react";
|
||||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||||
import { SplitText } from "gsap/SplitText";
|
import { SplitText } from "gsap/SplitText";
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,animation?:"type"|"slide",position:gsap.Position}) => {
|
import { cn } from "~/lib/utils";
|
||||||
|
const AnimateTextIn = ({
|
||||||
|
children,
|
||||||
|
animation = "type",
|
||||||
|
position = 0,
|
||||||
|
tlId = undefined,
|
||||||
|
speed = 1,
|
||||||
|
scrollOnly = false,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: ReactNode,
|
||||||
|
animation?: "type" | "slide",
|
||||||
|
position?: gsap.Position,
|
||||||
|
tlId?: string,
|
||||||
|
scrollOnly?: boolean,
|
||||||
|
speed?: number,
|
||||||
|
className?: HTMLAttributes<HTMLDivElement>['className']
|
||||||
|
}) => {
|
||||||
const el = useRef<HTMLDivElement>(null)
|
const el = useRef<HTMLDivElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
const rect = el.current?.getBoundingClientRect()
|
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' })
|
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"
|
const fromVars = animation === "slide"
|
||||||
? {opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut'}
|
? { opacity: 0, x: -10, duration: 0.2 * speed, stagger: { each: 0.08 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||||
: {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'}
|
: { opacity: 0, duration: 0.01 * speed, stagger: { each: 0.04 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||||
if (isInView) {
|
if (isInView && !scrollOnly) {
|
||||||
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position)
|
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position, tlId)
|
||||||
} else {
|
} 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: [] })
|
}, { dependencies: [] })
|
||||||
return (
|
return (
|
||||||
<div ref={el} className="opacity-0">
|
<div ref={el} className={cn(className, "opacity-0")}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
22
src/app/_components/Animated/AnimatePopUp.tsx
Normal file
22
src/app/_components/Animated/AnimatePopUp.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { type HTMLAttributes, type ReactNode } from "react";
|
||||||
|
import AnimatedDiv from "./AnimatedDiv";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
const AnimatePopUp = ({
|
||||||
|
children,
|
||||||
|
position,
|
||||||
|
className,
|
||||||
|
duration=1,
|
||||||
|
ease='elastic'
|
||||||
|
}:{
|
||||||
|
children:ReactNode
|
||||||
|
position:gsap.Position,
|
||||||
|
className?:HTMLAttributes<HTMLDivElement>['className']
|
||||||
|
duration?:number,
|
||||||
|
ease?:gsap.EaseString|gsap.EaseFunction
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AnimatedDiv children={children} position={position} className={cn(className,'h-0 translate-y-[50] overflow-hidden')} height='auto' y={0} overflow='' ease={ease} duration={duration} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimatePopUp;
|
||||||
41
src/app/_components/Animated/AnimatedDiv.tsx
Normal file
41
src/app/_components/Animated/AnimatedDiv.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import gsap from "gsap";
|
||||||
|
import { type HTMLAttributes,
|
||||||
|
type ReactNode, useLayoutEffect, useRef } from "react";
|
||||||
|
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||||
|
const AnimatedDiv = (
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
position,
|
||||||
|
className,
|
||||||
|
animationMode='to',
|
||||||
|
...tweenVars
|
||||||
|
}:
|
||||||
|
gsap.TweenVars & {
|
||||||
|
children:ReactNode,
|
||||||
|
position:gsap.Position,
|
||||||
|
animationMode?: 'from'|'to',
|
||||||
|
className?:HTMLAttributes<HTMLDivElement>['className']
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const div = useRef<HTMLDivElement>(null);
|
||||||
|
const gsapContext = useGsapContext()
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let tween:gsap.core.Tween;
|
||||||
|
switch(animationMode) {
|
||||||
|
case 'from':
|
||||||
|
tween = gsap.from(div.current,tweenVars);
|
||||||
|
break;
|
||||||
|
case 'to':
|
||||||
|
tween = gsap.to(div.current,tweenVars);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
gsapContext?.addAnimation(tween,position)
|
||||||
|
},[])
|
||||||
|
return (
|
||||||
|
<div ref={div} className={className}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimatedDiv;
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef } from "react";
|
import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef,type ReactNode } from "react";
|
||||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||||
import { SplitText } from "gsap/SplitText";
|
import { SplitText } from "gsap/SplitText";
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
const AnimatedPageTitle = (
|
const AnimatedPageTitle = (
|
||||||
{ text, position }: { text: string, position:gsap.Position }
|
{ children, position }: { children: ReactNode, position:gsap.Position }
|
||||||
) => {
|
) => {
|
||||||
const el = useRef<HTMLHeadingElement>(null)
|
const el = useRef<HTMLHeadingElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
console.log("add animated title with:",position)
|
const split = new SplitText(el.current, { type: "lines,chars", autoSplit:true })
|
||||||
const split = new SplitText(el.current, { type: "chars" })
|
|
||||||
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
|
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
|
||||||
gsapContext?.addAnimation(gsap.from(split.chars, {
|
gsapContext?.addAnimation(gsap.from(split.chars, { id: 'titlesplit',
|
||||||
stagger: 0.05, rotate: -90, opacity: 0, x: -10
|
stagger: 0.05, rotate: -90, opacity: 0, x: -10, onComplete: () => {split.revert()}
|
||||||
}),'>')
|
}),'>')
|
||||||
},[])
|
},[])
|
||||||
return (
|
return (
|
||||||
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1>
|
<h1 className="text-4xl break-keep opacity-0 font-bold text-balance w-full" ref={el}> {children} </h1>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
src/app/_components/ChatFAB.tsx
Normal file
25
src/app/_components/ChatFAB.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { MessageCircle } from 'lucide-react'
|
||||||
|
import { Show } from '@clerk/nextjs'
|
||||||
|
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 &&
|
||||||
|
<Show when="signed-in">
|
||||||
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
|
<Button asChild size="icon" className="h-14 w-14 rounded-full shadow-lg">
|
||||||
|
<Link href="/assistant">
|
||||||
|
<MessageCircle className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import { ThemeSwitch } from "./ThemeSwitch"
|
|||||||
|
|
||||||
export default function TopNav() {
|
export default function TopNav() {
|
||||||
return (
|
return (
|
||||||
<div className="fixed lg:w-full right-0 z-50 lg:bg-background">
|
<div className="fixed backdrop-blur-md lg:w-full right-0 z-50">
|
||||||
<nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full">
|
<nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full">
|
||||||
<div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row">
|
<div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row">
|
||||||
<Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline">
|
<Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline">
|
||||||
@@ -22,6 +22,11 @@ export default function TopNav() {
|
|||||||
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||||
<Link href={"/music"}> Music </Link>
|
<Link href={"/music"}> Music </Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Show when="signed-in">
|
||||||
|
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||||
|
<Link href="/chat"> Chat </Link>
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col-reverse flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row lg:ml-auto">
|
<div className="flex flex-col-reverse flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row lg:ml-auto">
|
||||||
<AdminWrap>
|
<AdminWrap>
|
||||||
@@ -47,7 +52,13 @@ export default function TopNav() {
|
|||||||
<Show when="signed-in">
|
<Show when="signed-in">
|
||||||
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
|
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
|
||||||
<div>
|
<div>
|
||||||
<UserButton />
|
<UserButton
|
||||||
|
userProfileProps={{
|
||||||
|
additionalOAuthScopes: {
|
||||||
|
google: ['https://www.googleapis.com/auth/calendar'],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
import { useGSAP } from '@gsap/react'
|
import { useGSAP } from '@gsap/react'
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
import { SplitText } from 'gsap/SplitText'
|
import { SplitText } from 'gsap/SplitText'
|
||||||
import { ScrollTrigger } from 'gsap/all'
|
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(useGSAP)
|
||||||
gsap.registerPlugin(ScrollTrigger)
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
gsap.registerPlugin(SplitText)
|
gsap.registerPlugin(SplitText)
|
||||||
|
gsap.registerPlugin(GSDevTools)
|
||||||
const GsapContext = createContext<{
|
const GsapContext = createContext<{
|
||||||
addAnimation: (
|
addAnimation: (
|
||||||
animation: gsap.core.TimelineChild,
|
animation: gsap.core.TimelineChild,
|
||||||
@@ -37,7 +38,7 @@ export const useTimeLine = (dep:any,all?:boolean) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},[dep])
|
},[dep])
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
gsapContext?.resetTimeline()
|
gsapContext?.resetTimeline()
|
||||||
}
|
}
|
||||||
@@ -48,10 +49,25 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
|||||||
const tl = useRef<gsap.core.Timeline | null>(null)
|
const tl = useRef<gsap.core.Timeline | null>(null)
|
||||||
const scrollerRef = useRef<Element | Window | null>(null)
|
const scrollerRef = useRef<Element | Window | null>(null)
|
||||||
const getScroller = useCallback(() => {
|
const getScroller = useCallback(() => {
|
||||||
const cached = scrollerRef.current
|
// const cached = scrollerRef.current
|
||||||
if (!cached || (cached instanceof Element && !document.contains(cached))) {
|
// if (!cached || (cached instanceof Element && !document.contains(cached))) {
|
||||||
scrollerRef.current = document.querySelector('[data-slot="scroll-area-viewport"]') ?? window
|
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
|
return scrollerRef.current
|
||||||
}, [])
|
}, [])
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ import { env } from "~/env"
|
|||||||
|
|
||||||
export async function isAdmin() {
|
export async function isAdmin() {
|
||||||
const userid = (await auth()).userId
|
const userid = (await auth()).userId
|
||||||
|
console.log(userid)
|
||||||
return (userid == env.ADMIN_USER_CLERK_ID)
|
return (userid == env.ADMIN_USER_CLERK_ID)
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/app/actions/scheduleMeeting.ts
Normal file
86
src/app/actions/scheduleMeeting.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
'use server'
|
||||||
|
import { clerkClient } from '@clerk/nextjs/server'
|
||||||
|
import { google } from 'googleapis'
|
||||||
|
import { env } from '~/env'
|
||||||
|
|
||||||
|
export async function scheduleMeeting({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
dateTime,
|
||||||
|
durationMinutes,
|
||||||
|
attendeeEmail,
|
||||||
|
attendeeName,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
dateTime: string
|
||||||
|
durationMinutes: number
|
||||||
|
attendeeEmail?: string
|
||||||
|
attendeeName?: string
|
||||||
|
userId: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const clerk = await clerkClient()
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
try {
|
||||||
|
const visitorTokenResponse = await clerk.users.getUserOauthAccessToken(userId, 'oauth_google')
|
||||||
|
if (visitorTokenResponse.data[0]) {
|
||||||
|
const user = await clerk.users.getUser(userId)
|
||||||
|
const googleAccount = user.externalAccounts.find((a) => a.provider === 'google')
|
||||||
|
visitorEmail = googleAccount?.emailAddress ?? undefined
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Visitor not signed in with Google — no invite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar";
|
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar";
|
||||||
import SimpleSidebarGroup from "~/components/ui/simple-sidebar-group";
|
import SimpleSidebarGroup from "~/components/ui/simple-sidebar-group";
|
||||||
|
|
||||||
export default async function AdminSideBar() {
|
export default function AdminSideBar() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
@@ -26,6 +26,9 @@ export default async function AdminSideBar() {
|
|||||||
<SimpleSidebarGroup lable="Blog">
|
<SimpleSidebarGroup lable="Blog">
|
||||||
<Link href={"/"}> Some Blog Action </Link>
|
<Link href={"/"}> Some Blog Action </Link>
|
||||||
</SimpleSidebarGroup>
|
</SimpleSidebarGroup>
|
||||||
|
<SimpleSidebarGroup lable="Chat">
|
||||||
|
<Link href={"/admin/chat"}> System Prompt </Link>
|
||||||
|
</SimpleSidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
39
src/app/admin/chat/_components/SystemPromptForm.tsx
Normal file
39
src/app/admin/chat/_components/SystemPromptForm.tsx
Normal file
@@ -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<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
setSaved(false)
|
||||||
|
mutation.mutate({ prompt: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
|
||||||
|
<Textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => { setValue(e.target.value); setSaved(false) }}
|
||||||
|
rows={16}
|
||||||
|
className="font-mono text-sm resize-y"
|
||||||
|
placeholder="Enter the system prompt for the AI recruiter..."
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? 'Saving…' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
{saved && <span className="text-sm text-muted-foreground">Saved</span>}
|
||||||
|
{mutation.error && <span className="text-sm text-destructive">{mutation.error.message}</span>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
src/app/admin/chat/page.tsx
Normal file
22
src/app/admin/chat/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { isAdmin } from '~/app/actions'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||||
|
import SystemPromptForm from './_components/SystemPromptForm'
|
||||||
|
|
||||||
|
export default async function SystemPromptPage() {
|
||||||
|
if (!(await isAdmin())) redirect('/admin')
|
||||||
|
|
||||||
|
const prompt = await servTrpc.chat.getSystemPrompt()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-2xl p-6 flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold">AI System Prompt</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
This prompt is sent to the model on every chat request.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SystemPromptForm initialValue={prompt} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
'use server'
|
|
||||||
|
|
||||||
import AdminSideBar from "./_components/AdminSideBar";
|
import AdminSideBar from "./_components/AdminSideBar";
|
||||||
|
|
||||||
export default async function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdminSideBar/>
|
<AdminSideBar/>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function CreateUpdateStackForm(params: { className?: string, enti
|
|||||||
const deleteMutation = trpc.techStack.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
|
const deleteMutation = trpc.techStack.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
|
||||||
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||||
setSubmitted(true)
|
setSubmitted(true)
|
||||||
params.entity ?
|
id ?
|
||||||
updateMutation.mutate(values) :
|
updateMutation.mutate(values) :
|
||||||
createMutation.mutate(values);
|
createMutation.mutate(values);
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/app/api/chat/route.ts
Normal file
98
src/app/api/chat/route.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
|
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
||||||
|
import { success, z } from 'zod'
|
||||||
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
import { env } from '~/env'
|
||||||
|
import { db } from '~/server/db'
|
||||||
|
import { chatSession, chatMessage } from '~/server/dbschema/schema'
|
||||||
|
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||||
|
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
|
||||||
|
|
||||||
|
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const { userId } = await auth()
|
||||||
|
if (userId == null) return new Response('Unauthorized', { status: 401 })
|
||||||
|
|
||||||
|
const { messages, sessionId } = (await req.json()) as {
|
||||||
|
messages: UIMessage[]
|
||||||
|
sessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this session belongs to the authenticated user
|
||||||
|
const session = await db
|
||||||
|
.select()
|
||||||
|
.from(chatSession)
|
||||||
|
.where(and(eq(chatSession.id, sessionId), eq(chatSession.userId, userId)))
|
||||||
|
.limit(1)
|
||||||
|
.then((r) => r[0])
|
||||||
|
|
||||||
|
if (!session) return new Response('Session not found', { status: 404 })
|
||||||
|
|
||||||
|
const systemPrompt = await servTrpc.chat.getSystemPrompt() || 'You are an AI recruiter assistant.'
|
||||||
|
|
||||||
|
// Save the latest user message
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
if (lastMessage?.role === 'user') {
|
||||||
|
const content = lastMessage.parts
|
||||||
|
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join('')
|
||||||
|
if (content) {
|
||||||
|
await db.insert(chatMessage).values({ sessionId, role: 'user', content })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = streamText({
|
||||||
|
model: openai('gpt-4o'),
|
||||||
|
system: systemPrompt,
|
||||||
|
messages: await convertToModelMessages(messages),
|
||||||
|
tools: {
|
||||||
|
scheduleMeeting: tool({
|
||||||
|
description: 'Schedule a meeting with Gregor Lohaus and add it to his Google Calendar',
|
||||||
|
inputSchema: z.object({
|
||||||
|
title: z.string().describe('Meeting title'),
|
||||||
|
description: z.string().describe('Meeting description / agenda'),
|
||||||
|
dateTime: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'ISO 8601 datetime for the meeting start, e.g. 2025-04-01T10:00:00',
|
||||||
|
),
|
||||||
|
durationMinutes: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(15)
|
||||||
|
.max(120)
|
||||||
|
.describe('Duration of the meeting in minutes'),
|
||||||
|
attendeeEmail: z
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.optional()
|
||||||
|
.describe('Email of the visitor to invite (if provided)'),
|
||||||
|
attendeeName: z.string().optional().describe('Name of the visitor'),
|
||||||
|
}),
|
||||||
|
execute: async (input) => scheduleMeeting({ ...input, userId }),
|
||||||
|
}),
|
||||||
|
getCurrentUnixTime: tool({
|
||||||
|
description: 'Get the current unix time to reference for meeting dates',
|
||||||
|
inputSchema: z.object({
|
||||||
|
none: z.string().optional().describe("no inputs are needed")
|
||||||
|
}),
|
||||||
|
execute: async () => { return {success: true, currentTime: Date.now()} }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stopWhen: stepCountIs(5),
|
||||||
|
onFinish: async ({ text, finishReason }) => {
|
||||||
|
if (text && finishReason === 'stop') {
|
||||||
|
await db.insert(chatMessage).values({
|
||||||
|
sessionId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.toUIMessageStreamResponse()
|
||||||
|
}
|
||||||
5
src/app/assistant/page.tsx
Normal file
5
src/app/assistant/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function AssistantPage() {
|
||||||
|
redirect('/chat')
|
||||||
|
}
|
||||||
68
src/app/chat/_components/AssistantMessage.tsx
Normal file
68
src/app/chat/_components/AssistantMessage.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type { UIMessage } from "ai";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
export const AssistantMessage = (props: { message: UIMessage }) => {
|
||||||
|
let message = props.message;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className='flex justify-start'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=
|
||||||
|
'max-w-[80%] px-4 py-2 text-sm space-y-2 bg-muted'
|
||||||
|
>
|
||||||
|
{message.parts.map((part, i) => {
|
||||||
|
if (part.type === 'text') {
|
||||||
|
return (
|
||||||
|
<Markdown key={i}>
|
||||||
|
{part.text}
|
||||||
|
</Markdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (part.type === 'tool-scheduleMeeting') {
|
||||||
|
const toolPart = part as unknown as {
|
||||||
|
type: 'tool-scheduleMeeting'
|
||||||
|
state: string
|
||||||
|
input: unknown
|
||||||
|
output?: { success: boolean; message?: string; htmlLink?: string; error?: string }
|
||||||
|
}
|
||||||
|
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
||||||
|
return (
|
||||||
|
<p key={i} className="text-xs opacity-70 italic">
|
||||||
|
Scheduling meeting…
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (toolPart.state === 'output-available' && toolPart.output) {
|
||||||
|
const result = toolPart.output
|
||||||
|
return (
|
||||||
|
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
||||||
|
{result.success ? (
|
||||||
|
<span>
|
||||||
|
✓ {result.message}{' '}
|
||||||
|
{result.htmlLink && (
|
||||||
|
<a
|
||||||
|
href={result.htmlLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
View event
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>✗ {result.error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
161
src/app/chat/_components/ChatInterface.tsx
Normal file
161
src/app/chat/_components/ChatInterface.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useChat } from '@ai-sdk/react'
|
||||||
|
import { DefaultChatTransport, type UIMessage } from 'ai'
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
import { Textarea } from '~/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
useGsapContext,
|
||||||
|
} from '~/app/_providers/GsapProvicer';
|
||||||
|
import Messages from './Messages'
|
||||||
|
import { DeleteIcon } from 'lucide-react';
|
||||||
|
import { trpc } from '~/app/_trpc/Client'
|
||||||
|
import { Spinner } from '~/components/ui/spinner';
|
||||||
|
interface DBMessage {
|
||||||
|
id: string
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatInterfaceProps {
|
||||||
|
sessionId?: string
|
||||||
|
// initialMessages: DBMessage[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
|
||||||
|
return dbMessages.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
parts: [{ type: 'text' as const, text: m.content }],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
export default function ChatInterface({ sessionId }: ChatInterfaceProps) {
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
const { data: dbMessages, refetch: refetchMessages } = trpc.chat.getMessages.useQuery(sessionId ? sessionId : "")
|
||||||
|
const [messages, setMessages] = useState<UIMessage[]>([]);
|
||||||
|
function addMessage(newMessage: UIMessage) {
|
||||||
|
setMessages(prev => [...prev, newMessage]);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
setMessages(toUIMessages(dbMessages ?? []));
|
||||||
|
}, [dbMessages]);
|
||||||
|
|
||||||
|
if (messages.at(0)?.id != 'init') {
|
||||||
|
messages.unshift({
|
||||||
|
id: "init",
|
||||||
|
role: 'assistant',
|
||||||
|
parts: [{
|
||||||
|
type: 'text',
|
||||||
|
text: "Hi im gregors ai assistant,you can ask me to provide general information or to schedule a meeting."
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
const { sendMessage, status, error, clearError } = useChat({
|
||||||
|
transport: new DefaultChatTransport({
|
||||||
|
api: '/api/chat', body: { sessionId },
|
||||||
|
}),
|
||||||
|
messages: messages,
|
||||||
|
})
|
||||||
|
const handleSend = () => {
|
||||||
|
const text = input.trim()
|
||||||
|
if (!text || status != 'ready') return
|
||||||
|
setInput('')
|
||||||
|
sendMessage({ text })
|
||||||
|
addMessage({
|
||||||
|
id: "", role: "user", parts: [
|
||||||
|
{ type: 'text', text }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
addMessage({
|
||||||
|
id: "", role: "assistant", parts: [
|
||||||
|
{ type: 'text', text: "Thinking..." }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const clearChatMutation = trpc.chat.clearChat.useMutation()
|
||||||
|
const handleClear = () => {
|
||||||
|
clearChatMutation.mutate(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.chat.getMessages.invalidate()
|
||||||
|
refetchMessages()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const gsapContext = useGsapContext()
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(status)
|
||||||
|
if (status == 'ready') {
|
||||||
|
utils.chat.getMessages.invalidate();
|
||||||
|
refetchMessages()
|
||||||
|
}
|
||||||
|
}, [status])
|
||||||
|
useEffect(() => {
|
||||||
|
let scroller = gsapContext?.getScroller()
|
||||||
|
if (scroller instanceof Window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(scroller?.scrollHeight)
|
||||||
|
scroller?.scrollTo({ behavior: 'smooth', top: scroller.scrollHeight })
|
||||||
|
}, [messages])
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{messages &&
|
||||||
|
<Messages messages={messages} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mx-4 mb-2 flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
|
<span className="flex-1">
|
||||||
|
{error.message.includes('quota') || error.message.includes('429')
|
||||||
|
? 'OpenAI quota exceeded. Please try again later.'
|
||||||
|
: `Error: ${error.message}`}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={clearError}
|
||||||
|
className="shrink-0 opacity-60 hover:opacity-100"
|
||||||
|
variant='destructive'
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4 border-t flex flex-row gap-2">
|
||||||
|
<Textarea
|
||||||
|
name='message'
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Ask about Gregor's experience or schedule a meeting…"
|
||||||
|
className="resize-none"
|
||||||
|
rows={2}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSend()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<Button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={status != "ready" || !input.trim()}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='destructive'
|
||||||
|
onClick={handleClear}
|
||||||
|
disabled={status != "ready" || clearChatMutation.isPending}
|
||||||
|
>
|
||||||
|
{clearChatMutation.isPending ?
|
||||||
|
<Spinner /> :
|
||||||
|
"Clear Chat"
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
src/app/chat/_components/Messages.tsx
Normal file
25
src/app/chat/_components/Messages.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { type UIMessage } from 'ai'
|
||||||
|
import * as Card from "~/components/ui/card"
|
||||||
|
import AnimateTextIn from '~/app/_components/Animated/AnimateIn';
|
||||||
|
import { UserMessage } from './UserMessage';
|
||||||
|
import { AssistantMessage } from './AssistantMessage';
|
||||||
|
import { ScrollArea } from '~/components/ui/scroll-area';
|
||||||
|
import { useTimeLine } from '~/app/_providers/GsapProvicer';
|
||||||
|
import {
|
||||||
|
memo
|
||||||
|
} from 'react';
|
||||||
|
const Messages = memo(({ messages}: { messages: UIMessage[]}) => {
|
||||||
|
return (
|
||||||
|
<ScrollArea data-scroller-priority='1' className="w-full h-[90%] max-w-4xl mx-auto">
|
||||||
|
{messages.map((message, i) => (
|
||||||
|
<Card.AnimatedCard scrollOnly={true} tlId='chat' position={i * 0.2} key={i}>
|
||||||
|
<Card.CardContent>
|
||||||
|
{message.role == 'assistant' && <AssistantMessage message={message} />}
|
||||||
|
{message.role == 'user' && <UserMessage message={message} />}
|
||||||
|
</Card.CardContent>
|
||||||
|
</Card.AnimatedCard>
|
||||||
|
))}
|
||||||
|
</ScrollArea>)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Messages;
|
||||||
23
src/app/chat/_components/UserMessage.tsx
Normal file
23
src/app/chat/_components/UserMessage.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { UIMessage } from "ai"
|
||||||
|
|
||||||
|
export const UserMessage = (props:{message: UIMessage}) => {
|
||||||
|
let message = props.message.parts.reduce((acc, part) => {
|
||||||
|
if (part.type == 'text') {
|
||||||
|
return acc + part.text
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},"");
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={props.message.id}
|
||||||
|
className='flex justify-end'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=
|
||||||
|
'max-w-[80%] px-4 py-2 text-sm space-y-2 bg-primary'
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
src/app/chat/page.tsx
Normal file
23
src/app/chat/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
import ChatInterface from './_components/ChatInterface'
|
||||||
|
import { trpc } from '../_trpc/Client';
|
||||||
|
import { Skeleton } from '~/components/ui/skeleton';
|
||||||
|
import AnimatedPageTitle from '../_components/Animated/AnimatedPageTitle';
|
||||||
|
import { useTimeLine } from '../_providers/GsapProvicer';
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||||
|
useTimeLine(session)
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
|
||||||
|
<AnimatedPageTitle position={0}>
|
||||||
|
<span>Talk To My </span> <span> AI-Assistant</span>
|
||||||
|
</AnimatedPageTitle>
|
||||||
|
<div className='flex items-center h-[80%] my-auto w-full'>
|
||||||
|
<ChatInterface sessionId={session?.id} />
|
||||||
|
{error && <div>{error.message}</div>}
|
||||||
|
{isLoading && <Skeleton/>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { ClerkProvider } from "@clerk/nextjs";
|
|||||||
import { config } from "@fortawesome/fontawesome-svg-core"
|
import { config } from "@fortawesome/fontawesome-svg-core"
|
||||||
import "@fortawesome/fontawesome-svg-core/styles.css"
|
import "@fortawesome/fontawesome-svg-core/styles.css"
|
||||||
import TopNav from "./_components/TopNav";
|
import TopNav from "./_components/TopNav";
|
||||||
|
import ChatFAB from "./_components/ChatFAB";
|
||||||
import TrpcProvider from "./_trpc/TrpcProvider";
|
import TrpcProvider from "./_trpc/TrpcProvider";
|
||||||
// import dynamic from "next/dynamic";
|
// import dynamic from "next/dynamic";
|
||||||
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
||||||
@@ -46,11 +47,12 @@ export default async function RootLayout({
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AnimatedBackGroundContainer followSpeed={0.003} particleCount={100} orbitRadius={2000}>
|
<AnimatedBackGroundContainer followSpeed={0.003} particleCount={100} orbitRadius={2000}>
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<main className="absolute lg:top-10 h-screen w-screen">
|
<main className="absolute lg:top-10 h-screen lg:h-[calc(100vh-var(--spacing)*10)] w-screen">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
{modal}
|
{modal}
|
||||||
</AnimatedBackGroundContainer>
|
</AnimatedBackGroundContainer>
|
||||||
|
<ChatFAB />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,26 +6,31 @@ import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
|
|||||||
import { Spinner } from "~/components/ui/spinner";
|
import { Spinner } from "~/components/ui/spinner";
|
||||||
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
||||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
|
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
||||||
export default function MusicPage() {
|
export default function MusicPage() {
|
||||||
const { data: tracks, isLoading } = trpc.music.list.useQuery();
|
const { data: tracks, isLoading } = trpc.music.list.useQuery();
|
||||||
useTimeLine(tracks)
|
useTimeLine(tracks)
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
|
||||||
<AnimatedPageTitle position={0} text="Just Some Music I Made" />
|
<AnimatedPageTitle position={0}><span>Just Some </span> <span>Music I Made</span> </AnimatedPageTitle>
|
||||||
<AnimateTextIn position={0.5}>
|
<div className="flex flex-wrap h-fit content-center">
|
||||||
<div className="flex flex-col lg:flex-row h-fit content-center">
|
<AnimateTextIn className="flex flex-wrap mr-[1em]" position={0.5}>
|
||||||
<p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p>
|
<div><p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p></div>
|
||||||
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>
|
<div><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div>
|
||||||
|
</AnimateTextIn>
|
||||||
|
<AnimatePopUp position={2} className="items-center content-center">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" />
|
<img className="max-w-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" />
|
||||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" />
|
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" />
|
||||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" />
|
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" />
|
||||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
|
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
</AnimatePopUp>
|
||||||
</div>
|
</div>
|
||||||
</AnimateTextIn>
|
<div className="pt-10" />
|
||||||
{tracks && tracks.map((track, i) => (
|
{tracks && tracks.map((track, i) => (
|
||||||
<Card.AnimatedCard key={track.id} position={i + 1}>
|
<div key={track.id}>
|
||||||
|
<Card.AnimatedCard position={i + 1}>
|
||||||
<Card.CardHeader>
|
<Card.CardHeader>
|
||||||
<AnimateTextIn position={i + 1.2} animation="slide">
|
<AnimateTextIn position={i + 1.2} animation="slide">
|
||||||
<Card.CardTitle>{track.title}</Card.CardTitle>
|
<Card.CardTitle>{track.title}</Card.CardTitle>
|
||||||
@@ -35,11 +40,15 @@ export default function MusicPage() {
|
|||||||
{track.description && (
|
{track.description && (
|
||||||
<p className="text-sm text-muted-foreground gsapant">{track.description}</p>
|
<p className="text-sm text-muted-foreground gsapant">{track.description}</p>
|
||||||
)}
|
)}
|
||||||
|
<AnimatePopUp position={i + 1.3}>
|
||||||
<audio controls className="w-full player" src={track.fileUrl}>
|
<audio controls className="w-full player" src={track.fileUrl}>
|
||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
</audio>
|
</audio>
|
||||||
|
</AnimatePopUp>
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
</Card.AnimatedCard>
|
</Card.AnimatedCard>
|
||||||
|
<div className="pt-5" />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{!isLoading && !tracks?.length &&
|
{!isLoading && !tracks?.length &&
|
||||||
<div className="flex justify-center items-center text-muted-foreground">
|
<div className="flex justify-center items-center text-muted-foreground">
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import * as Card from "~/components/ui/card";
|
|||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { StackBadge } from "~/components/StackBadge";
|
import { StackBadge } from "~/components/StackBadge";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
|
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
|
||||||
|
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
||||||
|
import { useTimeLine } from "../_providers/GsapProvicer";
|
||||||
|
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
|
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
|
||||||
|
useTimeLine(projects)
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
|
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
|
||||||
@@ -26,17 +32,22 @@ export default function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
|
||||||
{projects.map((project) => (
|
<AnimatedPageTitle position={0}><span>Project I've Been</span><span> Working on</span> </AnimatedPageTitle>
|
||||||
<Card.Card key={project.id}>
|
<div className="pt-10" />
|
||||||
|
{projects.map((project, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<Card.AnimatedCard position={i + 1.2} key={project.id}>
|
||||||
<Card.CardHeader>
|
<Card.CardHeader>
|
||||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||||
<Card.CardTitle>{project.title}</Card.CardTitle>
|
<AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{project.sourceType && (
|
{project.sourceType && (
|
||||||
|
<AnimatePopUp position={i + 2} duration={2}>
|
||||||
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
||||||
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</AnimatePopUp>
|
||||||
)}
|
)}
|
||||||
{project.releaseStatus && (
|
{project.releaseStatus && (
|
||||||
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
||||||
@@ -50,44 +61,54 @@ export default function ProjectsPage() {
|
|||||||
<Card.CardContent className="flex flex-col gap-3">
|
<Card.CardContent className="flex flex-col gap-3">
|
||||||
{project.description && (
|
{project.description && (
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
|
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
|
||||||
<Markdown>{project.description}</Markdown>
|
<AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
|
||||||
|
<AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-row">
|
||||||
|
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{project.techStack.stackItems.map((item, k) => (
|
||||||
|
<AnimatePopUp key={k} position={(i + 2) + k * 0.5}> <StackBadge key={item} item={item} /> </AnimatePopUp>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(project.sourceLink || project.releaseLink) && (
|
{(project.sourceLink || project.releaseLink) && (
|
||||||
<div className="flex gap-3 flex-wrap">
|
<div className="ml-auto flex-col lg:flex-row justify-center gap-5">
|
||||||
{project.sourceLink && (
|
{project.sourceLink &&
|
||||||
|
<Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
|
||||||
<a
|
<a
|
||||||
href={project.sourceLink}
|
href={project.sourceLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
className='items-center'
|
||||||
>
|
>
|
||||||
Source
|
Source
|
||||||
</a>
|
</a>
|
||||||
)}
|
</Button>
|
||||||
{project.releaseLink && (
|
}
|
||||||
|
{project.releaseLink &&
|
||||||
|
<Button variant='default' className="cursor-pointer min-w-18 items-center">
|
||||||
<a
|
<a
|
||||||
href={project.releaseLink}
|
href={project.releaseLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
className='items-center'
|
||||||
>
|
>
|
||||||
Live
|
Live
|
||||||
</a>
|
</a>
|
||||||
)}
|
</Button>
|
||||||
|
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{project.techStack.stackItems.map((item) => (
|
|
||||||
<StackBadge key={item} item={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
)}
|
)}
|
||||||
</Card.Card>
|
</Card.AnimatedCard>
|
||||||
))}
|
<div className="pt-5" />
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,20 +26,40 @@ function AnimatedCard({
|
|||||||
className,
|
className,
|
||||||
position = 0,
|
position = 0,
|
||||||
size = "default",
|
size = "default",
|
||||||
|
tlId = undefined,
|
||||||
|
scrollOnly = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm", position: gsap.Position }) {
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm", position: gsap.Position, tlId?: string, scrollOnly?: boolean }) {
|
||||||
const gsapContext = useGsapContext()
|
const gsapContext = useGsapContext()
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
const rect = ref.current?.getBoundingClientRect()
|
const rect = ref.current?.getBoundingClientRect()
|
||||||
const isInView = rect && rect.top < window.innerHeight
|
|
||||||
const fromVars = { x: -100, opacity: 0, duration: 0.5 }
|
|
||||||
if (isInView) {
|
|
||||||
gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position)
|
|
||||||
} else {
|
|
||||||
const scroller = gsapContext?.getScroller()
|
const scroller = gsapContext?.getScroller()
|
||||||
console.log('scroller:', scroller)
|
console.log(scroller)
|
||||||
gsap.from(ref.current, { ...fromVars, scrollTrigger: { trigger: ref.current, start: 'top 85%', 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 fromVars = { x: -100, opacity: 0, duration: 0.5 }
|
||||||
|
if (isInView && !scrollOnly) {
|
||||||
|
gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position, tlId)
|
||||||
|
} else {
|
||||||
|
gsap.from(ref.current,
|
||||||
|
{
|
||||||
|
...fromVars,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: ref.current,
|
||||||
|
start: 'top bottom',
|
||||||
|
end: 'bottom top',
|
||||||
|
toggleActions: "play reverse play reverse",
|
||||||
|
scroller
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, { dependencies: [] })
|
}, { dependencies: [] })
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const env = createEnv({
|
|||||||
CLERK_SECRET_KEY: z.string(),
|
CLERK_SECRET_KEY: z.string(),
|
||||||
ADMIN_USER_CLERK_ID: z.string(),
|
ADMIN_USER_CLERK_ID: z.string(),
|
||||||
UPLOADTHING_TOKEN: z.string(),
|
UPLOADTHING_TOKEN: z.string(),
|
||||||
|
OPENAI_API_KEY: z.string(),
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
@@ -66,6 +67,7 @@ export const env = createEnv({
|
|||||||
POSTGRES_PRISMA_URL: process.env.POSTGRES_PRISMA_URL,
|
POSTGRES_PRISMA_URL: process.env.POSTGRES_PRISMA_URL,
|
||||||
ADMIN_USER_CLERK_ID: process.env.ADMIN_USER_CLERK_ID,
|
ADMIN_USER_CLERK_ID: process.env.ADMIN_USER_CLERK_ID,
|
||||||
UPLOADTHING_TOKEN: process.env.UPLOADTHING_TOKEN,
|
UPLOADTHING_TOKEN: process.env.UPLOADTHING_TOKEN,
|
||||||
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
NEXT_PUBLIC_ADMIN_USER_CLERK_ID: process.env.NEXT_PUBLIC_ADMIN_USER_CLERK_ID,
|
NEXT_PUBLIC_ADMIN_USER_CLERK_ID: process.env.NEXT_PUBLIC_ADMIN_USER_CLERK_ID,
|
||||||
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||||
|
|||||||
@@ -103,3 +103,44 @@ export const music = createTable(
|
|||||||
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(),
|
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const messageRoleEnum = pgEnum('message_role', ['user', 'assistant'])
|
||||||
|
|
||||||
|
export const chatSession = createTable(
|
||||||
|
"chat_session",
|
||||||
|
(d) => ({
|
||||||
|
id: d.uuid().primaryKey().defaultRandom(),
|
||||||
|
userId: d.varchar({ length: 255 }).notNull(),
|
||||||
|
createdAt: d.timestamp({ withTimezone: true }).default(sql`CURRENT_TIMESTAMP`).notNull().$type<Date>(),
|
||||||
|
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const chatSessionRelations = relations(chatSession, ({ many }) => ({
|
||||||
|
messages: many(chatMessage),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const chatMessage = createTable(
|
||||||
|
"chat_message",
|
||||||
|
(d) => ({
|
||||||
|
id: d.uuid().primaryKey().defaultRandom(),
|
||||||
|
sessionId: d.uuid('session_id').notNull(),
|
||||||
|
role: messageRoleEnum().notNull(),
|
||||||
|
content: d.text().notNull(),
|
||||||
|
createdAt: d.timestamp({ withTimezone: true }).default(sql`CURRENT_TIMESTAMP`).notNull().$type<Date>(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const chatMessageRelations = relations(chatMessage, ({ one }) => ({
|
||||||
|
session: one(chatSession, {
|
||||||
|
fields: [chatMessage.sessionId],
|
||||||
|
references: [chatSession.id],
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const systemSettings = createTable(
|
||||||
|
"systemSetting",
|
||||||
|
(d) => ({
|
||||||
|
systemPropmt: d.text()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { cvEntryRouter } from "./cvEntry";
|
|||||||
import { musicRouter } from "./music";
|
import { musicRouter } from "./music";
|
||||||
import { trpcCrudRouterFromDrizzleEntity } from "../lib";
|
import { trpcCrudRouterFromDrizzleEntity } from "../lib";
|
||||||
import { cvCategory } from "../dbschema/schema";
|
import { cvCategory } from "../dbschema/schema";
|
||||||
|
import { chatRouter } from "./chat";
|
||||||
|
|
||||||
export const trpcRouter = router({
|
export const trpcRouter = router({
|
||||||
project: trpcCrudRouterFromDrizzleEntity('project').router,
|
project: trpcCrudRouterFromDrizzleEntity('project').router,
|
||||||
@@ -19,6 +20,7 @@ export const trpcRouter = router({
|
|||||||
entry: trpcCrudRouterFromDrizzleEntity('cvEntry').router,
|
entry: trpcCrudRouterFromDrizzleEntity('cvEntry').router,
|
||||||
entryv2: cvEntryRouter,
|
entryv2: cvEntryRouter,
|
||||||
music: musicRouter,
|
music: musicRouter,
|
||||||
|
chat: chatRouter
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TrpcRouter = typeof trpcRouter;
|
export type TrpcRouter = typeof trpcRouter;
|
||||||
|
|||||||
76
src/server/routers/chat.ts
Normal file
76
src/server/routers/chat.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
import { publicProcedure, router } from "../trpc";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { db } from '~/server/db'
|
||||||
|
import { chatMessage,
|
||||||
|
chatSession, systemSettings } from "../dbschema/schema";
|
||||||
|
import { isAdmin } from '~/app/actions';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
export const chatRouter = router({
|
||||||
|
getSession: publicProcedure.query(async () => {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
let session = await db.query.chatSession.findFirst({
|
||||||
|
where(fields, operators) {
|
||||||
|
return operators.eq(fields.userId, userId)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (session !== undefined) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
let newSession = await db.insert(chatSession).values({ userId: userId }).returning().execute().then((r) => r.at(0));
|
||||||
|
if (newSession == undefined) {
|
||||||
|
throw new TRPCError({ message: "failed to create session", code: "INTERNAL_SERVER_ERROR" });
|
||||||
|
}
|
||||||
|
session = await db.query.chatSession.findFirst({
|
||||||
|
where(fields, operators) {
|
||||||
|
return operators.eq(fields.userId, userId)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (session == undefined) {
|
||||||
|
throw new TRPCError({ message: "session not found", code: "NOT_FOUND" });
|
||||||
|
}
|
||||||
|
if (session !== undefined) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getMessages: publicProcedure.input(z.string()).query(async ({input}) => {
|
||||||
|
let res = await db.query.chatMessage.findMany({
|
||||||
|
where(fields,operators) {
|
||||||
|
return operators.eq(fields.sessionId,input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res;
|
||||||
|
}),
|
||||||
|
clearChat: publicProcedure.mutation(async () => {
|
||||||
|
console.log("deleting session")
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
let session = await db.query.chatSession.findFirst({
|
||||||
|
with: {
|
||||||
|
messages: true
|
||||||
|
},
|
||||||
|
where(fields, operators) {
|
||||||
|
return operators.eq(fields.userId, userId)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (session != undefined) {
|
||||||
|
db.delete(chatMessage).where(eq(chatMessage.sessionId,session.id)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
}),
|
||||||
|
getSystemPrompt: publicProcedure.query(async () => {
|
||||||
|
const row = await db.select().from(systemSettings).limit(1).then((r) => r[0])
|
||||||
|
return row?.systemPropmt ?? ''
|
||||||
|
}),
|
||||||
|
updateSystemPrompt: publicProcedure.input(z.object({ prompt: z.string() })).mutation(async ({ input }) => {
|
||||||
|
if (!(await isAdmin())) throw new TRPCError({ code: 'FORBIDDEN' })
|
||||||
|
await db.delete(systemSettings)
|
||||||
|
await db.insert(systemSettings).values({ systemPropmt: input.prompt })
|
||||||
|
}),
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user