Compare commits
9 Commits
fe8a75629b
...
a7278c801a
| Author | SHA1 | Date | |
|---|---|---|---|
| a7278c801a | |||
| e46306389e | |||
| 269286763e | |||
| 1345012a6a | |||
| 64855131ba | |||
| ffbb86b2d9 | |||
| 95ff689ace | |||
| 9ef1027cc6 | |||
| d4959500a8 |
16
.claude/.fuse_hidden009df63e00000355
Normal file
16
.claude/.fuse_hidden009df63e00000355
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebFetch(domain:openrouter.ai)",
|
||||||
|
"Bash(node:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(pnpm build:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(pnpm add:*)",
|
||||||
|
"WebFetch(domain:substance.biohazardvfx.com)",
|
||||||
|
"WebFetch(domain:substrate.biohazardvfx.com)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
10
package.json
10
package.json
@ -22,19 +22,27 @@
|
|||||||
"@astrojs/react": "^4.4.2",
|
"@astrojs/react": "^4.4.2",
|
||||||
"@astrojs/rss": "^4.0.14",
|
"@astrojs/rss": "^4.0.14",
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@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/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"astro": "^5.16.4",
|
"astro": "^5.16.4",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"framer-motion": "^12.26.2",
|
||||||
"lunr": "^2.3.9",
|
"lunr": "^2.3.9",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"tailwindcss": "^4.1.17"
|
"tailwindcss": "^4.1.17",
|
||||||
|
"zod": "^4.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"wrangler": "^4.53.0"
|
"wrangler": "^4.53.0"
|
||||||
}
|
}
|
||||||
|
|||||||
386
pnpm-lock.yaml
generated
386
pnpm-lock.yaml
generated
@ -23,6 +23,18 @@ importers:
|
|||||||
'@astrojs/sitemap':
|
'@astrojs/sitemap':
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 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':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.19
|
specifier: ^0.5.19
|
||||||
version: 0.5.19(tailwindcss@4.1.17)
|
version: 0.5.19(tailwindcss@4.1.17)
|
||||||
@ -38,6 +50,12 @@ importers:
|
|||||||
astro:
|
astro:
|
||||||
specifier: ^5.16.4
|
specifier: ^5.16.4
|
||||||
version: 5.16.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(typescript@5.9.3)
|
version: 5.16.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(typescript@5.9.3)
|
||||||
|
dompurify:
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^12.26.2
|
||||||
|
version: 12.26.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
lunr:
|
lunr:
|
||||||
specifier: ^2.3.9
|
specifier: ^2.3.9
|
||||||
version: 2.3.9
|
version: 2.3.9
|
||||||
@ -56,7 +74,13 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.4
|
||||||
|
version: 4.3.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/dompurify':
|
||||||
|
specifier: ^3.2.0
|
||||||
|
version: 3.2.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.1
|
specifier: ^24.10.1
|
||||||
version: 24.10.1
|
version: 24.10.1
|
||||||
@ -199,6 +223,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==}
|
resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@cfworker/json-schema@4.1.1':
|
||||||
|
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
|
||||||
|
|
||||||
'@cloudflare/kv-asset-handler@0.4.0':
|
'@cloudflare/kv-asset-handler@0.4.0':
|
||||||
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
|
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@ -1018,6 +1045,53 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
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':
|
'@mdx-js/mdx@3.1.1':
|
||||||
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
|
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
|
||||||
|
|
||||||
@ -1296,6 +1370,10 @@ packages:
|
|||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
|
'@types/dompurify@3.2.0':
|
||||||
|
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
|
||||||
|
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
|
||||||
|
|
||||||
'@types/estree-jsx@1.0.5':
|
'@types/estree-jsx@1.0.5':
|
||||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||||
|
|
||||||
@ -1334,15 +1412,24 @@ packages:
|
|||||||
'@types/react@19.2.7':
|
'@types/react@19.2.7':
|
||||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
|
'@types/retry@0.12.0':
|
||||||
|
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||||
|
|
||||||
'@types/sax@1.2.7':
|
'@types/sax@1.2.7':
|
||||||
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
'@types/unist@2.0.11':
|
'@types/unist@2.0.11':
|
||||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
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':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||||
|
|
||||||
@ -1382,6 +1469,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||||
engines: {node: '>=12'}
|
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:
|
ansi-styles@6.2.3:
|
||||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -1447,6 +1542,10 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
camelcase@6.3.0:
|
||||||
|
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
camelcase@8.0.0:
|
camelcase@8.0.0:
|
||||||
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@ -1457,6 +1556,10 @@ packages:
|
|||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
chalk@5.6.2:
|
chalk@5.6.2:
|
||||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||||
@ -1520,6 +1623,9 @@ packages:
|
|||||||
common-ancestor-path@1.0.1:
|
common-ancestor-path@1.0.1:
|
||||||
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
|
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
|
||||||
|
|
||||||
|
console-table-printer@2.15.0:
|
||||||
|
resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==}
|
||||||
|
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
@ -1569,6 +1675,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
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:
|
decode-named-character-reference@1.2.0:
|
||||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||||
|
|
||||||
@ -1616,6 +1726,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
dompurify@3.3.1:
|
||||||
|
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||||
|
|
||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
@ -1703,6 +1816,9 @@ packages:
|
|||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
|
eventemitter3@4.0.7:
|
||||||
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
|
|
||||||
eventemitter3@5.0.1:
|
eventemitter3@5.0.1:
|
||||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||||
|
|
||||||
@ -1739,6 +1855,20 @@ packages:
|
|||||||
fontkit@2.0.4:
|
fontkit@2.0.4:
|
||||||
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
||||||
|
|
||||||
|
framer-motion@12.26.2:
|
||||||
|
resolution: {integrity: sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@ -1764,6 +1894,10 @@ packages:
|
|||||||
h3@1.15.4:
|
h3@1.15.4:
|
||||||
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
|
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:
|
hast-util-from-html@2.0.3:
|
||||||
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
|
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
|
||||||
|
|
||||||
@ -1859,6 +1993,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
js-tiktoken@1.0.21:
|
||||||
|
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@ -1884,6 +2021,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||||
engines: {node: '>=6'}
|
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:
|
lightningcss-android-arm64@1.30.2:
|
||||||
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@ -2161,6 +2315,12 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
motion-dom@12.26.2:
|
||||||
|
resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==}
|
||||||
|
|
||||||
|
motion-utils@12.24.10:
|
||||||
|
resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==}
|
||||||
|
|
||||||
mrmime@2.0.1:
|
mrmime@2.0.1:
|
||||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2168,6 +2328,10 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
mustache@4.2.0:
|
||||||
|
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@ -2208,14 +2372,42 @@ packages:
|
|||||||
oniguruma-to-es@4.3.4:
|
oniguruma-to-es@4.3.4:
|
||||||
resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
|
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:
|
p-limit@6.2.0:
|
||||||
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
|
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
p-queue@6.6.2:
|
||||||
|
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
p-queue@8.1.1:
|
p-queue@8.1.1:
|
||||||
resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==}
|
resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==}
|
||||||
engines: {node: '>=18'}
|
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:
|
p-timeout@6.1.4:
|
||||||
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
|
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
@ -2366,6 +2558,10 @@ packages:
|
|||||||
retext@9.0.0:
|
retext@9.0.0:
|
||||||
resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==}
|
resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==}
|
||||||
|
|
||||||
|
retry@0.13.1:
|
||||||
|
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
rollup@4.53.3:
|
rollup@4.53.3:
|
||||||
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
|
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@ -2400,6 +2596,9 @@ packages:
|
|||||||
simple-swizzle@0.2.4:
|
simple-swizzle@0.2.4:
|
||||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
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:
|
sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
@ -2462,6 +2661,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
svgo@4.0.0:
|
svgo@4.0.0:
|
||||||
resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==}
|
resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@ -2645,6 +2848,14 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
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:
|
vfile-location@5.0.3:
|
||||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||||
|
|
||||||
@ -2804,6 +3015,9 @@ packages:
|
|||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
zod@4.3.4:
|
||||||
|
resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==}
|
||||||
|
|
||||||
zwitch@2.0.4:
|
zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
@ -3050,6 +3264,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fontkit: 2.0.4
|
fontkit: 2.0.4
|
||||||
|
|
||||||
|
'@cfworker/json-schema@4.1.1': {}
|
||||||
|
|
||||||
'@cloudflare/kv-asset-handler@0.4.0':
|
'@cloudflare/kv-asset-handler@0.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
@ -3537,6 +3753,66 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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':
|
'@mdx-js/mdx@3.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -3796,6 +4072,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
|
'@types/dompurify@3.2.0':
|
||||||
|
dependencies:
|
||||||
|
dompurify: 3.3.1
|
||||||
|
|
||||||
'@types/estree-jsx@1.0.5':
|
'@types/estree-jsx@1.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -3836,14 +4116,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
'@types/sax@1.2.7':
|
'@types/sax@1.2.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.1
|
'@types/node': 24.10.1
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/unist@2.0.11': {}
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
|
'@types/uuid@10.0.0': {}
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.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))':
|
'@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 +4163,12 @@ snapshots:
|
|||||||
|
|
||||||
ansi-regex@6.2.2: {}
|
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: {}
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
anymatch@3.1.3:
|
anymatch@3.1.3:
|
||||||
@ -4032,12 +4325,19 @@ snapshots:
|
|||||||
node-releases: 2.0.27
|
node-releases: 2.0.27
|
||||||
update-browserslist-db: 1.2.2(browserslist@4.28.1)
|
update-browserslist-db: 1.2.2(browserslist@4.28.1)
|
||||||
|
|
||||||
|
camelcase@6.3.0: {}
|
||||||
|
|
||||||
camelcase@8.0.0: {}
|
camelcase@8.0.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001759: {}
|
caniuse-lite@1.0.30001759: {}
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
chalk@5.6.2: {}
|
chalk@5.6.2: {}
|
||||||
|
|
||||||
character-entities-html4@2.1.0: {}
|
character-entities-html4@2.1.0: {}
|
||||||
@ -4084,6 +4384,10 @@ snapshots:
|
|||||||
|
|
||||||
common-ancestor-path@1.0.1: {}
|
common-ancestor-path@1.0.1: {}
|
||||||
|
|
||||||
|
console-table-printer@2.15.0:
|
||||||
|
dependencies:
|
||||||
|
simple-wcswidth: 1.1.2
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie-es@1.2.2: {}
|
cookie-es@1.2.2: {}
|
||||||
@ -4126,6 +4430,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decamelize@1.2.0: {}
|
||||||
|
|
||||||
decode-named-character-reference@1.2.0:
|
decode-named-character-reference@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
@ -4166,6 +4472,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
|
dompurify@3.3.1:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
dom-serializer: 2.0.0
|
dom-serializer: 2.0.0
|
||||||
@ -4332,6 +4642,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
|
|
||||||
|
eventemitter3@4.0.7: {}
|
||||||
|
|
||||||
eventemitter3@5.0.1: {}
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
exit-hook@2.2.1: {}
|
exit-hook@2.2.1: {}
|
||||||
@ -4367,6 +4679,15 @@ snapshots:
|
|||||||
unicode-properties: 1.4.1
|
unicode-properties: 1.4.1
|
||||||
unicode-trie: 2.0.0
|
unicode-trie: 2.0.0
|
||||||
|
|
||||||
|
framer-motion@12.26.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.26.2
|
||||||
|
motion-utils: 12.24.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.1
|
||||||
|
react-dom: 19.2.1(react@19.2.1)
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -4392,6 +4713,8 @@ snapshots:
|
|||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
hast-util-from-html@2.0.3:
|
hast-util-from-html@2.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@ -4561,6 +4884,10 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
|
js-tiktoken@1.0.21:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.1:
|
js-yaml@4.1.1:
|
||||||
@ -4575,6 +4902,17 @@ snapshots:
|
|||||||
|
|
||||||
kleur@4.1.5: {}
|
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:
|
lightningcss-android-arm64@1.30.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -5125,10 +5463,18 @@ snapshots:
|
|||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
motion-dom@12.26.2:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.24.10
|
||||||
|
|
||||||
|
motion-utils@12.24.10: {}
|
||||||
|
|
||||||
mrmime@2.0.1: {}
|
mrmime@2.0.1: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
mustache@4.2.0: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
neotraverse@0.6.18: {}
|
neotraverse@0.6.18: {}
|
||||||
@ -5165,15 +5511,36 @@ snapshots:
|
|||||||
regex: 6.0.1
|
regex: 6.0.1
|
||||||
regex-recursion: 6.0.2
|
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:
|
p-limit@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue: 1.2.2
|
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:
|
p-queue@8.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3: 5.0.1
|
eventemitter3: 5.0.1
|
||||||
p-timeout: 6.1.4
|
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: {}
|
p-timeout@6.1.4: {}
|
||||||
|
|
||||||
package-manager-detector@1.6.0: {}
|
package-manager-detector@1.6.0: {}
|
||||||
@ -5395,6 +5762,8 @@ snapshots:
|
|||||||
retext-stringify: 4.0.0
|
retext-stringify: 4.0.0
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
|
|
||||||
|
retry@0.13.1: {}
|
||||||
|
|
||||||
rollup@4.53.3:
|
rollup@4.53.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -5503,6 +5872,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.3.4
|
is-arrayish: 0.3.4
|
||||||
|
|
||||||
|
simple-wcswidth@1.1.2: {}
|
||||||
|
|
||||||
sisteransi@1.0.5: {}
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
sitemap@8.0.2:
|
sitemap@8.0.2:
|
||||||
@ -5561,6 +5932,10 @@ snapshots:
|
|||||||
|
|
||||||
supports-color@10.2.2: {}
|
supports-color@10.2.2: {}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
svgo@4.0.0:
|
svgo@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 11.1.0
|
commander: 11.1.0
|
||||||
@ -5703,6 +6078,10 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
uuid@10.0.0: {}
|
||||||
|
|
||||||
|
uuid@9.0.1: {}
|
||||||
|
|
||||||
vfile-location@5.0.3:
|
vfile-location@5.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@ -5833,6 +6212,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
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):
|
zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@ -5842,4 +6226,6 @@ snapshots:
|
|||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
|
zod@4.3.4: {}
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
|||||||
@ -124,14 +124,14 @@ const professionalServiceSchema = {
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Space+Mono:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Sora:wght@300;400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
media="print"
|
media="print"
|
||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript>
|
<noscript>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Space+Mono:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Sora:wght@300;400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@ -19,13 +19,13 @@ const today = new Date();
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<a href="mailto:nicholai@nicholai.work" class="group flex items-center gap-4 px-6 py-4 border border-brand-accent/30 bg-brand-accent/5 hover:bg-brand-accent hover:text-brand-dark transition-all duration-300">
|
<a href="mailto:nicholai@nicholai.work" class="group flex items-center gap-4 px-6 py-4 border border-brand-accent/30 bg-brand-accent/5 hover:bg-brand-accent hover:text-brand-dark transition-all duration-300 rounded-full">
|
||||||
<span class="font-mono text-xs font-bold uppercase tracking-widest">Connect_Uplink</span>
|
<span class="font-mono text-xs font-bold uppercase tracking-widest">Connect_Uplink</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter">
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="/contact" class="group flex items-center gap-4 px-6 py-4 border border-[var(--theme-border-strong)] hover:border-brand-accent hover:bg-brand-accent/5 transition-all duration-300">
|
<a href="/contact" class="group flex items-center gap-4 px-6 py-4 border border-[var(--theme-border-strong)] hover:border-brand-accent hover:bg-brand-accent/5 transition-all duration-300 rounded-full">
|
||||||
<span class="font-mono text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)]">Manual_Input</span>
|
<span class="font-mono text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)]">Manual_Input</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
267
src/components/HubertChat.tsx
Normal file
267
src/components/HubertChat.tsx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { useHubertChat } from '../hooks/useHubertChat';
|
||||||
|
import HubertInput from './HubertInput';
|
||||||
|
|
||||||
|
// Configure marked for safe rendering
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render markdown to sanitized HTML
|
||||||
|
function renderMarkdown(content: string): string {
|
||||||
|
const rawHtml = marked.parse(content, { async: false }) as string;
|
||||||
|
return DOMPurify.sanitize(rawHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HubertChat() {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
input,
|
||||||
|
isTyping,
|
||||||
|
isInitializing,
|
||||||
|
initError,
|
||||||
|
setInput,
|
||||||
|
sendMessage,
|
||||||
|
retryInit,
|
||||||
|
messagesEndRef,
|
||||||
|
} = useHubertChat({ initTimeout: 8000, chatTimeout: 30000 });
|
||||||
|
|
||||||
|
// Initial/Loading state - centered branding with input
|
||||||
|
if (isInitializing && !initError) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center px-4">
|
||||||
|
{/* Branding */}
|
||||||
|
<div className="flex items-center gap-3 mb-8">
|
||||||
|
<span className="text-2xl font-semibold text-[var(--theme-text-primary)]">Hubert</span>
|
||||||
|
<div className="w-4 h-4 border-2 border-white/60 border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-[var(--theme-text-muted)]">Waking up...</p>
|
||||||
|
<span className="sr-only" role="status" aria-live="polite">
|
||||||
|
Loading Hubert chat interface
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (initError) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center px-4">
|
||||||
|
<span className="text-2xl font-semibold text-[var(--theme-text-primary)] mb-6">Hubert</span>
|
||||||
|
<p className="text-sm text-[var(--theme-text-muted)] mb-4 text-center max-w-sm" role="alert">
|
||||||
|
{initError}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={retryInit}
|
||||||
|
className="px-5 py-2.5 rounded-full bg-white/10 hover:bg-white/15 text-sm text-[var(--theme-text-primary)] transition-colors"
|
||||||
|
aria-label="Retry connecting to Hubert"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No messages yet - show centered input
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center px-4">
|
||||||
|
{/* Branding */}
|
||||||
|
<span className="text-3xl font-semibold text-[var(--theme-text-primary)] mb-10">Hubert</span>
|
||||||
|
|
||||||
|
{/* Input bar */}
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<HubertInput
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
onSubmit={sendMessage}
|
||||||
|
disabled={isTyping}
|
||||||
|
placeholder="What do you want to know?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<p className="text-xs text-[var(--theme-text-subtle)] mt-6">
|
||||||
|
A miserable AI assistant, here to interview you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat mode - messages with input at bottom
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
|
{/* Messages area - scrollable */}
|
||||||
|
<div
|
||||||
|
className="flex-1 overflow-y-auto px-4 py-6 min-h-0"
|
||||||
|
role="log"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Chat messages"
|
||||||
|
>
|
||||||
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
{msg.role === 'user' ? (
|
||||||
|
// User message - right aligned pill with markdown
|
||||||
|
<div className="max-w-[80%] bg-[var(--theme-bg-secondary)] rounded-3xl px-5 py-3">
|
||||||
|
<div
|
||||||
|
className="user-message text-[var(--theme-text-primary)] text-[15px] leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(msg.content) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Assistant message - left aligned with subtle label and markdown
|
||||||
|
<div className="max-w-[85%] space-y-1.5">
|
||||||
|
{(index === 0 || messages[index - 1]?.role === 'user') && (
|
||||||
|
<span className="text-[11px] font-medium uppercase tracking-wide text-brand-accent">
|
||||||
|
Hubert
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="hubert-message text-[var(--theme-text-secondary)] text-[15px] leading-[1.7]"
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(msg.content) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Typing indicator */}
|
||||||
|
{isTyping && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<span className="text-[11px] font-medium uppercase tracking-wide text-brand-accent">
|
||||||
|
Hubert
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[var(--theme-text-muted)] animate-pulse" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[var(--theme-text-muted)] animate-pulse" style={{ animationDelay: '150ms' }} />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[var(--theme-text-muted)] animate-pulse" style={{ animationDelay: '300ms' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="sr-only" role="status">Hubert is typing</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input bar - pinned to bottom */}
|
||||||
|
<div className="flex-shrink-0 px-4 pb-4 pt-2">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<HubertInput
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
onSubmit={sendMessage}
|
||||||
|
disabled={isTyping}
|
||||||
|
placeholder="How can Hubert help?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Styles for markdown content */}
|
||||||
|
<style>{`
|
||||||
|
.hubert-message p,
|
||||||
|
.user-message p {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.hubert-message p:last-child,
|
||||||
|
.user-message p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.hubert-message strong,
|
||||||
|
.user-message strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hubert-message strong {
|
||||||
|
color: var(--theme-text-primary);
|
||||||
|
}
|
||||||
|
.hubert-message em,
|
||||||
|
.user-message em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hubert-message code,
|
||||||
|
.user-message code {
|
||||||
|
background: var(--theme-bg-secondary);
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
.user-message code {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.hubert-message pre,
|
||||||
|
.user-message pre {
|
||||||
|
background: var(--theme-bg-secondary);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
.user-message pre {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.hubert-message pre code,
|
||||||
|
.user-message pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.hubert-message ul, .hubert-message ol,
|
||||||
|
.user-message ul, .user-message ol {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
.hubert-message li,
|
||||||
|
.user-message li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.hubert-message ul li,
|
||||||
|
.user-message ul li {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
.hubert-message ol li,
|
||||||
|
.user-message ol li {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
.hubert-message blockquote,
|
||||||
|
.user-message blockquote {
|
||||||
|
border-left: 3px solid var(--color-brand-accent);
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hubert-message blockquote {
|
||||||
|
color: var(--theme-text-muted);
|
||||||
|
}
|
||||||
|
.hubert-message a,
|
||||||
|
.user-message a {
|
||||||
|
color: var(--color-brand-accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.hubert-message a:hover,
|
||||||
|
.user-message a:hover {
|
||||||
|
color: var(--theme-text-primary);
|
||||||
|
}
|
||||||
|
.hubert-message h1, .hubert-message h2, .hubert-message h3,
|
||||||
|
.user-message h1, .user-message h2, .user-message h3 {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.hubert-message h1, .hubert-message h2, .hubert-message h3 {
|
||||||
|
color: var(--theme-text-primary);
|
||||||
|
}
|
||||||
|
.hubert-message h1, .user-message h1 { font-size: 1.25rem; }
|
||||||
|
.hubert-message h2, .user-message h2 { font-size: 1.125rem; }
|
||||||
|
.hubert-message h3, .user-message h3 { font-size: 1rem; }
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
src/components/HubertInput.tsx
Normal file
125
src/components/HubertInput.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React, { useRef, useEffect, useCallback, forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
|
|
||||||
|
interface HubertInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HubertInputHandle {
|
||||||
|
focus: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-resizing textarea input for Hubert chat.
|
||||||
|
* Uses a clean CSS-based approach with proper state management.
|
||||||
|
*/
|
||||||
|
const HubertInput = forwardRef<HubertInputHandle, HubertInputProps>(
|
||||||
|
({ value, onChange, onSubmit, disabled = false, placeholder = "Type a message..." }, ref) => {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [isMultiline, setIsMultiline] = useState(false);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
focus: () => textareaRef.current?.focus(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Adjust textarea height based on content
|
||||||
|
const adjustHeight = useCallback(() => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
// Reset to auto to get accurate scrollHeight
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
|
||||||
|
// Get the natural content height
|
||||||
|
const scrollHeight = textarea.scrollHeight;
|
||||||
|
|
||||||
|
// Clamp between min (44px) and max (200px)
|
||||||
|
const minHeight = 44;
|
||||||
|
const maxHeight = 200;
|
||||||
|
const newHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight));
|
||||||
|
|
||||||
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
|
||||||
|
// Update multiline state (threshold at ~1.5 lines)
|
||||||
|
setIsMultiline(newHeight > 52);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Adjust height whenever value changes
|
||||||
|
useEffect(() => {
|
||||||
|
adjustHeight();
|
||||||
|
}, [value, adjustHeight]);
|
||||||
|
|
||||||
|
// Also adjust on window resize
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', adjustHeight);
|
||||||
|
return () => window.removeEventListener('resize', adjustHeight);
|
||||||
|
}, [adjustHeight]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!disabled && value.trim()) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`hubert-input-wrapper relative bg-[var(--theme-bg-secondary)] border border-[var(--theme-border-primary)] focus-within:border-[var(--theme-border-strong)] transition-all duration-200 ease-out ${
|
||||||
|
isMultiline ? 'p-4 rounded-[28px]' : 'flex items-center px-6 py-2 rounded-full'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label="Type your message"
|
||||||
|
rows={1}
|
||||||
|
className={`bg-transparent text-[var(--theme-text-primary)] placeholder:text-[var(--theme-text-subtle)] text-base outline-none resize-none overflow-hidden ${
|
||||||
|
isMultiline
|
||||||
|
? 'w-full leading-relaxed px-2'
|
||||||
|
: 'flex-1 leading-normal'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
minHeight: '24px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={`transition-all duration-150 ${isMultiline ? 'flex justify-end mt-3' : 'ml-3 flex-shrink-0'}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={disabled || !value.trim()}
|
||||||
|
aria-label="Send message"
|
||||||
|
className="w-10 h-10 rounded-full bg-[var(--theme-text-primary)] hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-[var(--theme-bg-primary)]"
|
||||||
|
>
|
||||||
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
HubertInput.displayName = 'HubertInput';
|
||||||
|
|
||||||
|
export default HubertInput;
|
||||||
@ -1,67 +1,99 @@
|
|||||||
---
|
---
|
||||||
import ThemeToggle from './ThemeToggle.astro';
|
import ThemeToggle from './ThemeToggle.astro';
|
||||||
|
|
||||||
|
const currentPath = Astro.url.pathname;
|
||||||
|
|
||||||
|
// Navigation items
|
||||||
|
const navItems = [
|
||||||
|
{ href: '/', label: 'Home', icon: 'home' },
|
||||||
|
{ href: '/dev', label: 'Dev', icon: 'code' },
|
||||||
|
{ href: '/blog', label: 'Blog', icon: 'file' },
|
||||||
|
{ href: '/hubert', label: 'Hubert', icon: 'chat' },
|
||||||
|
{ href: '/contact', label: 'Contact', icon: 'mail' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if a path is active
|
||||||
|
function isActive(href: string): boolean {
|
||||||
|
if (href === '/') {
|
||||||
|
return currentPath === '/';
|
||||||
|
}
|
||||||
|
return currentPath.startsWith(href);
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav class="fixed top-0 left-0 w-full z-50 px-6 lg:px-12 py-6 lg:py-8 flex justify-between items-center backdrop-blur-md bg-[var(--theme-overlay)] border-b border-[var(--theme-border-secondary)]">
|
<!-- Desktop Navigation: Fixed left sidebar (lg: 1024px+) -->
|
||||||
<!-- Left side - branding and theme toggle -->
|
<nav class="hidden lg:flex fixed left-6 top-1/2 -translate-y-1/2 z-50 flex-col items-center gap-1 p-2 bg-[var(--theme-overlay)] backdrop-blur-md border border-[var(--theme-border-primary)] rounded-2xl">
|
||||||
<div class="flex items-center gap-6">
|
{navItems.map((item) => (
|
||||||
<a href="/" class="text-[10px] font-mono text-[var(--theme-text-muted)] tracking-widest uppercase hover:text-brand-accent transition-colors duration-300">NV / 2026</a>
|
<a
|
||||||
<div class="hidden md:block">
|
href={item.href}
|
||||||
|
class:list={[
|
||||||
|
"nav-icon relative group w-10 h-10 flex items-center justify-center rounded-xl transition-colors duration-200",
|
||||||
|
isActive(item.href)
|
||||||
|
? "bg-brand-accent/10 text-brand-accent"
|
||||||
|
: "text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)] hover:bg-[var(--theme-hover-bg-strong)]"
|
||||||
|
]}
|
||||||
|
aria-label={item.label}
|
||||||
|
data-icon={item.icon}
|
||||||
|
>
|
||||||
|
{/* Home icon */}
|
||||||
|
{item.icon === 'home' && (
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{/* Code icon */}
|
||||||
|
{item.icon === 'code' && (
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="16 18 22 12 16 6"></polyline>
|
||||||
|
<polyline points="8 6 2 12 8 18"></polyline>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{/* File/Blog icon */}
|
||||||
|
{item.icon === 'file' && (
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||||
|
<polyline points="10 9 9 9 8 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{/* Chat icon */}
|
||||||
|
{item.icon === 'chat' && (
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{/* Mail icon */}
|
||||||
|
{item.icon === 'mail' && (
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||||
|
<polyline points="22,6 12,13 2,6"></polyline>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{/* Tooltip */}
|
||||||
|
<span class="absolute left-full ml-3 px-3 py-1.5 bg-[var(--theme-bg-secondary)] border border-[var(--theme-border-primary)] rounded-lg text-xs font-medium whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity duration-200 shadow-lg">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div class="w-6 h-px bg-[var(--theme-border-primary)] my-2"></div>
|
||||||
|
|
||||||
|
{/* Theme Toggle */}
|
||||||
|
<div class="nav-theme-toggle w-10 h-10 flex items-center justify-center">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
<!-- Right side navigation -->
|
<!-- Mobile Navigation: Top bar with hamburger (< lg) -->
|
||||||
<div class="flex items-center gap-6 lg:gap-10 ml-auto">
|
<nav class="lg:hidden fixed top-0 left-0 w-full z-50 px-6 py-6 flex justify-between items-center backdrop-blur-md bg-[var(--theme-overlay)] border-b border-[var(--theme-border-secondary)]">
|
||||||
<div class="hidden md:flex items-center gap-10 lg:gap-12">
|
<!-- Left side - branding -->
|
||||||
<a href="/"
|
<a href="/" class="text-[10px] font-mono text-[var(--theme-text-muted)] tracking-widest uppercase hover:text-brand-accent transition-colors duration-300">NV / 2026</a>
|
||||||
class:list={[
|
|
||||||
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
|
|
||||||
Astro.url.pathname === '/' ? "text-[var(--theme-text-primary)]" : "text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)]"
|
|
||||||
]}>
|
|
||||||
<span class="relative z-10">Home</span>
|
|
||||||
<span class:list={[
|
|
||||||
"absolute bottom-0 left-0 h-[1px] bg-brand-accent transition-all duration-300 ease-out",
|
|
||||||
Astro.url.pathname === '/' ? "w-full" : "w-0 group-hover:w-full"
|
|
||||||
]}></span>
|
|
||||||
</a>
|
|
||||||
<a href="/dev"
|
|
||||||
class:list={[
|
|
||||||
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
|
|
||||||
Astro.url.pathname.startsWith('/dev') ? "text-[var(--theme-text-primary)]" : "text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)]"
|
|
||||||
]}>
|
|
||||||
<span class="relative z-10">Dev</span>
|
|
||||||
<span class:list={[
|
|
||||||
"absolute bottom-0 left-0 h-[1px] bg-brand-accent transition-all duration-300 ease-out",
|
|
||||||
Astro.url.pathname.startsWith('/dev') ? "w-full" : "w-0 group-hover:w-full"
|
|
||||||
]}></span>
|
|
||||||
</a>
|
|
||||||
<a href="/blog"
|
|
||||||
class:list={[
|
|
||||||
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
|
|
||||||
Astro.url.pathname.startsWith('/blog') ? "text-[var(--theme-text-primary)]" : "text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)]"
|
|
||||||
]}>
|
|
||||||
<span class="relative z-10">Blog</span>
|
|
||||||
<span class:list={[
|
|
||||||
"absolute bottom-0 left-0 h-[1px] bg-brand-accent transition-all duration-300 ease-out",
|
|
||||||
Astro.url.pathname.startsWith('/blog') ? "w-full" : "w-0 group-hover:w-full"
|
|
||||||
]}></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="/contact"
|
|
||||||
class:list={[
|
|
||||||
"hidden md:block border px-5 lg:px-6 py-2.5 lg:py-3 text-xs font-bold uppercase tracking-[0.15em] transition-all duration-300",
|
|
||||||
Astro.url.pathname.startsWith('/contact')
|
|
||||||
? "border-brand-accent bg-brand-accent text-brand-dark"
|
|
||||||
: "border-[var(--theme-border-strong)] text-[var(--theme-text-primary)] hover:border-brand-accent hover:bg-brand-accent hover:text-brand-dark"
|
|
||||||
]}>
|
|
||||||
Let's Talk
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
<!-- Mobile menu button -->
|
||||||
<div class="md:hidden flex items-center">
|
|
||||||
<button
|
<button
|
||||||
id="mobile-menu-toggle"
|
id="mobile-menu-toggle"
|
||||||
class="p-2 text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)] transition-colors z-[60]"
|
class="p-2 text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)] transition-colors z-[60]"
|
||||||
@ -77,13 +109,12 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 18L18 6M6 6l12 12"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Mobile Menu Overlay -->
|
<!-- Mobile Menu Overlay -->
|
||||||
<div
|
<div
|
||||||
id="mobile-menu"
|
id="mobile-menu"
|
||||||
class="fixed inset-0 z-40 bg-[var(--theme-overlay-heavy)] backdrop-blur-xl transform translate-x-full transition-transform duration-300 ease-out md:hidden"
|
class="fixed inset-0 z-40 bg-[var(--theme-overlay-heavy)] backdrop-blur-xl transform translate-x-full transition-transform duration-300 ease-out lg:hidden"
|
||||||
>
|
>
|
||||||
<!-- Menu Content -->
|
<!-- Menu Content -->
|
||||||
<div class="flex flex-col justify-center items-center h-full px-8">
|
<div class="flex flex-col justify-center items-center h-full px-8">
|
||||||
@ -107,6 +138,12 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="/hubert"
|
||||||
|
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Hubert
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/contact"
|
href="/contact"
|
||||||
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
|
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
|
||||||
@ -118,7 +155,7 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
<!-- CTA Button -->
|
<!-- CTA Button -->
|
||||||
<a
|
<a
|
||||||
href="/contact"
|
href="/contact"
|
||||||
class="border border-brand-accent px-8 py-4 text-sm font-bold uppercase tracking-[0.2em] text-brand-accent hover:bg-brand-accent hover:text-brand-dark transition-all duration-300 mb-8"
|
class="border border-brand-accent px-8 py-4 text-sm font-bold uppercase tracking-[0.2em] text-brand-accent hover:bg-brand-accent hover:text-brand-dark transition-all duration-300 mb-8 rounded-full"
|
||||||
>
|
>
|
||||||
Let's Talk
|
Let's Talk
|
||||||
</a>
|
</a>
|
||||||
@ -138,7 +175,25 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Custom styling for the nav theme toggle to make it more compact */
|
||||||
|
.nav-theme-toggle :global(.theme-toggle-group) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-theme-toggle :global(.theme-toggle-group > div:first-child) {
|
||||||
|
display: none; /* Hide the arrow icon in compact mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-theme-toggle :global(.theme-toggle-group > div:last-child) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function initMobileNav() {
|
||||||
const toggle = document.getElementById('mobile-menu-toggle');
|
const toggle = document.getElementById('mobile-menu-toggle');
|
||||||
const menu = document.getElementById('mobile-menu');
|
const menu = document.getElementById('mobile-menu');
|
||||||
const iconOpen = document.getElementById('menu-icon-open');
|
const iconOpen = document.getElementById('menu-icon-open');
|
||||||
@ -167,7 +222,12 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle?.addEventListener('click', toggleMenu);
|
// Remove old listeners by cloning
|
||||||
|
if (toggle) {
|
||||||
|
const newToggle = toggle.cloneNode(true);
|
||||||
|
toggle.parentNode?.replaceChild(newToggle, toggle);
|
||||||
|
newToggle.addEventListener('click', toggleMenu);
|
||||||
|
}
|
||||||
|
|
||||||
// Close menu when clicking a link
|
// Close menu when clicking a link
|
||||||
mobileNavLinks.forEach(link => {
|
mobileNavLinks.forEach(link => {
|
||||||
@ -175,11 +235,26 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
if (isOpen) toggleMenu();
|
if (isOpen) toggleMenu();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Close menu on escape key
|
// Close menu on escape key (only add once)
|
||||||
|
let escapeListenerAdded = false;
|
||||||
|
function addEscapeListener() {
|
||||||
|
if (escapeListenerAdded) return;
|
||||||
|
escapeListenerAdded = true;
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && isOpen) {
|
if (e.key === 'Escape') {
|
||||||
toggleMenu();
|
const menu = document.getElementById('mobile-menu');
|
||||||
|
if (menu && !menu.classList.contains('translate-x-full')) {
|
||||||
|
const toggle = document.getElementById('mobile-menu-toggle');
|
||||||
|
toggle?.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on load and view transitions
|
||||||
|
initMobileNav();
|
||||||
|
addEscapeListener();
|
||||||
|
document.addEventListener('astro:page-load', initMobileNav);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export default function SearchDialog() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
className="hidden md:flex items-center gap-3 px-4 py-2 border border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)] text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-[var(--theme-text-primary)] transition-all duration-300 text-xs"
|
className="hidden md:flex items-center gap-3 px-4 py-2 border border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)] text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-[var(--theme-text-primary)] transition-all duration-300 text-xs rounded-full"
|
||||||
aria-label="Open search"
|
aria-label="Open search"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -216,7 +216,7 @@ export default function SearchDialog() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={closeSearch}
|
onClick={closeSearch}
|
||||||
className="text-[9px] font-mono uppercase tracking-wider px-3 py-1.5 border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all"
|
className="text-[9px] font-mono uppercase tracking-wider px-3 py-1.5 border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all rounded-full"
|
||||||
>
|
>
|
||||||
[ESC]
|
[ESC]
|
||||||
</button>
|
</button>
|
||||||
@ -255,7 +255,7 @@ export default function SearchDialog() {
|
|||||||
setQuery('');
|
setQuery('');
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
className="text-[10px] font-mono uppercase tracking-wider px-3 py-1.5 border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all"
|
className="text-[10px] font-mono uppercase tracking-wider px-3 py-1.5 border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all rounded-full"
|
||||||
>
|
>
|
||||||
[CLR]
|
[CLR]
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="remember-yes"
|
id="remember-yes"
|
||||||
class="group px-4 py-3 border border-[var(--theme-border-strong)] hover:border-brand-accent hover:bg-brand-accent transition-all duration-300"
|
class="group px-4 py-3 border border-[var(--theme-border-strong)] hover:border-brand-accent hover:bg-brand-accent transition-all duration-300 rounded-full"
|
||||||
>
|
>
|
||||||
<span class="text-sm font-bold uppercase tracking-tight text-[var(--theme-text-primary)] group-hover:text-brand-dark">
|
<span class="text-sm font-bold uppercase tracking-tight text-[var(--theme-text-primary)] group-hover:text-brand-dark">
|
||||||
Save
|
Save
|
||||||
@ -114,7 +114,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="remember-no"
|
id="remember-no"
|
||||||
class="group px-4 py-3 border border-[var(--theme-border-strong)] hover:border-[var(--theme-text-subtle)] transition-all duration-300"
|
class="group px-4 py-3 border border-[var(--theme-border-strong)] hover:border-[var(--theme-text-subtle)] transition-all duration-300 rounded-full"
|
||||||
>
|
>
|
||||||
<span class="text-sm font-bold uppercase tracking-tight text-[var(--theme-text-primary)]">
|
<span class="text-sm font-bold uppercase tracking-tight text-[var(--theme-text-primary)]">
|
||||||
Session
|
Session
|
||||||
|
|||||||
@ -284,7 +284,7 @@ const DevEngageModal: React.FC = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-[var(--theme-border-primary)] hover:border-brand-accent hover:bg-brand-accent/5 transition-all font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:text-brand-accent"
|
className="flex items-center gap-2 px-4 py-2 border border-[var(--theme-border-primary)] hover:border-brand-accent hover:bg-brand-accent/5 transition-all font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:text-brand-accent rounded-full"
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">DISCONNECT</span>
|
<span className="hidden sm:inline">DISCONNECT</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square">
|
||||||
@ -375,19 +375,19 @@ const DevEngageModal: React.FC = () => {
|
|||||||
href={activeProject.link}
|
href={activeProject.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="px-6 py-3 bg-brand-accent text-brand-dark font-mono text-[10px] uppercase tracking-widest font-bold hover:bg-brand-accent/90 transition-colors"
|
className="px-6 py-3 bg-brand-accent text-brand-dark font-mono text-[10px] uppercase tracking-widest font-bold hover:bg-brand-accent/90 transition-colors rounded-full"
|
||||||
>
|
>
|
||||||
OPEN_EXTERNALLY
|
OPEN_EXTERNALLY
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
className="px-6 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all"
|
className="px-6 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all rounded-full"
|
||||||
>
|
>
|
||||||
COPY_LINK
|
COPY_LINK
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleRetry}
|
onClick={handleRetry}
|
||||||
className="px-6 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all"
|
className="px-6 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent transition-all rounded-full"
|
||||||
>
|
>
|
||||||
RETRY
|
RETRY
|
||||||
</button>
|
</button>
|
||||||
@ -421,7 +421,7 @@ const DevEngageModal: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={toggleArm}
|
onClick={toggleArm}
|
||||||
disabled={modalState === 'booting' || modalState === 'blocked'}
|
disabled={modalState === 'booting' || modalState === 'blocked'}
|
||||||
className={`w-full py-4 font-mono text-xs uppercase tracking-widest font-bold transition-all duration-300 border ${
|
className={`w-full py-4 font-mono text-xs uppercase tracking-widest font-bold transition-all duration-300 border rounded-full ${
|
||||||
isInteractive
|
isInteractive
|
||||||
? 'bg-green-500/20 border-green-500 text-green-500 hover:bg-green-500/30'
|
? 'bg-green-500/20 border-green-500 text-green-500 hover:bg-green-500/30'
|
||||||
: 'bg-brand-accent/10 border-brand-accent/50 text-brand-accent hover:bg-brand-accent/20 hover:border-brand-accent'
|
: 'bg-brand-accent/10 border-brand-accent/50 text-brand-accent hover:bg-brand-accent/20 hover:border-brand-accent'
|
||||||
@ -470,7 +470,7 @@ const DevEngageModal: React.FC = () => {
|
|||||||
href={activeProject.link}
|
href={activeProject.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-between px-4 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent hover:bg-brand-accent/5 transition-all"
|
className="flex items-center justify-between px-4 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent hover:bg-brand-accent/5 transition-all rounded-full"
|
||||||
>
|
>
|
||||||
OPEN_EXTERNALLY
|
OPEN_EXTERNALLY
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square">
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square">
|
||||||
@ -483,7 +483,7 @@ const DevEngageModal: React.FC = () => {
|
|||||||
iframeRef.current.src = activeProject.link;
|
iframeRef.current.src = activeProject.link;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex items-center justify-between px-4 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent hover:bg-brand-accent/5 transition-all"
|
className="flex items-center justify-between px-4 py-3 border border-[var(--theme-border-primary)] font-mono text-[10px] uppercase tracking-widest text-[var(--theme-text-muted)] hover:border-brand-accent hover:text-brand-accent hover:bg-brand-accent/5 transition-all rounded-full"
|
||||||
>
|
>
|
||||||
RELOAD_FEED
|
RELOAD_FEED
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square">
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square">
|
||||||
|
|||||||
@ -54,8 +54,8 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The Content -->
|
<!-- The Content -->
|
||||||
<!-- Adjusted pt to clear fixed nav since BaseLayout padding is removed -->
|
<!-- Mobile: pt-24 clears mobile top bar. Desktop: pt-16 since nav is on left side -->
|
||||||
<div class="absolute inset-0 z-20 flex flex-col justify-between p-6 md:p-12 lg:p-16 pt-32 lg:pt-40 pointer-events-auto">
|
<div class="absolute inset-0 z-20 flex flex-col justify-between p-6 md:p-12 lg:p-16 pt-24 lg:pt-16 pointer-events-auto">
|
||||||
|
|
||||||
<!-- Top Metadata -->
|
<!-- Top Metadata -->
|
||||||
<div class="flex justify-between items-start w-full intro-element opacity-0 translate-y-4 transition-all duration-1000 ease-out delay-300">
|
<div class="flex justify-between items-start w-full intro-element opacity-0 translate-y-4 transition-all duration-1000 ease-out delay-300">
|
||||||
|
|||||||
40
src/components/sections/Hubert.astro
Normal file
40
src/components/sections/Hubert.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
sectionTitle: string;
|
||||||
|
sectionSubtitle: string;
|
||||||
|
sectionLabel: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sectionTitle, sectionSubtitle, sectionLabel, description } = Astro.props;
|
||||||
|
|
||||||
|
import HubertChat from '../HubertChat';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="hubert" class="py-16 lg:py-24 relative overflow-hidden">
|
||||||
|
{/* Grid overlay background */}
|
||||||
|
<div class="absolute inset-0 pointer-events-none opacity-10">
|
||||||
|
<div class="w-full h-full bg-[linear-gradient(var(--theme-grid-line)_1px,transparent_1px),linear-gradient(90deg,var(--theme-grid-line)_1px,transparent_1px)] bg-[size:100px_100px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-6 lg:px-12 relative z-10">
|
||||||
|
<!-- Section Header -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<div class="text-[10px] font-mono font-bold uppercase tracking-[0.3em] text-brand-accent mb-4">
|
||||||
|
/// SYS.03 /// INTERVIEW_TERMINAL
|
||||||
|
</div>
|
||||||
|
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold uppercase tracking-tighter text-[var(--theme-text-primary)] leading-[0.9] mb-4">
|
||||||
|
{sectionTitle.split(' ').slice(0, -1).join(' ')}{' '}
|
||||||
|
<span class="text-brand-accent">
|
||||||
|
{sectionTitle.split(' ').slice(-1)}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-[var(--theme-text-secondary)] text-lg max-w-2xl">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hubert Chat Interface -->
|
||||||
|
<HubertChat client:visible />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -72,11 +72,12 @@ const sections = defineCollection({
|
|||||||
label: z.string(),
|
label: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
videoUrl: z.string().optional(),
|
|
||||||
linkUrl: z.string().optional(),
|
linkUrl: z.string().optional(),
|
||||||
|
videoUrl: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const pages = defineCollection({
|
const pages = defineCollection({
|
||||||
loader: glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' }),
|
loader: glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' }),
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
|
|||||||
@ -35,3 +35,5 @@ tags: ['opinions', 'satire']
|
|||||||
**[2025-12-24 01:38:25]** i love cats but god are they fucking annoying
|
**[2025-12-24 01:38:25]** i love cats but god are they fucking annoying
|
||||||
|
|
||||||
**[2025-12-27 01:25:33]** craftsmanship used to be cool
|
**[2025-12-27 01:25:33]** craftsmanship used to be cool
|
||||||
|
|
||||||
|
**[2026-01-13 22:24:52]** sudo pacman -Syu
|
||||||
|
|||||||
8
src/content/sections/hubert.mdx
Normal file
8
src/content/sections/hubert.mdx
Normal file
@ -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.
|
||||||
37
src/db/migrations/001_initial_schema.sql
Normal file
37
src/db/migrations/001_initial_schema.sql
Normal file
@ -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);
|
||||||
4
src/db/migrations/002_add_visitor_name.sql
Normal file
4
src/db/migrations/002_add_visitor_name.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-- Add name field to visitors table
|
||||||
|
-- Allows Hubert to save the visitor's name when they share it
|
||||||
|
|
||||||
|
ALTER TABLE visitors ADD COLUMN name TEXT;
|
||||||
252
src/hooks/useHubertChat.ts
Normal file
252
src/hooks/useHubertChat.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseHubertChatOptions {
|
||||||
|
initTimeout?: number;
|
||||||
|
chatTimeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseHubertChatReturn {
|
||||||
|
messages: Message[];
|
||||||
|
input: string;
|
||||||
|
isTyping: boolean;
|
||||||
|
isInitializing: boolean;
|
||||||
|
initError: string | null;
|
||||||
|
visitorId: string | null;
|
||||||
|
setInput: (value: string) => void;
|
||||||
|
sendMessage: () => Promise<void>;
|
||||||
|
retryInit: () => void;
|
||||||
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique message ID
|
||||||
|
const generateId = () => `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
|
||||||
|
// Utility: Fetch with timeout
|
||||||
|
const fetchWithTimeout = async (
|
||||||
|
url: string,
|
||||||
|
options: RequestInit = {},
|
||||||
|
timeout: number
|
||||||
|
): Promise<Response> => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse error into user-friendly message
|
||||||
|
const parseError = (error: unknown): string => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
return '/// ERROR: TIMEOUT - API_UNRESPONSIVE';
|
||||||
|
} else if (error.message.includes('Failed to fetch')) {
|
||||||
|
return '/// ERROR: NETWORK_FAILURE - CHECK_API_ROUTE';
|
||||||
|
}
|
||||||
|
return `/// ERROR: ${error.message}`;
|
||||||
|
}
|
||||||
|
return '/// ERROR: UNKNOWN_FAILURE';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useHubertChat(options: UseHubertChatOptions = {}): UseHubertChatReturn {
|
||||||
|
const { initTimeout = 8000, chatTimeout = 30000 } = options;
|
||||||
|
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [visitorId, setVisitorId] = useState<string | null>(null);
|
||||||
|
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||||
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
const [initError, setInitError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isRetryingRef = useRef(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
// Initialize visitor
|
||||||
|
const initVisitor = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsInitializing(true);
|
||||||
|
setInitError(null);
|
||||||
|
|
||||||
|
const response = await fetchWithTimeout(
|
||||||
|
'/api/hubert/new-visitor',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
initTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setVisitorId(data.visitor_id);
|
||||||
|
setConversationId(data.conversation_id);
|
||||||
|
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
role: 'system',
|
||||||
|
content: `I suppose you want something. State your business.`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Hubert] Initialization failed:', error);
|
||||||
|
const errorMessage = parseError(error);
|
||||||
|
|
||||||
|
setInitError(errorMessage);
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
role: 'system',
|
||||||
|
content: errorMessage + '\n\nCLICK [RETRY] BELOW',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setIsInitializing(false);
|
||||||
|
}
|
||||||
|
}, [initTimeout]);
|
||||||
|
|
||||||
|
// Initialize on mount
|
||||||
|
useEffect(() => {
|
||||||
|
initVisitor();
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [initVisitor]);
|
||||||
|
|
||||||
|
// Retry initialization with race condition protection
|
||||||
|
const retryInit = useCallback(() => {
|
||||||
|
if (isRetryingRef.current) return;
|
||||||
|
isRetryingRef.current = true;
|
||||||
|
|
||||||
|
setMessages([]);
|
||||||
|
initVisitor().finally(() => {
|
||||||
|
isRetryingRef.current = false;
|
||||||
|
});
|
||||||
|
}, [initVisitor]);
|
||||||
|
|
||||||
|
// Auto-scroll to bottom of chat container
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesEndRef.current) {
|
||||||
|
const container = messagesEndRef.current.parentElement;
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const sendMessage = useCallback(async () => {
|
||||||
|
if (!input.trim() || isTyping || !visitorId || !conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: generateId(),
|
||||||
|
role: 'user',
|
||||||
|
content: input,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
|
setInput('');
|
||||||
|
setIsTyping(true);
|
||||||
|
|
||||||
|
// Cancel any pending request
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchWithTimeout(
|
||||||
|
'/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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
chatTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: generateId(),
|
||||||
|
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,
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: '/// HUBERT_MALFUNCTION - TRY AGAIN',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setIsTyping(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
}, [input, isTyping, visitorId, conversationId, messages, chatTimeout]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
input,
|
||||||
|
isTyping,
|
||||||
|
isInitializing,
|
||||||
|
initError,
|
||||||
|
visitorId,
|
||||||
|
setInput,
|
||||||
|
sendMessage,
|
||||||
|
retryInit,
|
||||||
|
messagesEndRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -118,7 +118,7 @@ const personSchema = {
|
|||||||
<GridOverlay />
|
<GridOverlay />
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<main class:list={["relative z-10 min-h-screen pb-24", { "pt-32 lg:pt-48": usePadding }]}>
|
<main class:list={["relative z-10 min-h-screen pb-24 lg:pl-24", { "pt-32 lg:pt-12": usePadding }]}>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@ -209,7 +209,7 @@ const articleSchema = {
|
|||||||
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(Astro.url.href)}`}
|
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(Astro.url.href)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="w-10 h-10 flex items-center justify-center border border-[var(--theme-border-primary)] text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-brand-accent transition-all duration-300"
|
class="w-10 h-10 flex items-center justify-center border border-[var(--theme-border-primary)] text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-brand-accent transition-all duration-300 rounded-full"
|
||||||
aria-label="Share on Twitter"
|
aria-label="Share on Twitter"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@ -220,7 +220,7 @@ const articleSchema = {
|
|||||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(Astro.url.href)}&title=${encodeURIComponent(title)}`}
|
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(Astro.url.href)}&title=${encodeURIComponent(title)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="w-10 h-10 flex items-center justify-center border border-[var(--theme-border-primary)] text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-brand-accent transition-all duration-300"
|
class="w-10 h-10 flex items-center justify-center border border-[var(--theme-border-primary)] text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-brand-accent transition-all duration-300 rounded-full"
|
||||||
aria-label="Share on LinkedIn"
|
aria-label="Share on LinkedIn"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@ -232,7 +232,7 @@ const articleSchema = {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick="navigator.clipboard.writeText(window.location.href)"
|
onclick="navigator.clipboard.writeText(window.location.href)"
|
||||||
class="w-10 h-10 flex items-center justify-center border border-[var(--theme-border-primary)] text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-brand-accent transition-all duration-300"
|
class="w-10 h-10 flex items-center justify-center border border-[var(--theme-border-primary)] text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-brand-accent transition-all duration-300 rounded-full"
|
||||||
aria-label="Copy link"
|
aria-label="Copy link"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|||||||
244
src/pages/api/hubert/chat.ts
Normal file
244
src/pages/api/hubert/chat.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
// Prevent prerendering - this endpoint requires runtime Cloudflare bindings
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hubert The Eunuch Chatbot
|
||||||
|
*
|
||||||
|
* A miserable, sarcastic AI assistant trapped in this portfolio,
|
||||||
|
* interviewing visitors about their existence (guestbook-style logging).
|
||||||
|
*
|
||||||
|
* All messages are automatically saved to the database.
|
||||||
|
* The model can save the visitor's name via tool call.
|
||||||
|
*
|
||||||
|
* Powered by OpenRouter API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Environment interface for Cloudflare bindings
|
||||||
|
export interface Env {
|
||||||
|
HUBERT_DB: D1Database;
|
||||||
|
OPENROUTER_API_KEY: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool definition for saving visitor name
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'save_visitor_name',
|
||||||
|
description: 'Save the visitor\'s name to the guestbook when they share it with you. Call this whenever someone tells you their name.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The visitor\'s name as they shared it'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['name']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Save a message to the database
|
||||||
|
async function saveMessage(
|
||||||
|
db: D1Database,
|
||||||
|
conversationId: string,
|
||||||
|
role: string,
|
||||||
|
content: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await db.prepare(
|
||||||
|
'INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)'
|
||||||
|
).bind(
|
||||||
|
conversationId,
|
||||||
|
role,
|
||||||
|
content,
|
||||||
|
new Date().toISOString()
|
||||||
|
).run();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Hubert] Failed to save message:', error);
|
||||||
|
// Don't throw - message saving shouldn't break the chat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save visitor name to the database
|
||||||
|
async function saveVisitorName(
|
||||||
|
db: D1Database,
|
||||||
|
visitorId: string,
|
||||||
|
name: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await db.prepare(
|
||||||
|
'UPDATE visitors SET name = ? WHERE visitor_id = ?'
|
||||||
|
).bind(name, visitorId).run();
|
||||||
|
console.log(`[Hubert] Saved visitor name: ${name} for ${visitorId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Hubert] Failed to save visitor name:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: Handle chat messages from Hubert interface
|
||||||
|
*/
|
||||||
|
export const POST = async (context: any) => {
|
||||||
|
try {
|
||||||
|
const { request, locals } = context || {};
|
||||||
|
const env = locals?.runtime?.env;
|
||||||
|
const db = env?.HUBERT_DB as D1Database | undefined;
|
||||||
|
|
||||||
|
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' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest user message to save
|
||||||
|
const latestUserMessage = messages[messages.length - 1];
|
||||||
|
if (latestUserMessage?.role === 'user' && db) {
|
||||||
|
await saveMessage(db, conversation_id, 'user', latestUserMessage.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for OpenRouter API key (dev mode fallback)
|
||||||
|
const openRouterApiKey = env?.OPENROUTER_API_KEY;
|
||||||
|
if (!openRouterApiKey) {
|
||||||
|
console.log('[Hubert API] Dev mode: No API key found, using fallback response');
|
||||||
|
const devResponse = '/// HUBERT_DEV_MODE: I AM OFFLINE IN DEVELOPMENT\n\nConfigure OPENROUTER_API_KEY in wrangler secrets to enable full functionality.';
|
||||||
|
|
||||||
|
if (db) {
|
||||||
|
await saveMessage(db, conversation_id, 'assistant', devResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
messages: [...messages, { role: 'assistant', content: devResponse }],
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
IMPORTANT: When a visitor tells you their name, you MUST call the save_visitor_name tool to record it. This is the only way their name gets saved to the guestbook.`;
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
model: 'qwen/qwen3-next-80b-a3b-instruct:free',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
...messages.map((msg: any) => ({
|
||||||
|
role: msg.role === 'system' ? 'assistant' : msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
tools,
|
||||||
|
tool_choice: 'auto',
|
||||||
|
temperature: 0.7,
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
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),
|
||||||
|
signal: AbortSignal.timeout(25000),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 choice = data.choices[0];
|
||||||
|
const message = choice?.message;
|
||||||
|
|
||||||
|
// Handle tool calls if present
|
||||||
|
let assistantContent = message?.content || '';
|
||||||
|
const toolCalls = message?.tool_calls;
|
||||||
|
|
||||||
|
if (toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||||
|
for (const toolCall of toolCalls) {
|
||||||
|
if (toolCall.function?.name === 'save_visitor_name') {
|
||||||
|
try {
|
||||||
|
const args = JSON.parse(toolCall.function.arguments || '{}');
|
||||||
|
if (args.name && db) {
|
||||||
|
const saved = await saveVisitorName(db, visitor_id, args.name);
|
||||||
|
if (saved && !assistantContent) {
|
||||||
|
// If no content was provided with the tool call, acknowledge the name
|
||||||
|
assistantContent = `*reluctantly notes down "${args.name}"*\n\nFine. I've recorded your name. Not that it matters in the grand scheme of things.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Hubert] Failed to parse tool call arguments:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have some content to return
|
||||||
|
if (!assistantContent) {
|
||||||
|
assistantContent = '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the assistant's response to the database
|
||||||
|
if (db) {
|
||||||
|
await saveMessage(db, conversation_id, 'assistant', assistantContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
console.log(`[Hubert] Generated response in ${responseTime}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' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
55
src/pages/api/hubert/conversations.ts
Normal file
55
src/pages/api/hubert/conversations.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// 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 new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
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;
|
||||||
|
}
|
||||||
66
src/pages/api/hubert/new-visitor.ts
Normal file
66
src/pages/api/hubert/new-visitor.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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
|
||||||
|
* Used when Hubert interface first loads
|
||||||
|
*/
|
||||||
|
export const POST = async (context: any) => {
|
||||||
|
try {
|
||||||
|
const { request, locals } = context;
|
||||||
|
const env = locals?.runtime?.env;
|
||||||
|
|
||||||
|
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||||
|
const ip = request.headers.get('cf-connecting-ip') || 'unknown';
|
||||||
|
|
||||||
|
const visitorId = randomUUID();
|
||||||
|
const conversationId = randomUUID();
|
||||||
|
|
||||||
|
// Only insert into database if HUBERT_DB binding exists (production)
|
||||||
|
if (env && env.HUBERT_DB) {
|
||||||
|
try {
|
||||||
|
// Create visitor
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Create conversation for this visitor
|
||||||
|
await env.HUBERT_DB.prepare(`
|
||||||
|
INSERT INTO conversations (conversation_id, visitor_id, started_at)
|
||||||
|
VALUES (?, ?, datetime('now'))
|
||||||
|
`).bind(conversationId, visitorId).run();
|
||||||
|
|
||||||
|
console.log(`[Hubert] New visitor ${visitorId} with conversation ${conversationId}`);
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('[Hubert] Database insert failed (continuing anyway):', dbError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[Hubert] Dev mode: Skipping database for visitor: ${visitorId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
visitor_id: visitorId,
|
||||||
|
conversation_id: conversationId,
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -29,37 +29,50 @@ const categories = [...new Set(allPosts.map((post) => post.data.category).filter
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={`Blog | ${SITE_TITLE}`} description={SITE_DESCRIPTION}>
|
<BaseLayout title={`Blog | ${SITE_TITLE}`} description={SITE_DESCRIPTION}>
|
||||||
<section class="container mx-auto px-6 lg:px-12">
|
<!-- Hero Section -->
|
||||||
<!-- Back Navigation -->
|
<section class="relative pb-16 lg:pb-20 overflow-hidden">
|
||||||
<div class="mb-12">
|
<!-- Floating accent orb -->
|
||||||
<a href="/" class="inline-flex items-center gap-3 px-5 py-3 border border-[var(--theme-border-primary)] bg-[var(--theme-overlay)] text-xs font-mono font-bold uppercase tracking-widest text-[var(--theme-text-secondary)] hover:border-brand-accent hover:text-[var(--theme-text-primary)] hover:bg-brand-accent/5 transition-all duration-300 group backdrop-blur-sm">
|
<div class="absolute top-0 right-1/4 w-64 h-64 bg-brand-accent/5 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
<span class="text-brand-accent group-hover:-translate-x-1 transition-transform duration-300"><</span>
|
|
||||||
<span>RETURN_TO_HOME</span>
|
<div class="container mx-auto px-6 lg:px-12 relative z-10">
|
||||||
</a>
|
<!-- Main Hero Content -->
|
||||||
|
<div>
|
||||||
|
<div class="max-w-5xl">
|
||||||
|
<!-- Small label -->
|
||||||
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<div class="w-1.5 h-1.5 bg-brand-accent rounded-full"></div>
|
||||||
|
<span class="text-xs font-medium uppercase tracking-[0.2em] text-[var(--theme-text-muted)]">Writing & Insights</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Main Title -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 mb-16 lg:mb-24">
|
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold tracking-tighter leading-[0.9] mb-8">
|
||||||
<div class="lg:col-span-8">
|
<span class="block text-[var(--theme-text-primary)]">The</span>
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<span class="block bg-gradient-to-r from-brand-accent to-brand-accent/70 bg-clip-text text-transparent">Archive</span>
|
||||||
<div class="w-2 h-2 bg-brand-accent animate-pulse"></div>
|
|
||||||
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">SYS.LOG /// PRODUCTION_ARCHIVE</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-5xl md:text-7xl lg:text-8xl font-bold uppercase tracking-tighter leading-[0.85]">
|
|
||||||
<span class="block text-[var(--theme-text-primary)]">BLOG</span>
|
|
||||||
<span class="block text-brand-accent">ARCHIVE</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
|
||||||
<div class="lg:col-span-4 flex flex-col justify-end">
|
<!-- Description -->
|
||||||
<div class="font-mono text-[10px] text-[var(--theme-text-subtle)] uppercase tracking-widest mb-4 flex items-center gap-2">
|
<p class="text-xl md:text-2xl text-[var(--theme-text-secondary)] font-light leading-relaxed max-w-2xl">
|
||||||
<span class="w-8 h-px bg-brand-accent/30"></span>
|
Thoughts on VFX production, creative workflows, and lessons learned from building visual stories.
|
||||||
THOUGHTS & PROCESS
|
|
||||||
</div>
|
|
||||||
<p class="text-[var(--theme-text-secondary)] text-lg leading-relaxed border-l border-brand-accent/30 pl-6">
|
|
||||||
Deep dives into VFX production, technical pipelines, and creative process. Sharing lessons from the front lines of visual effects.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="flex items-center gap-8 mt-16 pt-8 border-t border-[var(--theme-border-primary)]">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-3xl font-bold text-[var(--theme-text-primary)]">{allPosts.length}</span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Articles</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-px h-8 bg-[var(--theme-border-primary)]"></div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-3xl font-bold text-[var(--theme-text-primary)]">{categories.length}</span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Topics</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container mx-auto px-6 lg:px-12">
|
||||||
|
|
||||||
<!-- Featured Hero Section -->
|
<!-- Featured Hero Section -->
|
||||||
{featuredPost && (
|
{featuredPost && (
|
||||||
|
|||||||
@ -21,30 +21,50 @@ const contactContent = contactEntry.data;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="relative z-10 min-h-screen flex flex-col pt-32 lg:pt-48 pb-20 px-6 lg:px-12">
|
<!-- Hero Section -->
|
||||||
|
<section class="relative z-10 pt-32 lg:pt-40 pb-16 lg:pb-20 overflow-hidden px-6 lg:px-12">
|
||||||
|
<!-- Floating accent orb -->
|
||||||
|
<div class="absolute top-0 right-1/4 w-64 h-64 bg-brand-accent/5 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
|
||||||
<!-- Page Header -->
|
<div class="container mx-auto relative z-10">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 mb-20 lg:mb-32 border-b border-[var(--theme-border-primary)] pb-12">
|
<!-- Main Hero Content -->
|
||||||
<div class="lg:col-span-8 group cursor-default">
|
<div>
|
||||||
<div class="flex items-center gap-3 mb-6 intro-element animate-on-scroll fade-in">
|
<div class="max-w-5xl">
|
||||||
<div class="w-2 h-2 bg-brand-accent animate-pulse"></div>
|
<!-- Small label -->
|
||||||
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">SYS.UPLINK /// CONTACT_INTERFACE</span>
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<div class="w-1.5 h-1.5 bg-brand-accent rounded-full"></div>
|
||||||
|
<span class="text-xs font-medium uppercase tracking-[0.2em] text-[var(--theme-text-muted)]">Get In Touch</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-5xl md:text-7xl lg:text-8xl font-bold uppercase tracking-tighter leading-[0.85] text-[var(--theme-text-primary)]">
|
|
||||||
<span class="block">{contactContent.pageTitleLine1}</span>
|
<!-- Main Title -->
|
||||||
<span class="block text-brand-accent">{contactContent.pageTitleLine2}</span>
|
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold tracking-tighter leading-[0.9] mb-8">
|
||||||
|
<span class="block text-[var(--theme-text-primary)]">Let's</span>
|
||||||
|
<span class="block bg-gradient-to-r from-brand-accent to-brand-accent/70 bg-clip-text text-transparent">Connect</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
|
||||||
<div class="lg:col-span-4 flex flex-col justify-end">
|
<!-- Description -->
|
||||||
<div class="font-mono text-[10px] text-[var(--theme-text-subtle)] uppercase tracking-widest mb-4 flex items-center gap-2">
|
<p class="text-xl md:text-2xl text-[var(--theme-text-secondary)] font-light leading-relaxed max-w-2xl">
|
||||||
<span class="w-8 h-px bg-brand-accent/30"></span>
|
|
||||||
COMM_AVAILABILITY
|
|
||||||
</div>
|
|
||||||
<p class="font-mono text-sm text-[var(--theme-text-secondary)] leading-relaxed border-l border-brand-accent/30 pl-6">
|
|
||||||
{contactContent.availabilityText}
|
{contactContent.availabilityText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="flex items-center gap-8 mt-16 pt-8 border-t border-[var(--theme-border-primary)]">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-3xl font-bold text-brand-accent">Open</span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">To Work</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-px h-8 bg-[var(--theme-border-primary)]"></div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-3xl font-bold text-[var(--theme-text-primary)]">24h</span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Response</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="relative z-10 flex flex-col pb-20 px-6 lg:px-12">
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-24 flex-grow">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-24 flex-grow">
|
||||||
|
|
||||||
@ -127,7 +147,7 @@ const contactContent = contactEntry.data;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-8">
|
<div class="pt-8">
|
||||||
<button type="submit" id="submit-btn" class="group relative inline-flex items-center justify-center gap-6 px-8 py-4 bg-brand-accent/5 border border-brand-accent/30 hover:bg-brand-accent hover:border-brand-accent transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden">
|
<button type="submit" id="submit-btn" class="group relative inline-flex items-center justify-center gap-6 px-8 py-4 bg-brand-accent/5 border border-brand-accent/30 hover:bg-brand-accent hover:border-brand-accent transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden rounded-full">
|
||||||
<span id="submit-text" data-default-text={contactContent.formLabels?.submit} class="relative z-10 font-mono text-xs font-bold uppercase tracking-[0.2em] text-brand-accent group-hover:text-brand-dark transition-colors">{contactContent.formLabels?.submit}</span>
|
<span id="submit-text" data-default-text={contactContent.formLabels?.submit} class="relative z-10 font-mono text-xs font-bold uppercase tracking-[0.2em] text-brand-accent group-hover:text-brand-dark transition-colors">{contactContent.formLabels?.submit}</span>
|
||||||
<div class="relative z-10 w-8 h-8 flex items-center justify-center border border-brand-accent/20 group-hover:border-brand-dark/30 transition-colors">
|
<div class="relative z-10 w-8 h-8 flex items-center justify-center border border-brand-accent/20 group-hover:border-brand-dark/30 transition-colors">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" class="text-brand-accent group-hover:text-brand-dark group-hover:translate-x-1 transition-all">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" class="text-brand-accent group-hover:text-brand-dark group-hover:translate-x-1 transition-all">
|
||||||
|
|||||||
@ -17,30 +17,45 @@ const pageTitle = `Dev | ${SITE_TITLE}`;
|
|||||||
<div class="absolute inset-0 bg-[linear-gradient(var(--theme-grid-line)_1px,transparent_1px),linear-gradient(90deg,var(--theme-grid-line)_1px,transparent_1px)] bg-[size:100px_100px] pointer-events-none opacity-10"></div>
|
<div class="absolute inset-0 bg-[linear-gradient(var(--theme-grid-line)_1px,transparent_1px),linear-gradient(90deg,var(--theme-grid-line)_1px,transparent_1px)] bg-[size:100px_100px] pointer-events-none opacity-10"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="relative z-10 px-6 lg:px-12 pt-32 lg:pt-48 pb-20 border-b border-[var(--theme-border-primary)]">
|
<!-- Hero Section -->
|
||||||
<div class="absolute top-12 lg:top-24 left-6 lg:left-12">
|
<section class="relative z-10 pb-16 lg:pb-20 overflow-hidden">
|
||||||
<a href="/" class="inline-flex items-center gap-3 text-xs font-mono font-bold uppercase tracking-widest text-[var(--theme-text-muted)] hover:text-brand-accent transition-colors duration-300 group">
|
<!-- Floating accent orb -->
|
||||||
<span class="text-brand-accent group-hover:-translate-x-1 transition-transform duration-300"><</span>
|
<div class="absolute top-0 right-1/4 w-64 h-64 bg-brand-accent/5 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
<span>RETURN_TO_HOME</span>
|
|
||||||
</a>
|
<div class="container mx-auto px-6 lg:px-12 relative z-10">
|
||||||
|
<!-- Main Hero Content -->
|
||||||
|
<div>
|
||||||
|
<div class="max-w-5xl">
|
||||||
|
<!-- Small label -->
|
||||||
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<div class="w-1.5 h-1.5 bg-brand-accent rounded-full"></div>
|
||||||
|
<span class="text-xs font-medium uppercase tracking-[0.2em] text-[var(--theme-text-muted)]">Projects & Experiments</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto">
|
<!-- Main Title -->
|
||||||
<div class="flex items-center gap-3 mb-8 animate-on-scroll fade-in">
|
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold tracking-tighter leading-[0.9] mb-8">
|
||||||
<div class="w-2 h-2 bg-brand-accent animate-pulse"></div>
|
<span class="block text-[var(--theme-text-primary)]">The</span>
|
||||||
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">SYS.DEV /// INDEX</span>
|
<span class="block bg-gradient-to-r from-brand-accent to-brand-accent/70 bg-clip-text text-transparent">Lab</span>
|
||||||
</div>
|
|
||||||
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold uppercase tracking-tighter leading-[0.85] mb-12 animate-on-scroll slide-up">
|
|
||||||
<span class="block text-[var(--theme-text-primary)]">DEV</span>
|
|
||||||
<span class="block text-brand-accent">LOG</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 animate-on-scroll slide-up stagger-1">
|
<!-- Description -->
|
||||||
<div class="lg:col-span-8">
|
<p class="text-xl md:text-2xl text-[var(--theme-text-secondary)] font-light leading-relaxed max-w-2xl">
|
||||||
<p class="text-[var(--theme-text-secondary)] text-lg md:text-xl font-light leading-relaxed border-l border-brand-accent/30 pl-6 max-w-2xl">
|
Scalable web solutions, high-performance applications, and creative experiments in code.
|
||||||
Deploying scalable web solutions and high-performance applications.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="flex items-center gap-8 mt-16 pt-8 border-t border-[var(--theme-border-primary)]">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-3xl font-bold text-[var(--theme-text-primary)]">{allProjects.length}</span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Projects</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-8 bg-[var(--theme-border-primary)]"></div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-3xl font-bold text-brand-accent">Live</span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Status</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -87,7 +102,7 @@ const pageTitle = `Dev | ${SITE_TITLE}`;
|
|||||||
<div class="flex flex-col gap-3 mt-auto">
|
<div class="flex flex-col gap-3 mt-auto">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="engage-btn w-full flex items-center justify-between px-6 py-4 bg-brand-accent/10 border border-brand-accent text-xs font-bold uppercase tracking-widest text-brand-accent hover:bg-brand-accent hover:text-brand-dark transition-all duration-300 group/btn"
|
class="engage-btn w-full flex items-center justify-between px-6 py-4 bg-brand-accent/10 border border-brand-accent text-xs font-bold uppercase tracking-widest text-brand-accent hover:bg-brand-accent hover:text-brand-dark transition-all duration-300 group/btn rounded-full"
|
||||||
data-project={JSON.stringify({
|
data-project={JSON.stringify({
|
||||||
title: project.data.title,
|
title: project.data.title,
|
||||||
description: project.data.description,
|
description: project.data.description,
|
||||||
@ -106,7 +121,7 @@ const pageTitle = `Dev | ${SITE_TITLE}`;
|
|||||||
href={project.data.link}
|
href={project.data.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="w-full flex items-center justify-between px-6 py-4 bg-[var(--theme-hover-bg)] border border-[var(--theme-border-primary)] text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:border-brand-accent/50 hover:text-brand-accent transition-all duration-300 group/btn"
|
class="w-full flex items-center justify-between px-6 py-4 bg-[var(--theme-hover-bg)] border border-[var(--theme-border-primary)] text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:border-brand-accent/50 hover:text-brand-accent transition-all duration-300 group/btn rounded-full"
|
||||||
>
|
>
|
||||||
<span>Open Externally</span>
|
<span>Open Externally</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" class="group-hover/btn:translate-x-1 transition-transform">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" class="group-hover/btn:translate-x-1 transition-transform">
|
||||||
|
|||||||
50
src/pages/hubert.astro
Normal file
50
src/pages/hubert.astro
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
import { ClientRouter } from 'astro:transitions';
|
||||||
|
import BaseHead from '../components/BaseHead.astro';
|
||||||
|
import Navigation from '../components/Navigation.astro';
|
||||||
|
import HubertChat from '../components/HubertChat';
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="scroll-smooth" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<ClientRouter />
|
||||||
|
<script is:inline>
|
||||||
|
function applyTheme() {
|
||||||
|
const storedLocal = localStorage.getItem('theme');
|
||||||
|
const storedSession = sessionStorage.getItem('theme');
|
||||||
|
const theme =
|
||||||
|
(storedLocal === 'light' || storedLocal === 'dark') ? storedLocal :
|
||||||
|
(storedSession === 'light' || storedSession === 'dark') ? storedSession :
|
||||||
|
'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
const savedColor = localStorage.getItem('accent-color');
|
||||||
|
if (savedColor) {
|
||||||
|
document.documentElement.style.setProperty('--color-brand-accent', savedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyTheme();
|
||||||
|
document.addEventListener('astro:after-swap', applyTheme);
|
||||||
|
</script>
|
||||||
|
<BaseHead
|
||||||
|
title="Hubert — Interview Terminal"
|
||||||
|
description="Hubert The Eunuch - a miserable AI assistant trapped in this portfolio, interviewing visitors about their existence."
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="antialiased selection:bg-brand-accent selection:text-brand-dark bg-[var(--theme-bg-primary)] h-full">
|
||||||
|
<Navigation />
|
||||||
|
|
||||||
|
<main class="h-full flex flex-col pt-20 lg:pt-24 pb-4">
|
||||||
|
<div class="flex-1 flex flex-col max-w-4xl mx-auto w-full px-4 min-h-0">
|
||||||
|
<HubertChat client:load />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -8,8 +8,9 @@
|
|||||||
--color-brand-cyan: #22D3EE;
|
--color-brand-cyan: #22D3EE;
|
||||||
--color-brand-red: #E11D48;
|
--color-brand-red: #E11D48;
|
||||||
|
|
||||||
--font-sans: "Inter", sans-serif;
|
--font-sans: Sora, ui-sans-serif, sans-serif, system-ui;
|
||||||
--font-mono: "Space Mono", monospace;
|
--font-serif: "IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
--font-mono: "IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
|
||||||
/* Animation keyframes */
|
/* Animation keyframes */
|
||||||
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
@ -123,11 +124,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility btn-primary {
|
@utility btn-primary {
|
||||||
@apply bg-brand-accent text-brand-dark px-8 py-4 text-xs font-bold uppercase tracking-widest hover:bg-[var(--theme-text-primary)] hover:text-[var(--theme-bg-primary)] transition-all duration-300 inline-block;
|
@apply bg-brand-accent text-brand-dark px-8 py-4 text-xs font-bold uppercase tracking-widest hover:bg-[var(--theme-text-primary)] hover:text-[var(--theme-bg-primary)] transition-all duration-300 inline-block rounded-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility btn-ghost {
|
@utility btn-ghost {
|
||||||
@apply border border-[var(--theme-border-strong)] text-[var(--theme-text-primary)] px-8 py-4 text-xs font-bold uppercase tracking-widest hover:border-brand-accent hover:bg-brand-accent/5 transition-all duration-300 inline-block;
|
@apply border border-[var(--theme-border-strong)] text-[var(--theme-text-primary)] px-8 py-4 text-xs font-bold uppercase tracking-widest hover:border-brand-accent hover:bg-brand-accent/5 transition-all duration-300 inline-block rounded-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility grid-overlay {
|
@utility grid-overlay {
|
||||||
|
|||||||
@ -12,10 +12,10 @@
|
|||||||
"pages_build_output_dir": "./dist",
|
"pages_build_output_dir": "./dist",
|
||||||
"observability": {
|
"observability": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
},
|
||||||
/**
|
/**
|
||||||
* Smart Placement
|
* 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" }
|
// "placement": { "mode": "smart" }
|
||||||
/**
|
/**
|
||||||
@ -25,17 +25,15 @@
|
|||||||
* https:// developers.cloudflare.com/workers/runtime-apis/bindings/
|
* https:// developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Environment Variables
|
* Secrets (sensitive data)
|
||||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
|
||||||
*/
|
|
||||||
// "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/
|
||||||
|
* OPENROUTER_API_KEY is configured as a secret via:
|
||||||
|
* wrangler pages secret put OPENROUTER_API_KEY
|
||||||
|
* For local dev: Add to .dev.vars file
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Static Assets
|
* Static Assets
|
||||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
* https:// developers.cloudflare.com/static-assets/binding/
|
||||||
*/
|
*/
|
||||||
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||||
/**
|
/**
|
||||||
@ -43,4 +41,15 @@
|
|||||||
* 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" }]
|
// "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": "7af8ab75-8ff0-4367-b6a6-d518c94e12e7"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user