Compare commits
14 Commits
musicpage
...
399d78e508
| Author | SHA1 | Date | |
|---|---|---|---|
| 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/(.)chat/_components/ChatModal.tsx
Normal file
32
src/app/@modal/(.)chat/_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, initialMessages }: 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>AI Recruiter</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-hidden min-h-0">
|
||||||
|
<ChatInterface sessionId={sessionId} initialMessages={initialMessages} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
15
src/app/@modal/(.)chat/page.tsx
Normal file
15
src/app/@modal/(.)chat/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { Skeleton } from '~/components/ui/skeleton';
|
||||||
|
import ChatModal from './_components/ChatModal'
|
||||||
|
import { trpc } from '~/app/_trpc/Client'
|
||||||
|
|
||||||
|
export default function ChatModalPage() {
|
||||||
|
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{session && <ChatModal sessionId={session.id} initialMessages={session.messages} />}
|
||||||
|
{error && <div>{error.message}</div>}
|
||||||
|
{isLoading && <Skeleton />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,30 +1,41 @@
|
|||||||
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,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: ReactNode,
|
||||||
|
animation?: "type" | "slide",
|
||||||
|
position: gsap.Position,
|
||||||
|
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 isInView = rect && rect.top < window.innerHeight
|
||||||
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)
|
||||||
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, stagger: { each: 0.08 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||||
: {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'}
|
: { opacity: 0, duration: 0.01, stagger: { each: 0.04 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||||
if (isInView) {
|
if (isInView) {
|
||||||
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position)
|
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position)
|
||||||
} 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 85%', scroller: gsapContext?.getScroller() } })
|
||||||
}
|
}
|
||||||
}, { dependencies: [] })
|
}, { dependencies: [] })
|
||||||
return (
|
return (
|
||||||
<div ref={el} className="opacity-0">
|
<div ref={el} className={cn(className,"opacity-0")}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AnimateTextIn;
|
export default AnimateTextIn;
|
||||||
|
|||||||
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/app/_components/ChatFAB.tsx
Normal file
19
src/app/_components/ChatFAB.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { MessageCircle } from 'lucide-react'
|
||||||
|
import { Show } from '@clerk/nextjs'
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
|
||||||
|
export default function ChatFAB() {
|
||||||
|
return (
|
||||||
|
<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="/chat">
|
||||||
|
<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">
|
||||||
|
<a href="/chat"> Chat </a>
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 } 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)
|
||||||
@@ -22,7 +22,7 @@ export function useGsapContext() {
|
|||||||
return useContext(GsapContext)
|
return useContext(GsapContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTimeLine = (dep:any,all?:boolean) => {
|
export const useTimeLine = (dep?:any,all?:boolean) => {
|
||||||
const gsapContext = useGsapContext()
|
const gsapContext = useGsapContext()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dep instanceof Array && all) {
|
if (dep instanceof Array && all) {
|
||||||
@@ -37,7 +37,7 @@ export const useTimeLine = (dep:any,all?:boolean) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},[dep])
|
},[dep])
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
gsapContext?.resetTimeline()
|
gsapContext?.resetTimeline()
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,7 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
|||||||
tl.current?.add(animation, position);
|
tl.current?.add(animation, position);
|
||||||
},[])
|
},[])
|
||||||
const resetTimeline = useCallback(() => {
|
const resetTimeline = useCallback(() => {
|
||||||
|
console.log('resetting timeline')
|
||||||
tl.current?.kill()
|
tl.current?.kill()
|
||||||
tl.current?.revert()
|
tl.current?.revert()
|
||||||
ScrollTrigger.getAll().forEach(st => st.kill())
|
ScrollTrigger.getAll().forEach(st => st.kill())
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/app/api/chat/route.ts
Normal file
91
src/app/api/chat/route.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
|
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
||||||
|
import { 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 }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
stopWhen: stepCountIs(5),
|
||||||
|
onFinish: async ({ text, finishReason }) => {
|
||||||
|
if (text && finishReason === 'stop') {
|
||||||
|
await db.insert(chatMessage).values({
|
||||||
|
sessionId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.toUIMessageStreamResponse()
|
||||||
|
}
|
||||||
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>
|
||||||
|
{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
src/app/chat/_components/ChatInterface.tsx
Normal file
127
src/app/chat/_components/ChatInterface.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
'use client'
|
||||||
|
import { useRef, 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 { cn } from '~/lib/utils'
|
||||||
|
import Markdown from 'react-markdown';
|
||||||
|
import { AssistantMessage } from './AssistantMessage';
|
||||||
|
import { UserMessage } from './UserMessage';
|
||||||
|
|
||||||
|
type 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, initialMessages }: ChatInterfaceProps) {
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const { messages, sendMessage, status, error, clearError } = useChat({
|
||||||
|
transport: new DefaultChatTransport({
|
||||||
|
api: '/api/chat',
|
||||||
|
body: { sessionId },
|
||||||
|
}),
|
||||||
|
messages: toUIMessages(initialMessages),
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = status === 'submitted' || status === 'streaming'
|
||||||
|
const hasError = status === 'error'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
const text = input.trim()
|
||||||
|
if (!text || isLoading) return
|
||||||
|
setInput('')
|
||||||
|
sendMessage({ text })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.length === 0 && (
|
||||||
|
<div className="text-center text-muted-foreground py-12">
|
||||||
|
<p className="text-base font-medium mb-1">Hi! I'm Gregor's AI recruiter assistant.</p>
|
||||||
|
<p className="text-sm">Ask me about his skills and experience, or schedule a meeting!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{messages.map((message) => (
|
||||||
|
<>
|
||||||
|
{message.role == 'assistant' && <AssistantMessage message={message}/>}
|
||||||
|
{message.role == 'user' && <UserMessage message={message}/>}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-muted rounded-lg px-4 py-2 text-sm">
|
||||||
|
<span className="animate-pulse">Thinking…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4 border-t flex gap-2">
|
||||||
|
<Textarea
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={isLoading || hasError || !input.trim()}
|
||||||
|
className="self-end"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
src/app/chat/page.tsx
Normal file
27
src/app/chat/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
'use client'
|
||||||
|
import ChatInterface from './_components/ChatInterface'
|
||||||
|
import { trpc } from '../_trpc/Client';
|
||||||
|
import { Skeleton } from '~/components/ui/skeleton';
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||||
|
return (
|
||||||
|
<div className="container max-w-2xl mx-auto h-screen pt-10 pb-4 flex flex-col">
|
||||||
|
<div className="flex flex-col flex-1 bg-background border rounded-lg overflow-hidden">
|
||||||
|
<div className="p-4 border-b flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold">AI Recruiter</h1>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Chat with Gregor's AI assistant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{session && <ChatInterface sessionId={session?.id} initialMessages={session?.messages} /> }
|
||||||
|
{error && <div>{error.message}</div>}
|
||||||
|
{isLoading && <Skeleton/>}
|
||||||
|
</div>
|
||||||
|
</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})
|
||||||
@@ -51,6 +52,7 @@ export default async function RootLayout({
|
|||||||
</main>
|
</main>
|
||||||
{modal}
|
{modal}
|
||||||
</AnimatedBackGroundContainer>
|
</AnimatedBackGroundContainer>
|
||||||
|
<ChatFAB />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,45 +1,54 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { trpc } from "~/app/_trpc/Client";
|
import { trpc } from "~/app/_trpc/Client";
|
||||||
import * as Card from "~/components/ui/card";
|
import * as Card from "~/components/ui/card";
|
||||||
import { useTimeLine } from "../_providers/GsapProvicer";
|
import { useTimeLine } from "../_providers/GsapProvicer";
|
||||||
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
|
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>
|
||||||
</div>
|
</AnimatePopUp>
|
||||||
</AnimateTextIn>
|
</div>
|
||||||
|
<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.CardHeader>
|
<Card.AnimatedCard position={i + 1}>
|
||||||
<AnimateTextIn position={i + 1.2} animation="slide">
|
<Card.CardHeader>
|
||||||
<Card.CardTitle>{track.title}</Card.CardTitle>
|
<AnimateTextIn position={i + 1.2} animation="slide">
|
||||||
</AnimateTextIn>
|
<Card.CardTitle>{track.title}</Card.CardTitle>
|
||||||
</Card.CardHeader>
|
</AnimateTextIn>
|
||||||
<Card.CardContent className="flex flex-col gap-3">
|
</Card.CardHeader>
|
||||||
{track.description && (
|
<Card.CardContent className="flex flex-col gap-3">
|
||||||
<p className="text-sm text-muted-foreground gsapant">{track.description}</p>
|
{track.description && (
|
||||||
)}
|
<p className="text-sm text-muted-foreground gsapant">{track.description}</p>
|
||||||
<audio controls className="w-full player" src={track.fileUrl}>
|
)}
|
||||||
Your browser does not support the audio element.
|
<AnimatePopUp position={i + 1.3}>
|
||||||
</audio>
|
<audio controls className="w-full player" src={track.fileUrl}>
|
||||||
</Card.CardContent>
|
Your browser does not support the audio element.
|
||||||
</Card.AnimatedCard>
|
</audio>
|
||||||
|
</AnimatePopUp>
|
||||||
|
</Card.CardContent>
|
||||||
|
</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,68 +32,83 @@ 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" />
|
||||||
<Card.CardHeader>
|
{projects.map((project, i) => (
|
||||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
<div key={i}>
|
||||||
<Card.CardTitle>{project.title}</Card.CardTitle>
|
<Card.AnimatedCard position={i + 1.2} key={project.id}>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<Card.CardHeader>
|
||||||
{project.sourceType && (
|
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||||
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
<AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
|
||||||
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
<div className="flex gap-2 flex-wrap">
|
||||||
</Badge>
|
{project.sourceType && (
|
||||||
)}
|
<AnimatePopUp position={i + 2} duration={2}>
|
||||||
{project.releaseStatus && (
|
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
||||||
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
||||||
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
|
</Badge>
|
||||||
</Badge>
|
</AnimatePopUp>
|
||||||
)}
|
)}
|
||||||
|
{project.releaseStatus && (
|
||||||
|
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
||||||
|
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card.CardHeader>
|
||||||
</Card.CardHeader>
|
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
|
||||||
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
|
<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">
|
<AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
|
||||||
<Markdown>{project.description}</Markdown>
|
<AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(project.sourceLink || project.releaseLink) && (
|
<div className="flex flex-row">
|
||||||
<div className="flex gap-3 flex-wrap">
|
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
||||||
{project.sourceLink && (
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<a
|
{project.techStack.stackItems.map((item, k) => (
|
||||||
href={project.sourceLink}
|
<AnimatePopUp key={k} position={(i + 2) + k * 0.5}> <StackBadge key={item} item={item} /> </AnimatePopUp>
|
||||||
target="_blank"
|
))}
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
|
||||||
>
|
|
||||||
Source
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
{project.releaseLink && (
|
{(project.sourceLink || project.releaseLink) && (
|
||||||
<a
|
<div className="ml-auto flex-col lg:flex-row justify-center gap-5">
|
||||||
href={project.releaseLink}
|
{project.sourceLink &&
|
||||||
target="_blank"
|
<Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
|
||||||
rel="noopener noreferrer"
|
<a
|
||||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
href={project.sourceLink}
|
||||||
>
|
target="_blank"
|
||||||
Live
|
rel="noopener noreferrer"
|
||||||
</a>
|
className='items-center'
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{project.releaseLink &&
|
||||||
|
<Button variant='default' className="cursor-pointer min-w-18 items-center">
|
||||||
|
<a
|
||||||
|
href={project.releaseLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className='items-center'
|
||||||
|
>
|
||||||
|
Live
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Card.CardContent>
|
||||||
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
)}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
</Card.AnimatedCard>
|
||||||
{project.techStack.stackItems.map((item) => (
|
<div className="pt-5" />
|
||||||
<StackBadge key={item} item={item} />
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card.CardContent>
|
|
||||||
)}
|
|
||||||
</Card.Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
53
src/server/routers/chat.ts
Normal file
53
src/server/routers/chat.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
import { publicProcedure, router } from "../trpc";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { db } from '~/server/db'
|
||||||
|
import { chatSession, systemSettings } from "../dbschema/schema";
|
||||||
|
import { isAdmin } from '~/app/actions';
|
||||||
|
import { z } from 'zod';
|
||||||
|
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({
|
||||||
|
with: {
|
||||||
|
messages: true
|
||||||
|
},
|
||||||
|
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({
|
||||||
|
with: {
|
||||||
|
messages: true
|
||||||
|
},
|
||||||
|
where(fields, operators) {
|
||||||
|
return operators.eq(fields.id,newSession.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (session == undefined) {
|
||||||
|
throw new TRPCError({message: "session not found", code:"NOT_FOUND"});
|
||||||
|
}
|
||||||
|
if (session !== undefined) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
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