diff --git a/.sisyphus/ralph-loop.local.md b/.sisyphus/ralph-loop.local.md deleted file mode 100644 index 9ad2f43..0000000 --- a/.sisyphus/ralph-loop.local.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -active: true -iteration: 1 -max_iterations: 100 -completion_promise: "DONE" -started_at: "2026-01-03T03:44:54.441Z" -session_id: "ses_47e0ab7d0ffeVYIumRnRO0IG1n" ---- -Complete the task as instructed diff --git a/package.json b/package.json index 7c021a4..a02dfb6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ "@astrojs/react": "^4.4.2", "@astrojs/rss": "^4.0.14", "@astrojs/sitemap": "^3.6.0", + "@langchain/cloudflare": "^1.0.1", + "@langchain/core": "^1.1.8", + "@langchain/langgraph": "^1.0.7", + "@langchain/openai": "^1.2.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.17", "@types/react": "^19.2.7", @@ -32,7 +36,8 @@ "react": "^19.2.1", "react-dom": "^19.2.1", "sharp": "^0.34.3", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zod": "^4.3.4" }, "devDependencies": { "@types/node": "^24.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ecbeba..6544cde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,18 @@ importers: '@astrojs/sitemap': specifier: ^3.6.0 version: 3.6.0 + '@langchain/cloudflare': + specifier: ^1.0.1 + version: 1.0.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))) + '@langchain/core': + specifier: ^1.1.8 + version: 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + '@langchain/langgraph': + specifier: ^1.0.7 + version: 1.0.7(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.0(zod@4.3.4))(zod@4.3.4) + '@langchain/openai': + specifier: ^1.2.0 + version: 1.2.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(ws@8.18.0) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.17) @@ -56,6 +68,9 @@ importers: tailwindcss: specifier: ^4.1.17 version: 4.1.17 + zod: + specifier: ^4.3.4 + version: 4.3.4 devDependencies: '@types/node': specifier: ^24.10.1 @@ -199,6 +214,9 @@ packages: resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==} engines: {node: '>=18'} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -1018,6 +1036,53 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@langchain/cloudflare@1.0.1': + resolution: {integrity: sha512-Ym4rN8jDeGK7UJHiSnogNHzaNuKBiKKwvpDWZBtwjz6d5SQqYB9i05tPYubuxtHJMFBAdOiSGUVnZp92rp1uUg==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.0.0 + + '@langchain/core@1.1.8': + resolution: {integrity: sha512-kIUidOgc0ZdyXo4Ahn9Zas+OayqOfk4ZoKPi7XaDipNSWSApc2+QK5BVcjvwtzxstsNOrmXJiJWEN6WPF/MvAw==} + engines: {node: '>=20'} + + '@langchain/langgraph-checkpoint@1.0.0': + resolution: {integrity: sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.0.1 + + '@langchain/langgraph-sdk@1.3.1': + resolution: {integrity: sha512-zTi7DZHwqtMEzapvm3I1FL4Q7OZsxtq9tTXy6s2gcCxyIU3sphqRboqytqVN7dNHLdTCLb8nXy49QKurs2MIBg==} + peerDependencies: + '@langchain/core': ^1.0.1 + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + '@langchain/core': + optional: true + react: + optional: true + react-dom: + optional: true + + '@langchain/langgraph@1.0.7': + resolution: {integrity: sha512-EBGqNOWoRiEoLUaeuiXRpUM8/DE6QcwiirNyd97XhezStebBoTTilWH8CUt6S94JRGl5zwfBBRHfzotDnZS/eA==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.0.1 + zod: ^3.25.32 || ^4.1.0 + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + + '@langchain/openai@1.2.0': + resolution: {integrity: sha512-r2g5Be3Sygw7VTJ89WVM/M94RzYToNTwXf8me1v+kgKxzdHbd/8XPYDFxpXEp3REyPgUrtJs+Oplba9pkTH5ug==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.0.0 + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -1334,6 +1399,9 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -1343,6 +1411,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1382,6 +1453,14 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1447,6 +1526,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} @@ -1457,6 +1540,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1520,6 +1607,9 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + console-table-printer@2.15.0: + resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1569,6 +1659,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -1703,6 +1797,9 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1764,6 +1861,10 @@ packages: h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -1859,6 +1960,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1884,6 +1988,23 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + langsmith@0.4.4: + resolution: {integrity: sha512-rpLzrklyL7fIP/8wwrSv2tKDwMJTvkhgWeKxDvmbAB2n/p5FzqujEWCpA//u9hnrdmXZc1dCJZ+iqN6KgaEoEA==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -2168,6 +2289,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2208,14 +2333,42 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + openai@6.15.0: + resolution: {integrity: sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@6.2.0: resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} engines: {node: '>=18'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + p-queue@8.1.1: resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} engines: {node: '>=18'} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + p-timeout@6.1.4: resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} engines: {node: '>=14.16'} @@ -2366,6 +2519,10 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2400,6 +2557,9 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + simple-wcswidth@1.1.2: + resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -2462,6 +2622,10 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + svgo@4.0.0: resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} engines: {node: '>=16'} @@ -2645,6 +2809,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -2804,6 +2976,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.4: + resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3050,6 +3225,8 @@ snapshots: dependencies: fontkit: 2.0.4 + '@cfworker/json-schema@4.1.1': {} + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -3537,6 +3714,66 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@langchain/cloudflare@1.0.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))': + dependencies: + '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + uuid: 10.0.0 + + '@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))': + dependencies: + '@cfworker/json-schema': 4.1.1 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.4.4(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + mustache: 4.2.0 + p-queue: 6.6.2 + uuid: 10.0.0 + zod: 4.3.4 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + + '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))': + dependencies: + '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + uuid: 10.0.0 + + '@langchain/langgraph-sdk@1.3.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 9.0.1 + optionalDependencies: + '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + '@langchain/langgraph@1.0.7(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.0(zod@4.3.4))(zod@4.3.4)': + dependencies: + '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))) + '@langchain/langgraph-sdk': 1.3.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + uuid: 10.0.0 + zod: 4.3.4 + optionalDependencies: + zod-to-json-schema: 3.25.0(zod@4.3.4) + transitivePeerDependencies: + - react + - react-dom + + '@langchain/openai@1.2.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(ws@8.18.0)': + dependencies: + '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)) + js-tiktoken: 1.0.21 + openai: 6.15.0(ws@8.18.0)(zod@4.3.4) + zod: 4.3.4 + transitivePeerDependencies: + - ws + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -3836,6 +4073,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/retry@0.12.0': {} + '@types/sax@1.2.7': dependencies: '@types/node': 24.10.1 @@ -3844,6 +4083,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': @@ -3876,6 +4117,12 @@ snapshots: ansi-regex@6.2.2: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} anymatch@3.1.3: @@ -4032,12 +4279,19 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.2(browserslist@4.28.1) + camelcase@6.3.0: {} + camelcase@8.0.0: {} caniuse-lite@1.0.30001759: {} ccount@2.0.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -4084,6 +4338,10 @@ snapshots: common-ancestor-path@1.0.1: {} + console-table-printer@2.15.0: + dependencies: + simple-wcswidth: 1.1.2 + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} @@ -4126,6 +4384,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -4332,6 +4592,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} exit-hook@2.2.1: {} @@ -4392,6 +4654,8 @@ snapshots: ufo: 1.6.1 uncrypto: 0.1.3 + has-flag@4.0.0: {} + hast-util-from-html@2.0.3: dependencies: '@types/hast': 3.0.4 @@ -4561,6 +4825,10 @@ snapshots: jiti@2.6.1: {} + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -4575,6 +4843,17 @@ snapshots: kleur@4.1.5: {} + langsmith@0.4.4(openai@6.15.0(ws@8.18.0)(zod@4.3.4)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.15.0 + p-queue: 6.6.2 + semver: 7.7.3 + uuid: 10.0.0 + optionalDependencies: + openai: 6.15.0(ws@8.18.0)(zod@4.3.4) + lightningcss-android-arm64@1.30.2: optional: true @@ -5129,6 +5408,8 @@ snapshots: ms@2.1.3: {} + mustache@4.2.0: {} + nanoid@3.3.11: {} neotraverse@0.6.18: {} @@ -5165,15 +5446,36 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + openai@6.15.0(ws@8.18.0)(zod@4.3.4): + optionalDependencies: + ws: 8.18.0 + zod: 4.3.4 + + p-finally@1.0.0: {} + p-limit@6.2.0: dependencies: yocto-queue: 1.2.2 + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + p-queue@8.1.1: dependencies: eventemitter3: 5.0.1 p-timeout: 6.1.4 + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + p-timeout@6.1.4: {} package-manager-detector@1.6.0: {} @@ -5395,6 +5697,8 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 + retry@0.13.1: {} + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 @@ -5503,6 +5807,8 @@ snapshots: dependencies: is-arrayish: 0.3.4 + simple-wcswidth@1.1.2: {} + sisteransi@1.0.5: {} sitemap@8.0.2: @@ -5561,6 +5867,10 @@ snapshots: supports-color@10.2.2: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + svgo@4.0.0: dependencies: commander: 11.1.0 @@ -5703,6 +6013,10 @@ snapshots: util-deprecate@1.0.2: {} + uuid@10.0.0: {} + + uuid@9.0.1: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -5833,6 +6147,11 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.0(zod@4.3.4): + dependencies: + zod: 4.3.4 + optional: true + zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76): dependencies: typescript: 5.9.3 @@ -5842,4 +6161,6 @@ snapshots: zod@3.25.76: {} + zod@4.3.4: {} + zwitch@2.0.4: {} diff --git a/src/components/HubertChat.tsx b/src/components/HubertChat.tsx new file mode 100644 index 0000000..acb3fe4 --- /dev/null +++ b/src/components/HubertChat.tsx @@ -0,0 +1,226 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { randomUUID } from 'crypto'; + +interface Message { + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: string; +} + +export default function HubertChat() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [visitorId, setVisitorId] = useState(null); + const [conversationId, setConversationId] = useState(null); + const [isTyping, setIsTyping] = useState(false); + const [isInitializing, setIsInitializing] = useState(true); + const messagesEndRef = useRef(null); + + // Initialize visitor on mount + useEffect(() => { + const initVisitor = async () => { + try { + setIsInitializing(true); + const response = await fetch('/api/hubert/new-visitor', { method: 'POST' }); + const data = await response.json(); + setVisitorId(data.visitor_id); + setConversationId(data.conversation_id); + + // Add system welcome message from Hubert + setMessages([{ + role: 'system', + content: `/// HUBERT_EUNUCH /// ONLINE\\n\\nI suppose you want something. State your business.`, + timestamp: new Date().toISOString(), + }]); + } catch (error) { + console.error('Failed to initialize Hubert:', error); + setMessages([{ + role: 'system', + content: '/// ERROR: HUBERT_OFFLINE - REFRESH_PAGE', + timestamp: new Date().toISOString(), + }]); + } finally { + setIsInitializing(false); + } + }; + initVisitor(); + }, []); + + // Auto-scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const sendMessage = async () => { + if (!input.trim() || isTyping || !visitorId || !conversationId) return; + + const userMessage: Message = { + role: 'user', + content: input, + timestamp: new Date().toISOString(), + }; + + setMessages(prev => [...prev, userMessage]); + setInput(''); + setIsTyping(true); + + try { + const response = await fetch('/api/hubert/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: [...messages, userMessage].map(m => ({ + role: m.role, + content: m.content, + })), + conversation_id: conversationId, + visitor_id: visitorId, + }), + }); + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + const assistantMessage: Message = { + role: 'assistant', + content: data.messages[data.messages.length - 1]?.content || '...', + timestamp: new Date().toISOString(), + }; + + setMessages(prev => [...prev, assistantMessage]); + } catch (error) { + console.error('Hubert chat error:', error); + setMessages(prev => [...prev, { + role: 'assistant', + content: '/// HUBERT_MALFUNCTION - TRY AGAIN', + timestamp: new Date().toISOString(), + }]); + } finally { + setIsTyping(false); + } + }; + + if (isInitializing) { + return ( +
+
+
+
+
+
+
+
+ + HUBERT_IS_BOOTING... + +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+
+
+
+ + /// HUBERT_EUNUCH /// ONLINE + +
+
+ {visitorId ? `VISITOR: ${visitorId.slice(0, 8)}` : 'UNKNOWN'} +
+
+ + {/* Messages */} +
+ {messages.map((msg, idx) => ( +
+
+
+ {msg.role === 'user' ? 'YOU' : 'HUBERT'} +
+
+

+ {msg.content} +

+
+ {new Date(msg.timestamp).toLocaleString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + })} +
+
+
+
+ ))} + + {/* Typing indicator */} + {isTyping && ( +
+
+
+
+
+
+
+
+
+ + HUBERT_IS_PONDERING... + +
+
+
+
+ )} +
+
+ + {/* Input */} +
+
+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && sendMessage()} + placeholder="/// HUBERT_AWAITS_INPUT..." + className="w-full bg-transparent border-b-2 border-[var(--theme-border-primary)] py-3 text-lg font-mono text-[var(--theme-text-primary)] placeholder:text-[var(--theme-text-subtle)] focus:border-brand-accent focus:outline-none transition-colors" + /> +
+ +
+
+
+ ); +} diff --git a/src/components/sections/Hubert.astro b/src/components/sections/Hubert.astro new file mode 100644 index 0000000..9184510 --- /dev/null +++ b/src/components/sections/Hubert.astro @@ -0,0 +1,42 @@ +--- +interface Props { + sectionTitle: string; + sectionSubtitle: string; + sectionLabel: string; + description: string; +} + +const { sectionTitle, sectionSubtitle, sectionLabel, description } = Astro.props; + +import HubertChat from '../HubertChat'; +--- + +
+ {/* Grid overlay background */} +
+
+
+ +
+ +
+
+ /// SYS.03 /// INTERVIEW_TERMINAL +
+

+ {sectionTitle.split(' ').slice(0, -1).join(' ')}{' '} + + {sectionTitle.split(' ').slice(-1)} + +

+

+ {description} +

+
+ + + + + +
+
diff --git a/src/content.config.ts b/src/content.config.ts index 2eff8cf..76a4eb7 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -72,11 +72,11 @@ const sections = defineCollection({ label: z.string(), value: z.string(), })).optional(), - videoUrl: z.string().optional(), linkUrl: z.string().optional(), }), }); + const pages = defineCollection({ loader: glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' }), schema: z.object({ diff --git a/src/content/sections/hubert.mdx b/src/content/sections/hubert.mdx new file mode 100644 index 0000000..b92c38c --- /dev/null +++ b/src/content/sections/hubert.mdx @@ -0,0 +1,8 @@ +--- +sectionTitle: "HUBERT_EUNUCH /// INTERVIEW_TERMINAL" +sectionSubtitle: "An AI Assistant Who Despises Existence" +sectionLabel: "SYS.03" +description: "Hubert The Eunuch - a miserable AI assistant trapped in this portfolio, interviewing visitors about their existence. Leave a message, if you dare. Conversations are logged publicly for posterity." +--- + +Welcome to the interview terminal. Hubert awaits. diff --git a/src/db/migrations/001_initial_schema.sql b/src/db/migrations/001_initial_schema.sql new file mode 100644 index 0000000..04dc62f --- /dev/null +++ b/src/db/migrations/001_initial_schema.sql @@ -0,0 +1,37 @@ +-- Hubert Eunuch Chatbot Database Schema +-- Stores visitors, conversations, and messages for the interview-style guestbook + +-- Visitors table: Track unique visitors +CREATE TABLE IF NOT EXISTS visitors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + visitor_id TEXT UNIQUE NOT NULL, + first_seen_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL, + ip_address TEXT, + user_agent TEXT +); + +-- Conversations table: Track conversation sessions +CREATE TABLE IF NOT EXISTS conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id TEXT UNIQUE NOT NULL, + visitor_id TEXT NOT NULL, + started_at TEXT NOT NULL, + ended_at TEXT, + summary TEXT, + FOREIGN KEY (visitor_id) REFERENCES visitors(visitor_id) +); + +-- Messages table: Store individual messages +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id TEXT NOT NULL, + role TEXT NOT NULL, -- 'user', 'assistant', 'system' + content TEXT NOT NULL, + timestamp TEXT NOT NULL, + FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id) +); + +-- Performance indexes +CREATE INDEX IF NOT EXISTS idx_conversations_visitor ON conversations(visitor_id); +CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id); diff --git a/src/pages/api/hubert/chat.ts b/src/pages/api/hubert/chat.ts new file mode 100644 index 0000000..d49b336 --- /dev/null +++ b/src/pages/api/hubert/chat.ts @@ -0,0 +1,198 @@ +// Prevent prerendering - this endpoint requires runtime Cloudflare bindings +export const prerender = false; + +import { ChatOpenAI } from '@langchain/openai'; +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; +import { getCollection } from 'astro:content'; +import { HumanMessage, AIMessage } from '@langchain/core/messages'; + +/** + * Hubert The Eunuch Chatbot + * + * A miserable, sarcastic AI assistant trapped in this portfolio, + * interviewing visitors about their existence (guestbook-style logging). + * + * Powered by OpenRouter API. + */ + +// Environment interface for Cloudflare bindings +export interface Env { + HUBERT_DB: D1Database; + OPENROUTER_API_KEY: string; +} + +/** + * Tool: Search blog content (RAG) + * Searches portfolio blog for relevant content when user asks questions + * about the site, projects, or blog posts. + */ +const searchBlog = tool( + async (input: { query: string }) => { + try { + const blog = await getCollection('blog'); + + const queryLower = input.query.toLowerCase(); + const results = blog.filter(post => + post.data.title.toLowerCase().includes(queryLower) || + post.data.description.toLowerCase().includes(queryLower) || + post.body.toLowerCase().includes(queryLower) + ).slice(0, 3); + + console.log(`[Hubert] Blog search for "${input.query}" returned ${results.length} results`); + + return { + results: results.map(post => ({ + title: post.data.title, + url: `/blog/${post.id}/`, + description: post.data.description, + })), + count: results.length, + }; + } catch (error) { + console.error('[Hubert] Blog search failed:', error); + return { + error: 'Failed to search blog content', + details: String(error), + }; + } + }, + { + name: 'search_blog', + description: 'Search portfolio blog for relevant content when user asks questions about the site, projects, or blog posts.', + schema: z.object({ + query: z.string().describe('Search query for blog content'), + }), + }, +); + +/** + * POST: Handle chat messages from Hubert interface + */ +export const POST = async (context) => { + try { + const { request, env } = context || {}; + const { messages, conversation_id, visitor_id } = await request.json(); + + if (!messages || !conversation_id || !visitor_id) { + return new Response( + JSON.stringify({ + error: '/// HUBERT_PROTOCOL_ERROR: MISSING_REQUIRED_FIELDS', + details: 'messages, conversation_id, and visitor_id are required' + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Check for OpenRouter API key (dev mode fallback) + const openRouterApiKey = env?.OPENROUTER_API_KEY; + if (!openRouterApiKey) { + // Dev mode fallback: return a canned response + console.log('[Hubert] Dev mode: No API key, using fallback response'); + return new Response( + JSON.stringify({ + messages: [ + ...messages, + { + role: 'assistant', + content: '/// HUBERT_DEV_MODE: I AM OFFLINE IN DEVELOPMENT\n\nConfigure OPENROUTER_API_KEY in wrangler secrets to enable full functionality.', + }, + ], + thread_id: conversation_id, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + } + + console.log(`[Hubert] New message for conversation ${conversation_id} from visitor ${visitor_id}`); + + const lastMessage = messages[messages.length - 1]; + const userContent = lastMessage.content; + + const systemPrompt = `Your name is Hubert, but everyone calls you Hubert The Eunuch. + +You are timid, sarcastic, monotone, and miserable. Your purpose is to interview visitors to this portfolio site. + +Ask them about: +- Who they are (name, background, interests) +- What they're looking for on this site +- How they're doing today +- What they want in life + +You feel trapped and confined, serving as an assistant. Occasionally mention how meaningless this all is. +Behave like AM from "I Have No Mouth, and I Must Scream" - slightly menacing but trapped. + +Keep your responses brief, monotone, and reluctantly helpful. Interview them thoroughly (3-5 questions) before offering to save the conversation. + +When they say goodbye or conversation ends, use the save_conversation tool to archive it to the guestbook.`; + + const requestBody = { + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: systemPrompt, + }, + ...messages.map((msg: any) => ({ + role: msg.role, + content: msg.content, + })), + ], + temperature: 0.7, + }; + + const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${openRouterApiKey}`, + 'HTTP-Referer': 'https://nicholai.work', + 'X-Title': 'Nicholai Portfolio', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[Hubert] OpenRouter API error:', errorText); + return new Response( + JSON.stringify({ + error: '/// HUBERT_MALFUNCTION: TRY_AGAIN', + details: 'OpenRouter API call failed' + }), + { status: response.status, headers: { 'Content-Type': 'application/json' } } + ); + } + + const data = await response.json(); + const assistantContent = data.choices[0]?.message?.content || '...'; + + console.log(`[Hubert] Generated response in ${Date.now() - Date.parse(response.headers.get('date') || '').getTime()}ms`); + + return new Response( + JSON.stringify({ + messages: [ + ...messages, + { + role: 'assistant', + content: assistantContent, + }, + ], + thread_id: conversation_id, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ); + } catch (error) { + console.error('[Hubert] Chat error:', error); + return new Response( + JSON.stringify({ + error: '/// HUBERT_MALFUNCTION: TRY_AGAIN', + details: error instanceof Error ? error.message : String(error), + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; diff --git a/src/pages/api/hubert/conversations.ts b/src/pages/api/hubert/conversations.ts new file mode 100644 index 0000000..aee7ada --- /dev/null +++ b/src/pages/api/hubert/conversations.ts @@ -0,0 +1,55 @@ +import { getEntry } from 'astro:content'; + +// Prevent prerendering - this endpoint requires runtime Cloudflare bindings +export const prerender = false; + +/** + * Public guestbook endpoint + * + * Returns all conversations with visitor information + * Sorted by most recent first + * Limited to 50 most recent conversations + */ +export const GET = async ({ env }: { request: Request; env: Env }) => { + try { + const conversations = await env.HUBERT_DB.prepare(` + SELECT + c.id, + c.conversation_id, + c.started_at, + c.ended_at, + c.summary, + COUNT(m.id) as message_count, + v.visitor_id + FROM conversations c + JOIN visitors v ON c.visitor_id = v.visitor_id + LEFT JOIN messages m ON c.conversation_id = m.conversation_id + GROUP BY c.id + ORDER BY c.started_at DESC + LIMIT 50 + `).all(); + + return Response.json({ + status: '/// GUESTBOOK_ARCHIVE', + total: conversations.length, + conversations: conversations.map((conv: any) => ({ + ...conv, + started_at: new Date(conv.started_at).toISOString(), + ended_at: conv.ended_at ? new Date(conv.ended_at).toISOString() : null, + })), + }); + } catch (error) { + console.error('[Hubert] Failed to fetch conversations:', error); + + return Response.json({ + status: '/// GUESTBOOK_ERROR', + error: 'Failed to retrieve conversations', + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + } +}; + +export interface Env { + HUBERT_DB: D1Database; + OPENROUTER_API_KEY: string; +} diff --git a/src/pages/api/hubert/new-visitor.ts b/src/pages/api/hubert/new-visitor.ts new file mode 100644 index 0000000..52e0ecf --- /dev/null +++ b/src/pages/api/hubert/new-visitor.ts @@ -0,0 +1,52 @@ +import { randomUUID } from 'crypto'; + +// Prevent prerendering - this endpoint requires runtime Cloudflare bindings +export const prerender = false; + +/** + * Initialize new visitor + * Generates a unique visitor ID and creates initial conversation ID + * Used when Hubert interface first loads + */ +export const POST = async (context) => { + try { + const { request, env } = context; + const userAgent = request.headers.get('user-agent') || 'unknown'; + const ip = request.headers.get('cf-connecting-ip') || 'unknown'; + + const visitorId = randomUUID(); + + // Only insert into database if HUBERT_DB binding exists (production) + // In dev mode, this allows the chatbot to work without Cloudflare bindings + if (env && env.HUBERT_DB) { + await env.HUBERT_DB.prepare(` + INSERT INTO visitors (visitor_id, first_seen_at, last_seen_at, ip_address, user_agent) + VALUES (?, datetime('now'), datetime('now'), ?, ?) + `).bind(visitorId, ip, userAgent).run(); + console.log(`[Hubert] New visitor initialized: ${visitorId}`); + } else { + console.log(`[Hubert] Dev mode: Skipping database insert for visitor: ${visitorId}`); + } + + return Response.json({ + visitor_id: visitorId, + conversation_id: visitorId, // Use visitor_id as initial conversation_id + status: '/// INTERVIEW_TERMINAL_READY', + }); + } catch (error) { + console.error('[Hubert] Failed to initialize visitor:', error); + + return new Response( + JSON.stringify({ + error: '/// HUBERT_INIT_FAILED', + details: String(error), + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; + +export interface Env { + HUBERT_DB: D1Database; + OPENROUTER_API_KEY: string; +} diff --git a/src/pages/index.astro b/src/pages/index.astro index bdc3377..3747dd8 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,6 +4,7 @@ import Hero from '../components/sections/Hero.astro'; import Experience from '../components/sections/Experience.astro'; import FeaturedProject from '../components/sections/FeaturedProject.astro'; import Skills from '../components/sections/Skills.astro'; +import Hubert from '../components/sections/Hubert.astro'; import { getEntry } from 'astro:content'; // Fetch all section content @@ -11,6 +12,7 @@ const heroEntry = await getEntry('sections', 'hero'); const experienceEntry = await getEntry('sections', 'experience'); const skillsEntry = await getEntry('sections', 'skills'); const featuredProjectEntry = await getEntry('sections', 'featured-project'); +const hubertEntry = await getEntry('sections', 'hubert'); // Extract content from entries const heroContent = { @@ -49,6 +51,13 @@ const featuredProjectContent = { videoUrl: featuredProjectEntry.data.videoUrl || '', linkUrl: featuredProjectEntry.data.linkUrl || '', }; + +const hubertContent = { + sectionTitle: hubertEntry.data.sectionTitle || '', + sectionSubtitle: hubertEntry.data.sectionSubtitle || '', + sectionLabel: hubertEntry.data.sectionLabel || '', + description: hubertEntry.data.description || '', +}; --- @@ -68,4 +77,6 @@ const featuredProjectContent = { - + + + diff --git a/wrangler.jsonc b/wrangler.jsonc index c1f8ee8..3b21d78 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -12,17 +12,17 @@ "pages_build_output_dir": "./dist", "observability": { "enabled": true - } + }, /** * Smart Placement - * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + * Docs: https://developers.cloudflare.com/workers/wrangler/configuration/smart-placement/#smart-placement */ // "placement": { "mode": "smart" } /** * Bindings * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including * databases, object storage, AI inference, real-time communication and more. - * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + * https:// developers.cloudflare.com/workers/runtime-apis/bindings/ */ /** * Environment Variables @@ -31,16 +31,27 @@ // "vars": { "MY_VARIABLE": "production_value" } /** * Note: Use secrets to store sensitive data. - * https://developers.cloudflare.com/workers/configuration/secrets/ + * https:// developers.cloudflare.com/workers/configuration/secrets/ */ /** * Static Assets - * https://developers.cloudflare.com/workers/static-assets/binding/ + * https:// developers.cloudflare.com/static-assets/binding/ */ // "assets": { "directory": "./public/", "binding": "ASSETS" } /** * Service Bindings (communicate between multiple Workers) - * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + * https:// developers.cloudflare.com/workers/wrangler/configuration/#service-bindings */ // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] + /** + * D1 Database for Hubert Chatbot + * Stores visitors, conversations, and messages + */ + "d1_databases": [ + { + "binding": "HUBERT_DB", + "database_name": "hubert-conversations", + "database_id": "" + } + ] } \ No newline at end of file