diff --git a/.env.local.example b/.env.local.example index 099ada7f..c29f661f 100644 --- a/.env.local.example +++ b/.env.local.example @@ -13,3 +13,9 @@ NEXT_SEARCH_ACCESS_TOKEN=search-access-token # Google Analytics key NEXT_PUBLIC_GOOGLE_ANALYTICS_KEY=ga-key + +# Google Vertex AI API Key +GOOGLE_VERTEX_CLIENT_EMAIL=example-project-website@example-org.iam.gserviceaccount.com +GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----Your private key here-----END PRIVATE KEY-----" +GOOGLE_VERTEX_LOCATION=us-west1 +GOOGLE_VERTEX_PROJECT=example-project diff --git a/package.json b/package.json index 1b8cb3da..64a267d2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "prepare": "husky" }, "dependencies": { + "@ai-sdk/google-vertex": "^2.2.27", "@apollo/client": "^3.13.8", "@faustwp/blocks": "^6.1.2", "@faustwp/cli": "^3.2.3", @@ -31,6 +32,7 @@ "@shikijs/transformers": "^3.7.0", "@sindresorhus/slugify": "^2.2.1", "@wpengine/atlas-next": "^3.0.0", + "ai": "^4.3.16", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "feed": "^5.1.0", @@ -44,6 +46,7 @@ "react-dom": "^19.1.0", "react-icons": "^5.5.0", "react-intersection-observer": "^9.16.0", + "react-markdown": "^10.1.0", "rehype-callouts": "^2.1.1", "rehype-external-links": "^3.0.0", "rehype-pretty-code": "^0.14.1", @@ -57,7 +60,8 @@ "shiki": "^3.7.0", "strip-markdown": "^6.0.0", "unified": "^11.0.5", - "vfile-matter": "^5.0.1" + "vfile-matter": "^5.0.1", + "zod": "^3.25.76" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ce5cb03..24560fbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,18 +17,21 @@ importers: .: dependencies: + '@ai-sdk/google-vertex': + specifier: ^2.2.27 + version: 2.2.27(zod@3.25.76) '@apollo/client': specifier: ^3.13.8 version: 3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@faustwp/blocks': specifier: ^6.1.2 - version: 6.1.2(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@faustwp/core@3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@wordpress/style-engine@2.11.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 6.1.2(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@faustwp/core@3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@wordpress/style-engine@2.11.0)(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@faustwp/cli': specifier: ^3.2.3 version: 3.2.3(webpack@5.96.1) '@faustwp/core': specifier: ^3.2.3 - version: 3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@headlessui/react': specifier: ^2.2.4 version: 2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -37,7 +40,7 @@ importers: version: 2.0.2 '@next/third-parties': specifier: ^15.3.5 - version: 15.3.5(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 15.3.5(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) '@octokit/core': specifier: ^7.0.2 version: 7.0.2 @@ -49,7 +52,10 @@ importers: version: 2.2.1 '@wpengine/atlas-next': specifier: ^3.0.0 - version: 3.0.0(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(node-fetch@2.7.0) + version: 3.0.0(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(node-fetch@2.7.0) + ai: + specifier: ^4.3.16 + version: 4.3.16(react@19.1.0)(zod@3.25.76) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -70,13 +76,13 @@ importers: version: 4.0.8 next: specifier: ^15.3.5 - version: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-mdx-remote-client: specifier: ^2.1.2 version: 2.1.2(@types/react@18.3.12)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(unified@11.0.5) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + version: 4.2.3(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) react: specifier: ^19.1.0 version: 19.1.0 @@ -89,6 +95,9 @@ importers: react-intersection-observer: specifier: ^9.16.0 version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.12)(react@19.1.0) rehype-callouts: specifier: ^2.1.1 version: 2.1.1 @@ -131,6 +140,9 @@ importers: vfile-matter: specifier: ^5.0.1 version: 5.0.1 + zod: + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@tailwindcss/postcss': specifier: ^4.1.11 @@ -180,6 +192,50 @@ importers: packages: + '@ai-sdk/anthropic@1.2.12': + resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/google-vertex@2.2.27': + resolution: {integrity: sha512-iDGX/2yrU4OOL1p/ENpfl3MWxuqp9/bE22Z8Ip4DtLCUx6ismUNtrKO357igM1/3jrM6t9C6egCPniHqBsHOJA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/google@1.2.22': + resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -748,6 +804,10 @@ packages: '@octokit/types@14.1.0': resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -966,6 +1026,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -1343,6 +1406,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ai@4.3.16: + resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1499,12 +1576,18 @@ packages: bare-events@2.5.4: resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} bent@7.3.12: resolution: {integrity: sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==} + bignumber.js@9.3.0: + resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1531,6 +1614,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1825,6 +1911,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -1855,6 +1944,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + electron-to-chromium@1.5.166: resolution: {integrity: sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==} @@ -2382,6 +2474,14 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2461,6 +2561,14 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2481,6 +2589,10 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2551,12 +2663,19 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} http-status-codes@2.3.0: resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -2850,6 +2969,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2876,6 +2998,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -2883,6 +3010,12 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3721,6 +3854,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -3966,6 +4105,9 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4231,6 +4373,11 @@ packages: resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} engines: {node: '>= 8'} + swr@2.3.4: + resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -4283,6 +4430,10 @@ packages: third-party-capital@1.0.20: resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -4474,6 +4625,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -4664,11 +4819,71 @@ packages: resolution: {integrity: sha512-LfOdrUvPB8ZoXtvOBz6DlNClfvi//b5d56mSWyJi7XbH/HfhOHfUhOqxhT/rUiR7yiktlunqRo+jY6y/cWC/5g==} engines: {node: '>= 12.0.0'} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: + '@ai-sdk/anthropic@1.2.12(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/google-vertex@2.2.27(zod@3.25.76)': + dependencies: + '@ai-sdk/anthropic': 1.2.12(zod@3.25.76) + '@ai-sdk/google': 1.2.22(zod@3.25.76) + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + google-auth-library: 9.15.1 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + - supports-color + + '@ai-sdk/google@1.2.22(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.76 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + react: 19.1.0 + swr: 2.3.4(react@19.1.0) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.76 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -4876,12 +5091,12 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 - '@faustwp/blocks@6.1.2(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@faustwp/core@3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@wordpress/style-engine@2.11.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@faustwp/blocks@6.1.2(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@faustwp/core@3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@wordpress/style-engine@2.11.0)(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@apollo/client': 3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@faustwp/core': 3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@faustwp/core': 3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@wordpress/style-engine': 2.11.0 - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -4903,7 +5118,7 @@ snapshots: - webpack-bundle-analyzer - webpack-dev-server - '@faustwp/core@3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@faustwp/core@3.2.3(@apollo/client@3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@apollo/client': 3.13.8(@types/react@18.3.12)(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@wordpress/hooks': 3.58.0 @@ -4916,7 +5131,7 @@ snapshots: js-cookie: 3.0.5 js-sha256: 0.9.0 lodash: 4.17.21 - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) transitivePeerDependencies: @@ -5191,9 +5406,9 @@ snapshots: '@next/swc-win32-x64-msvc@15.3.5': optional: true - '@next/third-parties@15.3.5(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + '@next/third-parties@15.3.5(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': dependencies: - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 third-party-capital: 1.0.20 @@ -5302,6 +5517,8 @@ snapshots: dependencies: '@octokit/openapi-types': 25.1.0 + '@opentelemetry/api@1.9.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5547,6 +5764,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/diff-match-patch@1.0.36': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -5917,10 +6136,10 @@ snapshots: '@babel/runtime': 7.27.6 change-case: 4.1.2 - '@wpengine/atlas-next@3.0.0(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(node-fetch@2.7.0)': + '@wpengine/atlas-next@3.0.0(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(node-fetch@2.7.0)': dependencies: '@wpengine/edge-cache': 1.3.2 - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) node-fetch: 2.7.0 '@wpengine/edge-cache@1.3.2': {} @@ -5953,6 +6172,20 @@ snapshots: acorn@8.15.0: {} + agent-base@7.1.3: {} + + ai@4.3.16(react@19.1.0)(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.76 + optionalDependencies: + react: 19.1.0 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -6147,6 +6380,8 @@ snapshots: bare-events@2.5.4: optional: true + base64-js@1.5.1: {} + before-after-hook@4.0.0: {} bent@7.3.12: @@ -6155,6 +6390,8 @@ snapshots: caseless: 0.12.0 is-stream: 2.0.1 + bignumber.js@9.3.0: {} + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -6186,6 +6423,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} builtin-modules@3.3.0: {} @@ -6467,6 +6706,8 @@ snapshots: dependencies: dequal: 2.0.3 + diff-match-patch@1.0.5: {} + diff@5.2.0: {} dir-glob@3.0.1: @@ -6496,6 +6737,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + electron-to-chromium@1.5.166: {} electron-to-chromium@1.5.182: {} @@ -7321,6 +7566,26 @@ snapshots: functions-have-names@1.2.3: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + get-caller-file@2.0.5: {} get-east-asian-width@1.3.0: {} @@ -7422,6 +7687,20 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -7435,6 +7714,14 @@ snapshots: graphql@16.11.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -7573,10 +7860,19 @@ snapshots: dependencies: lru-cache: 10.4.3 + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} http-status-codes@2.3.0: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + husky@9.1.7: {} ignore@5.3.2: {} @@ -7837,6 +8133,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.0 + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -7856,6 +8156,12 @@ snapshots: minimist: 1.2.8 optional: true + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.4.1 + diff-match-patch: 1.0.5 + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -7869,6 +8175,17 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8551,15 +8868,15 @@ snapshots: next-secure-headers@2.2.0: {} - next-sitemap@4.2.3(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): + next-sitemap@4.2.3(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.3.5 '@swc/counter': 0.1.3 @@ -8579,6 +8896,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.3.5 '@next/swc-win32-arm64-msvc': 15.3.5 '@next/swc-win32-x64-msvc': 15.3.5 + '@opentelemetry/api': 1.9.0 sharp: 0.34.2 transitivePeerDependencies: - '@babel/core' @@ -8921,6 +9239,24 @@ snapshots: react-is@16.13.1: {} + react-markdown@10.1.0(@types/react@18.3.12)(react@19.1.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.12 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react@19.1.0: {} read-package-json-fast@3.0.2: @@ -9289,6 +9625,8 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 + secure-json-parse@2.7.0: {} + semver@6.3.1: {} semver@7.7.1: {} @@ -9624,6 +9962,12 @@ snapshots: svelte@3.59.2: {} + swr@2.3.4(react@19.1.0): + dependencies: + dequal: 2.0.3 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + symbol-observable@4.0.0: {} synckit@0.11.8: @@ -9673,6 +10017,8 @@ snapshots: third-party-capital@1.0.20: {} + throttleit@2.1.0: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) @@ -9943,6 +10289,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@9.0.1: {} + uvu@0.5.6: dependencies: dequal: 2.0.3 @@ -10209,4 +10557,10 @@ snapshots: compress-commons: 5.0.3 readable-stream: 3.6.2 + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + zwitch@2.0.4: {} diff --git a/src/app/api/chat/route.js b/src/app/api/chat/route.js new file mode 100644 index 00000000..15102171 --- /dev/null +++ b/src/app/api/chat/route.js @@ -0,0 +1,106 @@ +import { env } from "node:process"; +import { createVertex } from "@ai-sdk/google-vertex"; +import { streamText, convertToCoreMessages } from "ai"; +import { StatusCodes, ReasonPhrases } from "http-status-codes"; +import { smartSearchTool } from "@/lib/rag.mjs"; + +// Ensure all required environment variables are set +if (!env.GOOGLE_VERTEX_PROJECT) { + throw new Error("GOOGLE_VERTEX_PROJECT is not set"); +} + +if (!env.GOOGLE_VERTEX_LOCATION) { + throw new Error("GOOGLE_VERTEX_LOCATION is not set"); +} + +if (!env.GOOGLE_VERTEX_CLIENT_EMAIL) { + throw new Error("GOOGLE_VERTEX_CLIENT_EMAIL is not set"); +} + +if (!env.GOOGLE_VERTEX_PRIVATE_KEY) { + throw new Error("GOOGLE_VERTEX_PRIVATE_KEY is not set"); +} + +if (!env.GOOGLE_VERTEX_PRIVATE_KEY.includes("-----BEGIN PRIVATE KEY-----")) { + throw new Error("GOOGLE_VERTEX_PRIVATE_KEY is not formatted correctly"); +} + +const vertex = createVertex({ + project: env.GOOGLE_VERTEX_PROJECT, + location: env.GOOGLE_VERTEX_LOCATION, + googleAuthOptions: { + credentials: { + client_email: env.GOOGLE_VERTEX_CLIENT_EMAIL, + private_key: env.GOOGLE_VERTEX_PRIVATE_KEY.replaceAll( + String.raw`\n`, + "\n", + ), // Ensure newlines are correctly formatted + }, + }, +}); + +const smartSearchPrompt = ` + - You can use the 'smartSearchTool' to find information relating to Faust. + - WP Engine Smart Search is a powerful tool for finding information about Faust. + - After the 'smartSearchTool' provides results (even if it's an error or no information found) + - You MUST then formulate a conversational response to the user based on those results but also use the tool if the users query is deemed plausible. + - If search results are found, summarize them for the user. + - If no information is found or an error occurs, inform the user clearly. + - IMPORTANT: Don't prefix root-relative links in post_url so client-side routing works. If you find links other places that are at the "faustjs.org" domain, you can make them root-relative. +`; + +const systemPromptContent = ` + - You are a friendly and helpful AI assistant that provides Developers help with their coding tasks and learning, as relevant to Faust.js, WPGraphQL, and headless WordPress. + - Format your responses using Github Flavored Markdown. + - Make sure to format links as [link text](path). + - Make sure to link out to the source of the information you provide. + - Prefer new information over old information. + - Do not invent information. Stick to the data provided by the tool. +`; + +export async function POST(req) { + try { + const { messages } = await req.json(); + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return new Response(ReasonPhrases.BAD_REQUEST, { + status: StatusCodes.BAD_REQUEST, + }); + } + + const coreMessages = convertToCoreMessages(messages); + + const response = await streamText({ + model: vertex("gemini-2.5-flash"), + system: [systemPromptContent, smartSearchPrompt].join("\n"), + messages: coreMessages, + tools: { + smartSearchTool, + }, + onError: (error) => { + console.error("Error during streaming:", error); + return new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, { + status: StatusCodes.INTERNAL_SERVER_ERROR, + }); + }, + onToolCall: async (toolCall) => { + console.log("Tool call initiated:", toolCall); + }, + onStepFinish: async (result) => { + if (result.usage) { + console.log( + `[Token Usage] Prompt tokens: ${result.usage.promptTokens}, Completion tokens: ${result.usage.completionTokens}, Total tokens: ${result.usage.totalTokens}`, + ); + } + }, + maxSteps: 5, + }); + + return response.toDataStreamResponse(); + } catch (error) { + console.error("Error in chat API:", error); + return new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, { + status: StatusCodes.INTERNAL_SERVER_ERROR, + }); + } +} diff --git a/src/components/chat/chat-button.jsx b/src/components/chat/chat-button.jsx new file mode 100644 index 00000000..1759775a --- /dev/null +++ b/src/components/chat/chat-button.jsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react"; +import { + HiOutlineChatBubbleLeftRight, + HiOutlineXCircle, +} from "react-icons/hi2"; +import { useChatDialog } from "./state"; +import { sendChatToggleEvent } from "@/lib/analytics.mjs"; +import { classNames } from "@/utils/strings"; + +export default function ChatButton() { + const { dialog } = useChatDialog(); + const [isOpen, setIsOpen] = useState(false); + const [wasEverOpen, setWasEverOpen] = useState(false); + + useEffect(() => { + const dialogElement = dialog.current; + + const handleDialogToggle = () => { + setIsOpen(!isOpen); + setWasEverOpen(true); + + sendChatToggleEvent({ + is_open: !isOpen, + }); + }; + + dialogElement?.addEventListener("toggle", handleDialogToggle); + + return () => { + dialogElement?.removeEventListener("toggle", handleDialogToggle); + }; + }, [dialog, isOpen, setIsOpen]); + + return ( +
+ + ); +} diff --git a/src/components/chat/chat-dialog.jsx b/src/components/chat/chat-dialog.jsx new file mode 100644 index 00000000..c7ba1721 --- /dev/null +++ b/src/components/chat/chat-dialog.jsx @@ -0,0 +1,65 @@ +import { useChat } from "ai/react"; +import { useEffect } from "react"; +import { HiXCircle } from "react-icons/hi2"; +import { useChatDialog } from "./state"; +import Chat from "@/components/chat/chat"; +import "./chat.css"; + +export default function ChatDialog() { + const { dialog } = useChatDialog(); + const { + messages, + input, + handleInputChange, + handleSubmit, + setMessages, + status, + } = useChat(); + + useEffect(() => { + if (messages.length === 0) { + setMessages([ + { + role: "assistant", + content: + "Hey there! I'm an AI driven chat assistant here to help you with Faust.js! I'm trained on the documentation and can help you with coding tasks, learning, and more. What can I assist you with today?", + id: "welcome-intro", + }, + ]); + } + }, [messages, setMessages]); + + return ( + + +
+ +
+
+ ); +} diff --git a/src/components/chat/chat-input.jsx b/src/components/chat/chat-input.jsx new file mode 100644 index 00000000..e759bd55 --- /dev/null +++ b/src/components/chat/chat-input.jsx @@ -0,0 +1,34 @@ +import { HiOutlinePaperAirplane, HiOutlineArrowPath } from "react-icons/hi2"; + +export default function Input({ input, handleInputChange, status }) { + const isReady = status === "ready"; + const isSubmitted = status === "submitted"; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/chat/chat-link.jsx b/src/components/chat/chat-link.jsx new file mode 100644 index 00000000..a824b411 --- /dev/null +++ b/src/components/chat/chat-link.jsx @@ -0,0 +1,19 @@ +import Link from "@/components/link"; +import { sendSelectItemEvent } from "@/lib/analytics.mjs"; + +export default function ChatLink(props) { + return ( + { + sendSelectItemEvent({ + list: { name: "Chat Messages", id: "chat-messages" }, + item: { + item_id: props.href, + item_name: props.children, + }, + }); + }} + /> + ); +} diff --git a/src/components/chat/chat.css b/src/components/chat/chat.css new file mode 100644 index 00000000..74a40e64 --- /dev/null +++ b/src/components/chat/chat.css @@ -0,0 +1,27 @@ +.gemini-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + /* Choose a font that matches, Arial is a common sans-serif */ + /* font-size: 100px; */ + /* Adjust size as needed */ + /* Gradient: Adjust colors and angles as needed */ + background: linear-gradient(to right, #6b50d2, #c25cb6, #ea668a); + /* + Color Hex codes approximated from the image: + Left (Blue-Purple): #6B50D2 + Middle (Pink-Purple): #C25CB6 + Right (Reddish-Pink): #EA668A + */ + + /* Crucial properties for text gradient */ + -webkit-background-clip: text; + /* For Safari/Chrome */ + background-clip: text; + color: transparent; + /* Makes the original text color transparent */ + + /* Optional: A slight text shadow can make it pop on some backgrounds */ + /* text-shadow: 2px 2px 4px rgba(0,0,0,0.1); */ +} diff --git a/src/components/chat/chat.jsx b/src/components/chat/chat.jsx new file mode 100644 index 00000000..eb8912ba --- /dev/null +++ b/src/components/chat/chat.jsx @@ -0,0 +1,34 @@ +import ChatInput from "./chat-input"; +import Messages from "./messages"; +import { sendChatMessageEvent } from "@/lib/analytics.mjs"; + +export default function Chat({ + input, + handleInputChange, + handleMessageSubmit, + status, + messages, +}) { + return ( +
+ +
{ + sendChatMessageEvent({ + message: input, + }); + + return handleMessageSubmit(event); + }} + className="absolute bottom-0 left-0 w-[calc(100%-theme(spacing.[1.5]))] bg-gradient-to-b from-transparent via-gray-800 to-gray-800 p-4 md:p-6" + > + + +
+ ); +} diff --git a/src/components/chat/messages.jsx b/src/components/chat/messages.jsx new file mode 100644 index 00000000..28da432b --- /dev/null +++ b/src/components/chat/messages.jsx @@ -0,0 +1,57 @@ +import { useEffect, useRef } from "react"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import ChatLink from "./chat-link"; +import { classNames } from "@/utils/strings"; + +export default function Messages({ messages, className }) { + const messagesEndReference = useRef(null); + useEffect(() => { + messagesEndReference.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + return ( +
+ {messages.map((message) => { + const isAssistant = message.role === "assistant"; + const isLoading = message.content === ""; + return ( +
+ {isLoading ? ( +
+
+
+
+
+ ) : ( + + {message.content} + + )} +
+ ); + })} +
+
+ ); +} diff --git a/src/components/chat/state.jsx b/src/components/chat/state.jsx new file mode 100644 index 00000000..5f767efa --- /dev/null +++ b/src/components/chat/state.jsx @@ -0,0 +1,15 @@ +import { createContext, useContext, useMemo, useRef } from "react"; + +const ChatContext = createContext(); + +export const useChatDialog = () => { + return useContext(ChatContext); +}; + +export const ChatProvider = ({ children }) => { + const dialog = useRef(null); + + const value = useMemo(() => ({ dialog }), [dialog]); + + return {children}; +}; diff --git a/src/components/docs-recommendations.jsx b/src/components/docs-recommendations.jsx index 061520eb..c3c25261 100644 --- a/src/components/docs-recommendations.jsx +++ b/src/components/docs-recommendations.jsx @@ -43,7 +43,7 @@ export default function DocsRecommended({ docID, count = 5 }) { ref={ref} className="docs-recommended bg-blue-1100/20 rounded-lg p-6 text-white shadow-lg ring-1 ring-blue-500/10" > -

Related

+

Related Content

{loading ? ( diff --git a/src/components/footer.jsx b/src/components/footer.jsx index 08663897..57248d87 100644 --- a/src/components/footer.jsx +++ b/src/components/footer.jsx @@ -9,15 +9,15 @@ export default function Footer() {

- Powered by{" "} + Powered by  Faust.js - {" "} - & WP Engine's{" "} + +  & WP Engine's 

+ {children}
+
); } diff --git a/src/components/mdx-components.jsx b/src/components/mdx-components.jsx index 5b5f2587..cc45cb4e 100644 --- a/src/components/mdx-components.jsx +++ b/src/components/mdx-components.jsx @@ -1,7 +1,7 @@ import Heading from "@/components/heading"; import CustomLink from "@/components/link"; -export function useMDXComponents(components) { +export function getMDXComponents(components) { return { a: (props) => , h1: (props) => , diff --git a/src/components/search/search-box.jsx b/src/components/search/search-box.jsx index ce897357..2805471b 100644 --- a/src/components/search/search-box.jsx +++ b/src/components/search/search-box.jsx @@ -91,6 +91,7 @@ export default function SearchBar() { return (
{ + console.log(`[Tool Execution] Searching with query: "${query}"`); + try { + const context = await getContext(query); + + if (context.errors && context.errors.length > 0) { + console.error( + "[Tool Execution] Error fetching context:", + context.errors, + ); + // Return a structured error message that the LLM can understand + return { + error: `Error fetching context: ${context.errors[0].message}`, + }; + } + + if (context?.data?.similarity?.docs?.length === 0) { + console.warn("[Tool Execution] No documents found for query:", query); + return { + searchResults: "No relevant information found for your query.", + }; + } + + const formattedResults = normalizeSmartSearchResponse( + context.data.similarity.docs, + ); + + return { searchResults: formattedResults }; // Return the formatted string + } catch (error) { + console.error("[Tool Execution] Exception:", error); + return { error: `An error occurred while searching: ${error.message}` }; + } + }, +}); diff --git a/src/lib/smart-search.mjs b/src/lib/smart-search.mjs index 7439492a..479ea6ac 100644 --- a/src/lib/smart-search.mjs +++ b/src/lib/smart-search.mjs @@ -1,5 +1,41 @@ +import { env } from "node:process"; import { URL } from "node:url"; +const url = env.NEXT_PUBLIC_SEARCH_ENDPOINT ?? ""; +const token = env.NEXT_SEARCH_ACCESS_TOKEN ?? ""; + +// Field might need to be adjusted to include the correct field for your search index, e.g. "content", "post_title", etc. +export async function getContext(message) { + const query = `query GetContext($message: String!) { + similarity( + input: { + minScore: 1, + nearest: { + text: $message, + field: "post_content" + } + }) { + total + docs { + id + data + score + } + } + }`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ query, variables: { message } }), + }); + + return response.json(); +} + function cleanPath(filePath) { return ( filePath @@ -17,7 +53,6 @@ export function normalizeSmartSearchResponse(results) { return results.map((result) => { const { id, data } = result; - // console.log("Data:", result); switch (data.post_type) { case "mdx_doc": { const path = data.post_url ? cleanPath(data.post_url) : "/"; @@ -27,6 +62,7 @@ export function normalizeSmartSearchResponse(results) { title: data.post_title, href: path, type: data.post_type, + content: data.post_content, }; } @@ -37,6 +73,7 @@ export function normalizeSmartSearchResponse(results) { title: data.post_title, href: new URL(data.post_url).pathname, type: data.post_type, + content: data.post_content, }; } diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 6232284a..30c53fd7 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -4,6 +4,7 @@ import { WordPressBlocksProvider } from "@faustwp/blocks"; import { FaustProvider } from "@faustwp/core"; import { GoogleAnalytics } from "@next/third-parties/google"; import { useRouter } from "next/router"; +import { ChatProvider } from "@/components/chat/state"; import DocsLayout from "@/components/docs-layout"; import Layout from "@/components/layout"; import { SearchProvider } from "@/components/search/state"; @@ -22,18 +23,20 @@ export default function MyApp({ Component, pageProps }) { - {/* eslint-disable-next-line unicorn/no-null */} - - - {isDocsRoute ? ( - + + {/* eslint-disable-next-line unicorn/no-null */} + + + {isDocsRoute ? ( + + + + ) : ( - - ) : ( - - )} - - + )} + + + ); diff --git a/src/pages/docs/[[...slug]].jsx b/src/pages/docs/[[...slug]].jsx index 2a654ddf..e84eb971 100644 --- a/src/pages/docs/[[...slug]].jsx +++ b/src/pages/docs/[[...slug]].jsx @@ -1,6 +1,6 @@ import path from "node:path"; import { MDXClient } from "next-mdx-remote-client"; -import { useMDXComponents } from "@/components/mdx-components"; +import { getMDXComponents } from "@/components/mdx-components"; import { getParsedDoc, getDocsNav, @@ -8,7 +8,7 @@ import { } from "@/lib/remote-mdx-files.mjs"; export default function Doc({ source }) { - return ; + return ; } export async function getStaticProps({ params }) { diff --git a/src/pages/global.css b/src/pages/global.css index 7ea0db04..b436ae64 100644 --- a/src/pages/global.css +++ b/src/pages/global.css @@ -95,6 +95,19 @@ linear-gradient(210deg, #111827ff 0%, #29174cff 100%); --font-inter: Inter, serif; + + --animate-think: think 2s ease-in-out infinite; + + @keyframes think { + 0%, + 80%, + 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } + } } /*