feat(agent): add AI chat panel and dashboard updates (#34)

* feat(agent): add AI chat panel and dashboard updates

Add ElizaOS-powered agent chat panel with streaming,
voice input, markdown rendering, and page-aware context.
Update dashboard layout with context menu and refactored
pages. Add agent memory schema, new UI components,
and fix lint errors across AI-related files.

* fix(auth): use Host header for SSO redirect URI

nextUrl.origin returns http://localhost:3000 on CF Workers,
breaking OAuth callbacks. Use Host header to derive the
correct production origin for WorkOS redirect URI.

* fix(auth): add Toaster to auth layout, fix error codes

Auth pages had no Toaster component so toast.error() calls
were invisible. Also return 401 for auth errors instead of
generic 500 from the login API.

---------

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
This commit is contained in:
Nicholai 2026-02-05 15:56:06 -07:00 committed by GitHub
parent a0dd50f59b
commit a0f7852845
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 12152 additions and 356 deletions

3
.gitignore vendored
View File

@ -26,4 +26,7 @@ dist/
.playwright-mcp
mobile-ui-references/
.fuse_*
# directories
tmp/
references/

304
bun.lock
View File

@ -10,6 +10,8 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@json-render/core": "^0.4.0",
"@json-render/react": "^0.4.0",
"@opennextjs/cloudflare": "^1.14.4",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@ -41,23 +43,31 @@
"@tanstack/react-table": "^8.21.3",
"@workos-inc/authkit-nextjs": "^2.13.0",
"@workos-inc/node": "^8.1.0",
"ai": "^6.0.72",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.45.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "11",
"frappe-gantt": "^1.0.4",
"input-otp": "^1.4.2",
"lucide-react": "^0.562.0",
"lucide-react": "^0.563.0",
"nanoid": "^5.1.6",
"next": "15.5.9",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.1.4",
"react-day-picker": "^9.13.0",
"react-dom": "19.1.4",
"react-hook-form": "^7.71.1",
"react-markdown": "10",
"react-resizable-panels": "^4.4.1",
"recharts": "2.15.4",
"remark-gfm": "4",
"remeda": "2",
"shiki": "1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
@ -81,6 +91,12 @@
},
},
"packages": {
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9aRTVM1P1u4yUIjBpco/WCF1WXr/DgWKuDYgLLHdENS8kiEuxDOPJuGbc/6+7EwQ6ZqSh0UOgeqvHfGJfU23Qg=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
@ -413,6 +429,10 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@json-render/core": ["@json-render/core@0.4.0", "", { "dependencies": { "zod": "^4.0.0" } }, "sha512-zcmNNetyXoqShG9qfXnipnkaGLn3XC7Kec/t3+C39mXAStg2RX29ciZwsZT+Fzo900LeRHRIGel7L/IHCdktrA=="],
"@json-render/react": ["@json-render/react@0.4.0", "", { "dependencies": { "@json-render/core": "0.4.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-OAXdWdOrAXHEFzpEF7xV+84D00JEmLMKlt5u0wc+C/P+q4q6TnpAWx2j28PBpDB2mpidkW9VnTUM+SCH9J8Lrw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@next/env": ["@next/env@15.5.9", "", {}, "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg=="],
@ -459,6 +479,8 @@
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.15.0", "", { "dependencies": { "@ast-grep/napi": "0.40.0", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.11", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "^14.2.35 || ~15.0.7 || ~15.1.11 || ~15.2.8 || ~15.3.8 || ~15.4.10 || ~15.5.9 || ^16.0.10", "wrangler": "^4.59.2" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-AZPaqk25XUBxtdkfjUZQBbY3ovifVLC4GgSRHuejqsIWfv8KjTRNFVdaCaaPmbLkrgymqxNhkbfJS5sD28AK/g=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="],
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
@ -475,6 +497,8 @@
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
@ -509,6 +533,8 @@
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
@ -521,6 +547,10 @@
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
@ -551,10 +581,14 @@
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
@ -583,6 +617,20 @@
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="],
"@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="],
"@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="],
"@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="],
"@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
"@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="],
@ -689,6 +737,8 @@
"@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
@ -765,12 +815,18 @@
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
@ -785,8 +841,12 @@
"@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
@ -803,6 +863,8 @@
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="],
@ -823,6 +885,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
@ -861,6 +925,8 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@2.13.0", "", { "dependencies": { "@workos-inc/node": "^7.72.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-ppxzhfakPumHPPggYSROaAlgxfS7viFMPmWPG76Tp6Rh9G7YqkBSp7xtvMtM6gXOFFMvvEJRcKEta6YHeercTQ=="],
"@workos-inc/node": ["@workos-inc/node@8.1.0", "", { "dependencies": { "iron-webcrypto": "^2.0.0", "jose": "~6.1.0" } }, "sha512-Ep2QSP43y4ZdJIOuL4Hjaq5f0u8Z0qZe7QWzrrBV6cHc/kcicDBcB0AanMP6eB9x3x6FaHfevLbkbjPF4+TCYQ=="],
@ -875,6 +941,8 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@6.0.72", "", { "dependencies": { "@ai-sdk/gateway": "3.0.35", "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D3TzDX6LzYL8qwi1A0rLnmuUexqDcCu4LSg77hcDHsqNRkaGspGItkz1U3RnN3ojv31XQYI9VmoWpkj44uvIUA=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
@ -921,6 +989,8 @@
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
@ -951,8 +1021,18 @@
"caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
@ -971,6 +1051,8 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@ -1025,6 +1107,8 @@
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
@ -1035,10 +1119,14 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
@ -1067,6 +1155,8 @@
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
@ -1131,6 +1221,8 @@
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
@ -1139,10 +1231,14 @@
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
@ -1183,6 +1279,8 @@
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="],
"frappe-gantt": ["frappe-gantt@1.0.4", "", {}, "sha512-N94OP9ZiapaG5nzgCeZdxsKP8HD5aLVlH5sEHxSNZQnNKQ4BOn2l46HUD+KIE0LpYIterP7gIrFfkLNRuK0npQ=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
@ -1243,6 +1341,16 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
@ -1263,6 +1371,8 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
@ -1275,6 +1385,10 @@
"iron-webcrypto": ["iron-webcrypto@2.0.0", "", { "dependencies": { "uint8array-extras": "^1.5.0" } }, "sha512-rtffZKDUHciZElM8mjFCufBC7nVhCxHYyWHESqs89OioEDz4parOofd8/uhrejh/INhQFfYQfByS22LlezR9sQ=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
@ -1293,6 +1407,8 @@
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
@ -1303,6 +1419,8 @@
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
@ -1311,6 +1429,8 @@
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@ -1351,6 +1471,8 @@
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
@ -1401,16 +1523,50 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
@ -1419,6 +1575,62 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
@ -1439,9 +1651,13 @@
"mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="],
"motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="],
"motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
@ -1485,6 +1701,8 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@ -1497,6 +1715,8 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@ -1523,6 +1743,8 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@ -1535,6 +1757,8 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
@ -1549,6 +1773,8 @@
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@ -1567,8 +1793,24 @@
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
"regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"remeda": ["remeda@2.33.5", "", {}, "sha512-FqmpPA9i9T5EGcqgyHf9kHjefnyCZM1M3kSdZjPk1j2StGNoJyoYp0807RYcjNkQ1UpsEQa5qzgsjLY4vYtT8g=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@ -1611,6 +1853,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
@ -1629,6 +1873,8 @@
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
@ -1651,6 +1897,8 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -1663,6 +1911,10 @@
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@ -1687,6 +1939,10 @@
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="],
@ -1723,6 +1979,18 @@
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
@ -1743,6 +2011,10 @@
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
@ -1793,6 +2065,8 @@
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="],
@ -2333,6 +2607,8 @@
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@ -2349,6 +2625,8 @@
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@smithy/chunked-blob-reader-native/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
@ -2471,14 +2749,32 @@
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"miniflare/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"radix-ui/@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
"radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
"radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"radix-ui/@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
"radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
"radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
@ -3005,6 +3301,8 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],

2725
docs/spec.json Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
CREATE TABLE `agent_conversations` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`title` text,
`last_message_at` text NOT NULL,
`created_at` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `agent_memories` (
`id` text PRIMARY KEY NOT NULL,
`conversation_id` text NOT NULL,
`user_id` text NOT NULL,
`role` text NOT NULL,
`content` text NOT NULL,
`embedding` text,
`metadata` text,
`created_at` text NOT NULL,
FOREIGN KEY (`conversation_id`) REFERENCES `agent_conversations`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);

2324
drizzle/meta/0008_snapshot.json Executable file

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,13 @@
"when": 1770321600000,
"tag": "0007_add_customer_fields",
"breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1770320934942,
"tag": "0008_superb_lifeguard",
"breakpoints": true
}
]
}

View File

@ -47,11 +47,4 @@ VALUES
('gm-1', 'group-1', 'user-2', '2026-01-15T00:00:00Z'),
('gm-2', 'group-2', 'user-4', '2026-01-25T00:00:00Z');
-- seed project members (using existing project)
INSERT INTO project_members (id, project_id, user_id, role, assigned_at)
VALUES
('pm-1', 'proj-o-001', 'user-1', 'admin', '2026-01-01T00:00:00Z'),
('pm-2', 'proj-o-001', 'user-2', 'manager', '2026-01-15T00:00:00Z'),
('pm-3', 'proj-o-001', 'user-4', 'crew', '2026-01-25T00:00:00Z'),
('pm-4', 'proj-o-002', 'user-2', 'manager', '2026-01-16T00:00:00Z'),
('pm-5', 'proj-o-003', 'user-3', 'manager', '2026-01-21T00:00:00Z');
-- project_members are seeded in seed.sql (after projects exist)

View File

@ -152,6 +152,16 @@ INSERT OR IGNORE INTO schedule_tasks (id, project_id, title, start_date, workday
('task-n001-040', 'proj-n-001', 'Punch List', '2026-03-19', 5, '2026-03-25', 'closeout', 'PENDING', 0, 0, 0, NULL, 40, '2025-08-15T09:00:00Z', '2025-08-15T09:00:00Z'),
('task-n001-041', 'proj-n-001', 'Certificate of Occupancy', '2026-03-26', 2, '2026-03-27', 'closeout', 'PENDING', 1, 1, 0, 'Daniel M Vogel', 41, '2025-08-15T09:00:00Z', '2025-08-15T09:00:00Z');
-- ─── Project Members (must come after projects) ───
INSERT OR IGNORE INTO project_members (id, project_id, user_id, role, assigned_at)
VALUES
('pm-1', 'proj-o-001', 'user-1', 'admin', '2026-01-01T00:00:00Z'),
('pm-2', 'proj-o-001', 'user-2', 'manager', '2026-01-15T00:00:00Z'),
('pm-3', 'proj-o-001', 'user-4', 'crew', '2026-01-25T00:00:00Z'),
('pm-4', 'proj-o-002', 'user-2', 'manager', '2026-01-16T00:00:00Z'),
('pm-5', 'proj-o-003', 'user-3', 'manager', '2026-01-21T00:00:00Z');
-- ─── Dependencies for N-001 (finish-to-start) ───
INSERT OR IGNORE INTO task_dependencies (id, predecessor_id, successor_id, type, lag_days) VALUES

View File

@ -1,7 +1,21 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
eslint: {
ignoreDuringBuilds: true,
},
experimental: {
optimizePackageImports: [
"@tabler/icons-react",
"lucide-react",
"@radix-ui/react-icons",
"recharts",
"@workos-inc/node",
"date-fns",
"remeda",
"framer-motion",
],
},
};
export default nextConfig;

View File

@ -22,6 +22,8 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@json-render/core": "^0.4.0",
"@json-render/react": "^0.4.0",
"@opennextjs/cloudflare": "^1.14.4",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@ -53,23 +55,31 @@
"@tanstack/react-table": "^8.21.3",
"@workos-inc/authkit-nextjs": "^2.13.0",
"@workos-inc/node": "^8.1.0",
"ai": "^6.0.72",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.45.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "11",
"frappe-gantt": "^1.0.4",
"input-otp": "^1.4.2",
"lucide-react": "^0.562.0",
"lucide-react": "^0.563.0",
"nanoid": "^5.1.6",
"next": "15.5.9",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.1.4",
"react-day-picker": "^9.13.0",
"react-dom": "19.1.4",
"react-hook-form": "^7.71.1",
"react-markdown": "10",
"react-resizable-panels": "^4.4.1",
"recharts": "2.15.4",
"remark-gfm": "4",
"remeda": "2",
"shiki": "1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",

View File

@ -1,9 +1,10 @@
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent } from "@/components/ui/card"
import { Toaster } from "@/components/ui/sonner"
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
children: React.ReactNode
}) {
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-muted/30">
@ -26,6 +27,7 @@ export default function AuthLayout({
High Performance Structures
</p>
</div>
<Toaster position="bottom-right" />
</div>
);
)
}

196
src/app/api/agent/route.ts Executable file
View File

@ -0,0 +1,196 @@
/**
* Agent API Route - Proxy to ElizaOS Server
*
* POST /api/agent - Send message to the Compass agent
* GET /api/agent - Get conversation history
*
* This route proxies requests to the ElizaOS sidecar server,
* handling auth on the Next.js side and forwarding messages
* to the agent's sessions API.
*/
import { NextResponse } from "next/server"
import { getCurrentUser } from "@/lib/auth"
const ELIZAOS_URL =
process.env.ELIZAOS_API_URL ?? "http://localhost:3001"
interface RequestBody {
message: string
conversationId?: string
context?: {
view?: string
projectId?: string
}
}
interface ElizaSessionResponse {
id: string
agentId?: string
userId?: string
}
interface ElizaMessageResponse {
id: string
content: string
authorId?: string
createdAt?: string
metadata?: Record<string, unknown>
sessionStatus?: Record<string, unknown>
}
async function getOrCreateSession(
userId: string,
conversationId?: string
): Promise<string> {
if (conversationId) return conversationId
const response = await fetch(
`${ELIZAOS_URL}/api/messaging/sessions`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
}
)
if (!response.ok) {
throw new Error(
`Failed to create session: ${response.status} ${response.statusText}`
)
}
const data: ElizaSessionResponse = await response.json()
return data.id
}
export async function POST(request: Request): Promise<Response> {
try {
const user = await getCurrentUser()
if (!user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
const body: RequestBody = await request.json()
if (!body.message || typeof body.message !== "string") {
return NextResponse.json(
{ error: "Message is required" },
{ status: 400 }
)
}
const sessionId = await getOrCreateSession(
user.id,
body.conversationId
)
// Send message to ElizaOS sessions API
const response = await fetch(
`${ELIZAOS_URL}/api/messaging/sessions/${sessionId}/messages`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: body.message,
metadata: {
source: body.context?.view ?? "dashboard",
projectId: body.context?.projectId,
userId: user.id,
userRole: user.role,
userName: user.displayName ?? user.email,
},
}),
}
)
if (!response.ok) {
const errorText = await response.text()
console.error("ElizaOS error:", errorText)
return NextResponse.json(
{ error: "Agent unavailable" },
{ status: 502 }
)
}
const data: ElizaMessageResponse = await response.json()
// Extract action data from metadata if present
const actionData = data.metadata?.action as
| { type: string; payload?: Record<string, unknown> }
| undefined
const actions = actionData ? [actionData] : undefined
return NextResponse.json({
id: data.id ?? crypto.randomUUID(),
text: data.content ?? "",
actions,
ui: data.metadata?.ui,
conversationId: sessionId,
})
} catch (error) {
console.error("Agent API error:", error)
return NextResponse.json(
{
error:
error instanceof Error
? error.message
: "Internal server error",
},
{ status: 500 }
)
}
}
export async function GET(request: Request): Promise<Response> {
try {
const user = await getCurrentUser()
if (!user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get("conversationId")
if (!sessionId) {
// No session listing support via proxy yet
return NextResponse.json({ conversations: [] })
}
// Get messages from ElizaOS session
const response = await fetch(
`${ELIZAOS_URL}/api/messaging/sessions/${sessionId}/messages?limit=100`
)
if (!response.ok) {
return NextResponse.json(
{ error: "Session not found" },
{ status: 404 }
)
}
const messages = await response.json()
return NextResponse.json({
conversation: { id: sessionId },
messages,
})
} catch (error) {
console.error("Agent API error:", error)
return NextResponse.json(
{
error:
error instanceof Error
? error.message
: "Internal server error",
},
{ status: 500 }
)
}
}

View File

@ -151,9 +151,12 @@ export async function POST(request: NextRequest) {
)
} catch (error) {
console.error("Login error:", error)
const err = error as { code?: string }
const isAuthError = ["invalid_credentials", "user_not_found",
"expired_code", "invalid_code"].includes(err.code || "")
return NextResponse.json(
{ success: false, error: mapWorkOSError(error) },
{ status: 500 }
{ status: isAuthError ? 401 : 500 }
)
}
}

View File

@ -32,10 +32,16 @@ export async function GET(request: NextRequest) {
const workos = getWorkOS()
// derive origin from Host header (nextUrl.origin is wrong on CF Workers)
const host = request.headers.get("host")
const proto = request.headers.get("x-forwarded-proto") || "https"
const origin = host ? `${proto}://${host}` : request.nextUrl.origin
const redirectUri = `${origin}/api/auth/callback`
const authorizationUrl = workos.userManagement.getAuthorizationUrl({
provider: provider as Provider,
clientId: process.env.WORKOS_CLIENT_ID!,
redirectUri: process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI!,
redirectUri,
state: from || "/dashboard",
})

View File

@ -2,6 +2,7 @@
import * as React from "react"
import { IconPlus } from "@tabler/icons-react"
import { Plus } from "lucide-react"
import { toast } from "sonner"
import {
@ -14,6 +15,7 @@ import type { Customer } from "@/db/schema"
import { Button } from "@/components/ui/button"
import { CustomersTable } from "@/components/financials/customers-table"
import { CustomerDialog } from "@/components/financials/customer-dialog"
import { useRegisterPageActions } from "@/hooks/use-register-page-actions"
export default function CustomersPage() {
const [customers, setCustomers] = React.useState<Customer[]>([])
@ -21,6 +23,24 @@ export default function CustomersPage() {
const [dialogOpen, setDialogOpen] = React.useState(false)
const [editing, setEditing] = React.useState<Customer | null>(null)
const openCreate = React.useCallback(() => {
setEditing(null)
setDialogOpen(true)
}, [])
const pageActions = React.useMemo(
() => [
{
id: "add-customer",
label: "Add Customer",
icon: Plus,
onSelect: openCreate,
},
],
[openCreate]
)
useRegisterPageActions(pageActions)
const load = async () => {
try {
const data = await getCustomers()
@ -34,11 +54,6 @@ export default function CustomersPage() {
React.useEffect(() => { load() }, [])
const handleCreate = () => {
setEditing(null)
setDialogOpen(true)
}
const handleEdit = (customer: Customer) => {
setEditing(customer)
setDialogOpen(true)
@ -115,7 +130,7 @@ export default function CustomersPage() {
Manage customer accounts
</p>
</div>
<Button onClick={handleCreate} className="w-full sm:w-auto">
<Button onClick={openCreate} className="w-full sm:w-auto">
<IconPlus className="mr-2 size-4" />
Add Customer
</Button>

View File

@ -2,8 +2,10 @@
import * as React from "react"
import { IconPlus } from "@tabler/icons-react"
import { Plus } from "lucide-react"
import { useSearchParams, useRouter } from "next/navigation"
import { toast } from "sonner"
import { useRegisterPageActions } from "@/hooks/use-register-page-actions"
import { getCustomers } from "@/app/actions/customers"
import { getVendors } from "@/app/actions/vendors"
@ -137,6 +139,62 @@ function FinancialsContent() {
React.useEffect(() => { loadAll() }, [])
const openInvoice = React.useCallback(() => {
setEditingInvoice(null)
setInvoiceDialogOpen(true)
}, [])
const openBill = React.useCallback(() => {
setEditingBill(null)
setBillDialogOpen(true)
}, [])
const openPayment = React.useCallback(() => {
setEditingPayment(null)
setPaymentDialogOpen(true)
}, [])
const openMemo = React.useCallback(() => {
setEditingMemo(null)
setMemoDialogOpen(true)
}, [])
const TAB_ACTIONS: Record<
Tab,
{ id: string; label: string; onSelect: () => void }
> = React.useMemo(
() => ({
invoices: {
id: "new-invoice",
label: "New Invoice",
onSelect: openInvoice,
},
bills: {
id: "new-bill",
label: "New Bill",
onSelect: openBill,
},
payments: {
id: "new-payment",
label: "New Payment",
onSelect: openPayment,
},
"credit-memos": {
id: "new-credit-memo",
label: "New Credit Memo",
onSelect: openMemo,
},
}),
[openInvoice, openBill, openPayment, openMemo]
)
const pageActions = React.useMemo(() => {
const action = TAB_ACTIONS[tab]
return [{ ...action, icon: Plus }]
}, [tab, TAB_ACTIONS])
useRegisterPageActions(pageActions)
const handleTabChange = (value: string) => {
setTab(value as Tab)
router.replace(`/dashboard/financials?tab=${value}`, { scroll: false })
@ -293,53 +351,25 @@ function FinancialsContent() {
</div>
{tab === "invoices" && (
<Button
onClick={() => {
setEditingInvoice(null)
setInvoiceDialogOpen(true)
}}
size="sm"
className="w-full sm:w-auto h-9"
>
<Button onClick={openInvoice} size="sm" className="w-full sm:w-auto h-9">
<IconPlus className="mr-2 size-4" />
New Invoice
</Button>
)}
{tab === "bills" && (
<Button
onClick={() => {
setEditingBill(null)
setBillDialogOpen(true)
}}
size="sm"
className="w-full sm:w-auto h-9"
>
<Button onClick={openBill} size="sm" className="w-full sm:w-auto h-9">
<IconPlus className="mr-2 size-4" />
New Bill
</Button>
)}
{tab === "payments" && (
<Button
onClick={() => {
setEditingPayment(null)
setPaymentDialogOpen(true)
}}
size="sm"
className="w-full sm:w-auto h-9"
>
<Button onClick={openPayment} size="sm" className="w-full sm:w-auto h-9">
<IconPlus className="mr-2 size-4" />
New Payment
</Button>
)}
{tab === "credit-memos" && (
<Button
onClick={() => {
setEditingMemo(null)
setMemoDialogOpen(true)
}}
size="sm"
className="w-full sm:w-auto h-9"
>
<Button onClick={openMemo} size="sm" className="w-full sm:w-auto h-9">
<IconPlus className="mr-2 size-4" />
New Credit Memo
</Button>

View File

@ -4,7 +4,11 @@ import { MobileBottomNav } from "@/components/mobile-bottom-nav"
import { CommandMenuProvider } from "@/components/command-menu-provider"
import { SettingsProvider } from "@/components/settings-provider"
import { FeedbackWidget } from "@/components/feedback-widget"
import { PageActionsProvider } from "@/components/page-actions-provider"
import { DashboardContextMenu } from "@/components/dashboard-context-menu"
import { Toaster } from "@/components/ui/sonner"
import { ChatPanel } from "@/components/agent/chat-panel"
import { AgentProvider } from "@/components/agent/agent-provider"
import {
SidebarInset,
SidebarProvider,
@ -26,7 +30,9 @@ export default async function DashboardLayout({
return (
<SettingsProvider>
<AgentProvider>
<ProjectListProvider projects={projectList}>
<PageActionsProvider>
<CommandMenuProvider>
<SidebarProvider
defaultOpen={false}
@ -41,10 +47,15 @@ export default async function DashboardLayout({
<FeedbackWidget>
<SidebarInset className="overflow-hidden">
<SiteHeader user={user} />
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden pb-14 md:pb-0">
<div className="@container/main flex flex-1 flex-col min-w-0">
{children}
<div className="flex min-h-0 flex-1 overflow-hidden">
<DashboardContextMenu>
<div className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden pb-14 md:pb-0 min-w-0">
<div className="@container/main flex flex-1 flex-col min-w-0">
{children}
</div>
</div>
</DashboardContextMenu>
<ChatPanel />
</div>
</SidebarInset>
</FeedbackWidget>
@ -55,7 +66,9 @@ export default async function DashboardLayout({
<Toaster position="bottom-right" />
</SidebarProvider>
</CommandMenuProvider>
</PageActionsProvider>
</ProjectListProvider>
</AgentProvider>
</SettingsProvider>
)
}

View File

@ -1,18 +1,6 @@
export const dynamic = "force-dynamic"
import { FeedbackCallout } from "@/components/feedback-widget"
import {
IconBrandGithub,
IconExternalLink,
IconGitCommit,
IconGitFork,
IconStar,
IconAlertCircle,
IconEye,
} from "@tabler/icons-react"
const REPO = "High-Performance-Structures/compass"
const GITHUB_URL = `https://github.com/${REPO}`
import { DashboardChat } from "@/components/dashboard-chat"
type RepoStats = {
stargazers_count: number
@ -21,20 +9,16 @@ type RepoStats = {
subscribers_count: number
}
type Commit = {
sha: string
commit: {
message: string
author: { name: string; date: string }
}
html_url: string
}
const REPO = "High-Performance-Structures/compass"
async function getRepoData() {
async function getRepoStats(): Promise<RepoStats | null> {
try {
const { getCloudflareContext } = await import("@opennextjs/cloudflare")
const { getCloudflareContext } = await import(
"@opennextjs/cloudflare"
)
const { env } = await getCloudflareContext()
const token = (env as unknown as Record<string, unknown>).GITHUB_TOKEN as string | undefined
const token = (env as unknown as Record<string, unknown>)
.GITHUB_TOKEN as string | undefined
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
@ -42,234 +26,19 @@ async function getRepoData() {
}
if (token) headers.Authorization = `Bearer ${token}`
const [repoRes, commitsRes] = await Promise.all([
fetch(`https://api.github.com/repos/${REPO}`, {
next: { revalidate: 300 },
headers,
}),
fetch(`https://api.github.com/repos/${REPO}/commits?per_page=8`, {
next: { revalidate: 300 },
headers,
}),
])
if (!repoRes.ok || !commitsRes.ok) return null
const repo: RepoStats = await repoRes.json()
const commits: Commit[] = await commitsRes.json()
return { repo, commits }
const res = await fetch(
`https://api.github.com/repos/${REPO}`,
{ next: { revalidate: 300 }, headers }
)
if (!res.ok) return null
return (await res.json()) as RepoStats
} catch {
return null
}
}
function timeAgo(date: string) {
const seconds = Math.floor(
(Date.now() - new Date(date).getTime()) / 1000
)
if (seconds < 60) return "just now"
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
if (days < 30) return `${days}d ago`
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}
export default async function Page() {
const data = await getRepoData()
const stats = await getRepoStats()
return (
<div className="flex flex-1 items-start justify-center p-3 sm:p-6 md:p-12 overflow-x-hidden min-w-0">
<div className="w-full max-w-6xl py-4 sm:py-8 min-w-0">
<div className="mb-6 sm:mb-10 text-center">
<span
className="mx-auto mb-3 block size-12 bg-foreground"
style={{
maskImage: "url(/logo-black.png)",
maskSize: "contain",
maskRepeat: "no-repeat",
WebkitMaskImage: "url(/logo-black.png)",
WebkitMaskSize: "contain",
WebkitMaskRepeat: "no-repeat",
}}
/>
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
Compass
</h1>
<p className="text-muted-foreground mt-2 text-sm sm:text-base px-2">
Development preview features may be incomplete
or change without notice.
</p>
<div className="mt-4">
<FeedbackCallout />
</div>
</div>
<div className="grid gap-6 sm:gap-10 lg:grid-cols-2 min-w-0">
<div className="space-y-6 sm:space-y-8 text-sm leading-relaxed min-w-0">
<section>
<h2 className="mb-2 sm:mb-3 text-sm sm:text-base font-semibold flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-green-500" />
Working
</h2>
<ul className="space-y-1.5 pl-4 break-words">
<li className="break-words">Projects create and manage projects with D1 database</li>
<li className="break-words">Schedule Gantt chart with phases, tasks, dependencies, and critical path</li>
<li className="break-words">File browser drive-style UI with folder navigation</li>
<li className="break-words">Settings app preferences with theme and notifications</li>
<li className="break-words">Sidebar navigation with contextual project/file views</li>
<li className="break-words">Command palette search (Cmd+K)</li>
</ul>
</section>
<section>
<h2 className="mb-2 sm:mb-3 text-sm sm:text-base font-semibold flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-yellow-500" />
In Progress
</h2>
<ul className="space-y-1.5 pl-4 break-words">
<li className="break-words">Project auto-provisioning (code generation, CSI folder structure)</li>
<li className="break-words">Budget tracking (CSI divisions, estimated vs actual, change orders)</li>
<li className="break-words">Document management (S3/R2 storage, metadata, versioning)</li>
<li className="break-words">Communication logging (manual entries, timeline view)</li>
<li className="break-words">Dashboard three-column layout (past due, due today, action items)</li>
<li className="break-words">User authentication and roles (WorkOS)</li>
<li className="break-words">Email notifications (Resend)</li>
<li className="break-words">Basic reports (budget variance, overdue tasks, monthly actuals)</li>
</ul>
</section>
<section>
<h2 className="mb-2 sm:mb-3 text-sm sm:text-base font-semibold flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-muted-foreground/50" />
Planned
</h2>
<ul className="space-y-1.5 pl-4 text-muted-foreground break-words">
<li className="break-words">Client portal with read-only views</li>
<li className="break-words">BuilderTrend import wizard (CSV-based)</li>
<li className="break-words">Daily logs</li>
<li className="break-words">Time tracking</li>
<li className="break-words">Report builder (custom fields and filters)</li>
<li className="break-words">Bid package management</li>
</ul>
</section>
<section>
<h2 className="mb-2 sm:mb-3 text-sm sm:text-base font-semibold flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-muted-foreground/30" />
Future
</h2>
<ul className="space-y-1.5 pl-4 text-muted-foreground break-words">
<li className="break-words">Netsuite/QuickBooks API sync</li>
<li className="break-words">Payment integration</li>
<li className="break-words">RFI/Submittal tracking</li>
<li className="break-words">Native mobile apps (iOS/Android)</li>
<li className="break-words">Advanced scheduling (resource leveling, baseline comparison)</li>
</ul>
</section>
</div>
{data && (
<div className="lg:sticky lg:top-6 lg:self-start space-y-4 sm:space-y-6 min-w-0">
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="hover:bg-muted/50 border rounded-lg px-3 sm:px-4 py-3 flex items-center gap-3 transition-colors"
>
<IconBrandGithub className="size-5 shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">View on GitHub</p>
<p className="text-muted-foreground text-xs truncate">{REPO}</p>
</div>
<IconExternalLink className="text-muted-foreground size-3.5 shrink-0" />
</a>
<div className="grid grid-cols-2 gap-3">
<StatCard
icon={<IconStar className="size-4" />}
label="Stars"
value={data.repo.stargazers_count}
/>
<StatCard
icon={<IconGitFork className="size-4" />}
label="Forks"
value={data.repo.forks_count}
/>
<StatCard
icon={<IconAlertCircle className="size-4" />}
label="Issues"
value={data.repo.open_issues_count}
/>
<StatCard
icon={<IconEye className="size-4" />}
label="Watchers"
value={data.repo.subscribers_count}
/>
</div>
<div>
<h2 className="text-muted-foreground mb-2 sm:mb-3 text-xs font-medium uppercase tracking-wider">
Recent Commits
</h2>
<div className="border rounded-lg divide-y">
{data.commits.map((commit) => (
<a
key={commit.sha}
href={commit.html_url}
target="_blank"
rel="noopener noreferrer"
className="hover:bg-muted/50 flex items-start gap-2 sm:gap-3 px-3 sm:px-4 py-2 sm:py-3 transition-colors"
>
<IconGitCommit className="text-muted-foreground mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm break-words">
{commit.commit.message.split("\n")[0]}
</p>
<p className="text-muted-foreground mt-0.5 text-xs truncate">
{commit.commit.author.name}
<span className="mx-1.5">·</span>
{timeAgo(commit.commit.author.date)}
</p>
</div>
<code className="text-muted-foreground shrink-0 font-mono text-xs hidden sm:inline">
{commit.sha.slice(0, 7)}
</code>
</a>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
)
}
function StatCard({
icon,
label,
value,
}: {
icon: React.ReactNode
label: string
value: number
}) {
return (
<div className="border rounded-lg px-3 sm:px-4 py-2 sm:py-3">
<div className="text-muted-foreground mb-1 flex items-center gap-1.5 text-xs">
{icon}
{label}
</div>
<p className="text-xl sm:text-2xl font-semibold tabular-nums">
{value.toLocaleString()}
</p>
</div>
)
return <DashboardChat stats={stats} />
}

View File

@ -2,6 +2,7 @@
import * as React from "react"
import { IconUserPlus } from "@tabler/icons-react"
import { UserPlus } from "lucide-react"
import { toast } from "sonner"
import { getUsers, deactivateUser, type UserWithRelations } from "@/app/actions/users"
@ -9,6 +10,7 @@ import { Button } from "@/components/ui/button"
import { PeopleTable } from "@/components/people-table"
import { UserDrawer } from "@/components/people/user-drawer"
import { InviteDialog } from "@/components/people/invite-dialog"
import { useRegisterPageActions } from "@/hooks/use-register-page-actions"
export default function PeoplePage() {
@ -18,6 +20,19 @@ export default function PeoplePage() {
const [drawerOpen, setDrawerOpen] = React.useState(false)
const [inviteDialogOpen, setInviteDialogOpen] = React.useState(false)
const pageActions = React.useMemo(
() => [
{
id: "invite-user",
label: "Invite User",
icon: UserPlus,
onSelect: () => setInviteDialogOpen(true),
},
],
[]
)
useRegisterPageActions(pageActions)
React.useEffect(() => {
loadUsers()
}, [])

View File

@ -2,6 +2,7 @@
import * as React from "react"
import { IconPlus } from "@tabler/icons-react"
import { Plus } from "lucide-react"
import { toast } from "sonner"
import {
@ -14,6 +15,7 @@ import type { Vendor } from "@/db/schema"
import { Button } from "@/components/ui/button"
import { VendorsTable } from "@/components/financials/vendors-table"
import { VendorDialog } from "@/components/financials/vendor-dialog"
import { useRegisterPageActions } from "@/hooks/use-register-page-actions"
export default function VendorsPage() {
const [vendors, setVendors] = React.useState<Vendor[]>([])
@ -21,6 +23,24 @@ export default function VendorsPage() {
const [dialogOpen, setDialogOpen] = React.useState(false)
const [editing, setEditing] = React.useState<Vendor | null>(null)
const openCreate = React.useCallback(() => {
setEditing(null)
setDialogOpen(true)
}, [])
const pageActions = React.useMemo(
() => [
{
id: "add-vendor",
label: "Add Vendor",
icon: Plus,
onSelect: openCreate,
},
],
[openCreate]
)
useRegisterPageActions(pageActions)
const load = async () => {
try {
const data = await getVendors()
@ -34,11 +54,6 @@ export default function VendorsPage() {
React.useEffect(() => { load() }, [])
const handleCreate = () => {
setEditing(null)
setDialogOpen(true)
}
const handleEdit = (vendor: Vendor) => {
setEditing(vendor)
setDialogOpen(true)
@ -114,7 +129,7 @@ export default function VendorsPage() {
Manage vendor relationships
</p>
</div>
<Button onClick={handleCreate} className="w-full sm:w-auto">
<Button onClick={openCreate} className="w-full sm:w-auto">
<IconPlus className="mr-2 size-4" />
Add Vendor
</Button>

View File

@ -0,0 +1,50 @@
/**
* Agent Provider
*
* Provides context for controlling the chat panel from anywhere in the app.
*/
"use client"
import * as React from "react"
interface AgentContextValue {
isOpen: boolean
open: () => void
close: () => void
toggle: () => void
}
const AgentContext = React.createContext<AgentContextValue | null>(null)
export function AgentProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = React.useState(false)
const contextValue = React.useMemo(
() => ({
isOpen,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen((prev) => !prev),
}),
[isOpen]
)
return (
<AgentContext.Provider value={contextValue}>
{children}
</AgentContext.Provider>
)
}
export function useAgent() {
const context = React.useContext(AgentContext)
if (!context) {
throw new Error("useAgent must be used within an AgentProvider")
}
return context
}
export function useAgentOptional() {
return React.useContext(AgentContext)
}

View File

@ -0,0 +1,303 @@
"use client"
import { useState, useEffect, useCallback, useRef } from "react"
import { useRouter, usePathname } from "next/navigation"
import { MessageSquare } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Chat } from "@/components/ui/chat"
import { cn } from "@/lib/utils"
import {
useElizaChat,
initializeActionHandlers,
executeAction,
unregisterActionHandler,
ALL_HANDLER_TYPES,
type AgentAction,
} from "@/lib/eliza/chat-adapter"
import { DynamicUI } from "./dynamic-ui"
import { useAgentOptional } from "./agent-provider"
import { toast } from "sonner"
interface ChatPanelProps {
className?: string
}
export function ChatPanel({ className }: ChatPanelProps) {
const agentContext = useAgentOptional()
const isOpen = agentContext?.isOpen ?? false
const setIsOpen = agentContext
? (open: boolean) =>
open ? agentContext.open() : agentContext.close()
: () => {}
const router = useRouter()
const pathname = usePathname()
const routerRef = useRef(router)
routerRef.current = router
const onAction = useCallback((action: AgentAction) => {
executeAction(action)
}, [])
const onError = useCallback((error: Error) => {
toast.error(error.message)
}, [])
useEffect(() => {
initializeActionHandlers(() => routerRef.current)
const handleToast = (event: CustomEvent) => {
const { message, type = "default" } = event.detail ?? {}
if (message) {
if (type === "success") toast.success(message)
else if (type === "error") toast.error(message)
else toast(message)
}
}
window.addEventListener(
"agent-toast",
handleToast as EventListener
)
return () => {
window.removeEventListener(
"agent-toast",
handleToast as EventListener
)
for (const type of ALL_HANDLER_TYPES) {
unregisterActionHandler(type)
}
}
}, [])
const {
messages,
isGenerating,
stop,
append,
setMessages,
} = useElizaChat({
context: { view: pathname },
onAction,
onError,
})
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === ".") {
e.preventDefault()
agentContext?.toggle()
}
if (e.key === "Escape" && isOpen) {
setIsOpen(false)
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [isOpen, setIsOpen, agentContext])
const suggestions = getSuggestionsForPath(pathname)
const chatMessages = messages.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
createdAt: msg.createdAt,
}))
const handleRateResponse = useCallback(
(
messageId: string,
rating: "thumbs-up" | "thumbs-down"
) => {
console.log("Rating:", messageId, rating)
},
[]
)
const [panelWidth, setPanelWidth] = useState(420)
const [isResizing, setIsResizing] = useState(false)
const dragStartX = useRef(0)
const dragStartWidth = useRef(0)
useEffect(() => {
const onMouseMove = (e: MouseEvent) => {
if (!dragStartWidth.current) return
const delta = dragStartX.current - e.clientX
const next = Math.min(720, Math.max(320, dragStartWidth.current + delta))
setPanelWidth(next)
}
const onMouseUp = () => {
if (!dragStartWidth.current) return
dragStartWidth.current = 0
setIsResizing(false)
document.body.style.cursor = ""
document.body.style.userSelect = ""
}
window.addEventListener("mousemove", onMouseMove)
window.addEventListener("mouseup", onMouseUp)
return () => {
window.removeEventListener("mousemove", onMouseMove)
window.removeEventListener("mouseup", onMouseUp)
}
}, [])
const handleResizeStart = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
setIsResizing(true)
dragStartX.current = e.clientX
dragStartWidth.current = panelWidth
document.body.style.cursor = "col-resize"
document.body.style.userSelect = "none"
},
[panelWidth]
)
// Dashboard has its own inline chat — skip the side panel
if (pathname === "/dashboard") return null
return (
<>
{/* Panel — mobile: full-screen overlay, desktop: integrated flex child */}
<div
className={cn(
"flex flex-col bg-background",
"fixed inset-0 z-50",
"md:relative md:inset-auto md:z-auto",
"md:shrink-0 md:overflow-hidden md:border-l md:border-border",
isResizing
? "transition-none"
: "transition-[transform,width,border-color] duration-300 ease-in-out",
isOpen
? "translate-x-0"
: "translate-x-full md:translate-x-0 md:w-0 md:border-l-0",
className
)}
style={isOpen ? { width: panelWidth } : undefined}
>
{/* Desktop resize handle */}
<div
className="absolute left-0 top-0 z-10 hidden h-full w-1 cursor-col-resize md:block hover:bg-border/60 active:bg-border"
onMouseDown={handleResizeStart}
/>
<div className="flex h-full w-full flex-col">
{/* Chat */}
<div className="flex-1 overflow-hidden">
<Chat
messages={chatMessages}
isGenerating={isGenerating}
stop={stop}
append={append}
suggestions={
messages.length === 0 ? suggestions : []
}
onRateResponse={handleRateResponse}
setMessages={setMessages as never}
className="h-full"
/>
</div>
{/* Dynamic UI for agent-generated components */}
{messages.some((m) => m.actions) && (
<div className="max-h-64 overflow-auto border-t p-4">
{messages
.filter((m) => m.actions)
.slice(-1)
.map((m) => {
const uiAction = m.actions?.find(
(a) => a.type === "RENDER_UI"
)
if (!uiAction?.payload?.spec) return null
return (
<DynamicUI
key={m.id}
spec={uiAction.payload.spec as never}
/>
)
})}
</div>
)}
</div>
</div>
{/* Mobile backdrop */}
{isOpen && (
<div
className="fixed inset-0 z-40 bg-black/20 md:hidden"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
)}
{/* Mobile FAB trigger (desktop uses header button) */}
{!isOpen && (
<Button
size="icon"
className="fixed bottom-4 right-4 z-50 h-12 w-12 rounded-full shadow-lg md:hidden"
onClick={() => setIsOpen(true)}
aria-label="Open chat"
>
<MessageSquare className="h-5 w-5" />
</Button>
)}
</>
)
}
function getSuggestionsForPath(pathname: string): string[] {
if (pathname.includes("/customers")) {
return [
"Show me all customers",
"Create a new customer",
"Find customers without email",
]
}
if (pathname.includes("/vendors")) {
return [
"List all vendors",
"Add a new subcontractor",
"Show vendors by category",
]
}
if (pathname.includes("/schedule")) {
return [
"What tasks are on the critical path?",
"Show overdue tasks",
"Add a new task",
]
}
if (pathname.includes("/finances")) {
return [
"Show overdue invoices",
"What payments are pending?",
"Create a new invoice",
]
}
if (pathname.includes("/projects")) {
return [
"List all active projects",
"Create a new project",
"Which projects are behind schedule?",
]
}
if (pathname.includes("/netsuite")) {
return [
"Sync customers from NetSuite",
"Check for sync conflicts",
"When was the last sync?",
]
}
return [
"What can you help me with?",
"Show me today's tasks",
"Navigate to customers",
]
}
export default ChatPanel

View File

@ -0,0 +1,622 @@
/**
* Dynamic UI Renderer
*
* Renders agent-generated UI specs using shadcn/ui components.
* Handles action callbacks for interactive elements.
*/
"use client"
import { useCallback } from "react"
import { executeAction } from "@/lib/eliza/chat-adapter"
import type { ComponentSpec } from "@/lib/eliza/json-render/catalog"
// Import shadcn components
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Progress } from "@/components/ui/progress"
import { cn } from "@/lib/utils"
interface DynamicUIProps {
spec: ComponentSpec
className?: string
}
export function DynamicUI({ spec, className }: DynamicUIProps) {
const handleAction = useCallback(
async (action: { type: string; payload?: Record<string, unknown> }) => {
await executeAction(action)
},
[]
)
return (
<div className={cn("dynamic-ui", className)}>
<ComponentRenderer spec={spec} onAction={handleAction} />
</div>
)
}
interface RendererProps {
spec: ComponentSpec
onAction: (action: { type: string; payload?: Record<string, unknown> }) => void
}
function ComponentRenderer({ spec, onAction }: RendererProps) {
switch (spec.type) {
case "DataTable":
return <DataTableRenderer {...spec.props} onAction={onAction} />
case "Card":
return <CardRenderer {...spec.props} onAction={onAction} />
case "Badge":
return <Badge variant={spec.props.variant}>{spec.props.label}</Badge>
case "StatCard":
return <StatCardRenderer {...spec.props} />
case "Button":
return (
<Button
variant={spec.props.variant}
size={spec.props.size}
onClick={() => onAction(spec.props.action)}
>
{spec.props.label}
</Button>
)
case "ButtonGroup":
return (
<div className="flex gap-2">
{spec.props.buttons.map((btn, i) => (
<Button
key={i}
variant={btn.variant}
size={btn.size}
onClick={() => onAction(btn.action)}
>
{btn.label}
</Button>
))}
</div>
)
case "InvoiceTable":
return <InvoiceTableRenderer {...spec.props} onAction={onAction} />
case "CustomerCard":
return <CustomerCardRenderer {...spec.props} onAction={onAction} />
case "VendorCard":
return <VendorCardRenderer {...spec.props} onAction={onAction} />
case "SchedulePreview":
return <SchedulePreviewRenderer {...spec.props} onAction={onAction} />
case "ProjectSummary":
return <ProjectSummaryRenderer {...spec.props} onAction={onAction} />
case "Grid":
return (
<div
className={cn(
"grid gap-4",
spec.props.columns === 1 && "grid-cols-1",
spec.props.columns === 2 && "grid-cols-2",
spec.props.columns === 3 && "grid-cols-3",
spec.props.columns === 4 && "grid-cols-4"
)}
style={{ gap: spec.props.gap }}
>
{(spec.props.children as ComponentSpec[])?.map((child, i) => (
<ComponentRenderer key={i} spec={child} onAction={onAction} />
))}
</div>
)
case "Stack":
return (
<div
className={cn(
"flex",
spec.props.direction === "horizontal" ? "flex-row" : "flex-col",
"gap-4"
)}
style={{ gap: spec.props.gap }}
>
{(spec.props.children as ComponentSpec[])?.map((child, i) => (
<ComponentRenderer key={i} spec={child} onAction={onAction} />
))}
</div>
)
default:
return (
<div className="text-muted-foreground text-sm">
Unknown component type: {(spec as { type: string }).type}
</div>
)
}
}
// DataTable renderer
function DataTableRenderer({
columns,
data,
onRowClick,
onAction,
}: {
columns: Array<{ key: string; header: string; format?: string }>
data: Array<Record<string, unknown>>
onRowClick?: { type: string; payload?: Record<string, unknown> }
onAction: RendererProps["onAction"]
}) {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead key={col.key}>{col.header}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, i) => (
<TableRow
key={i}
className={onRowClick ? "cursor-pointer hover:bg-muted" : ""}
onClick={() =>
onRowClick &&
onAction({
...onRowClick,
payload: { ...onRowClick.payload, rowData: row },
})
}
>
{columns.map((col) => (
<TableCell key={col.key}>
{formatValue(row[col.key], col.format)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
// Card renderer
function CardRenderer({
title,
description,
children,
footer,
onAction,
}: {
title: string
description?: string
children?: unknown[]
footer?: string
onAction: RendererProps["onAction"]
}) {
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
{children && children.length > 0 && (
<CardContent>
{(children as ComponentSpec[]).map((child, i) => (
<ComponentRenderer key={i} spec={child} onAction={onAction} />
))}
</CardContent>
)}
{footer && (
<CardFooter>
<p className="text-sm text-muted-foreground">{footer}</p>
</CardFooter>
)}
</Card>
)
}
// StatCard renderer
function StatCardRenderer({
title,
value,
change,
changeLabel,
}: {
title: string
value: string | number
change?: number
changeLabel?: string
}) {
return (
<Card>
<CardHeader className="pb-2">
<CardDescription>{title}</CardDescription>
<CardTitle className="text-2xl">{value}</CardTitle>
</CardHeader>
{change !== undefined && (
<CardContent>
<div className="text-xs text-muted-foreground">
<span className={change >= 0 ? "text-green-600" : "text-red-600"}>
{change >= 0 ? "+" : ""}
{change}%
</span>
{changeLabel && ` ${changeLabel}`}
</div>
</CardContent>
)}
</Card>
)
}
// Invoice table renderer
function InvoiceTableRenderer({
invoices,
onRowClick,
onAction,
}: {
invoices: Array<{
id: string
number: string
customer: string
amount: number
dueDate: string
status: string
}>
onRowClick?: { type: string; payload?: Record<string, unknown> }
onAction: RendererProps["onAction"]
}) {
const statusVariant = (status: string) => {
switch (status) {
case "paid":
return "default"
case "overdue":
return "destructive"
default:
return "secondary"
}
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice #</TableHead>
<TableHead>Customer</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow
key={invoice.id}
className={onRowClick ? "cursor-pointer hover:bg-muted" : ""}
onClick={() =>
onRowClick &&
onAction({
...onRowClick,
payload: { ...onRowClick.payload, invoiceId: invoice.id },
})
}
>
<TableCell className="font-medium">{invoice.number}</TableCell>
<TableCell>{invoice.customer}</TableCell>
<TableCell className="text-right">
{formatCurrency(invoice.amount)}
</TableCell>
<TableCell>{formatDate(invoice.dueDate)}</TableCell>
<TableCell>
<Badge variant={statusVariant(invoice.status)}>
{invoice.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
// Customer card renderer
function CustomerCardRenderer({
customer,
actions,
onAction,
}: {
customer: {
id: string
name: string
company?: string
email?: string
phone?: string
}
actions?: Array<{ type: string; payload?: Record<string, unknown> }>
onAction: RendererProps["onAction"]
}) {
return (
<Card>
<CardHeader>
<CardTitle>{customer.name}</CardTitle>
{customer.company && (
<CardDescription>{customer.company}</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-1 text-sm">
{customer.email && <p>Email: {customer.email}</p>}
{customer.phone && <p>Phone: {customer.phone}</p>}
</CardContent>
{actions && actions.length > 0 && (
<CardFooter className="gap-2">
{actions.map((action, i) => (
<Button
key={i}
variant="outline"
size="sm"
onClick={() => onAction(action)}
>
{action.type.replace(/_/g, " ")}
</Button>
))}
</CardFooter>
)}
</Card>
)
}
// Vendor card renderer
function VendorCardRenderer({
vendor,
actions,
onAction,
}: {
vendor: {
id: string
name: string
category: string
email?: string
phone?: string
}
actions?: Array<{ type: string; payload?: Record<string, unknown> }>
onAction: RendererProps["onAction"]
}) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{vendor.name}</CardTitle>
<Badge variant="outline">{vendor.category}</Badge>
</div>
</CardHeader>
<CardContent className="space-y-1 text-sm">
{vendor.email && <p>Email: {vendor.email}</p>}
{vendor.phone && <p>Phone: {vendor.phone}</p>}
</CardContent>
{actions && actions.length > 0 && (
<CardFooter className="gap-2">
{actions.map((action, i) => (
<Button
key={i}
variant="outline"
size="sm"
onClick={() => onAction(action)}
>
{action.type.replace(/_/g, " ")}
</Button>
))}
</CardFooter>
)}
</Card>
)
}
// Schedule preview renderer
function SchedulePreviewRenderer({
projectName,
tasks,
onTaskClick,
onAction,
}: {
projectId: string
projectName: string
tasks: Array<{
id: string
title: string
startDate: string
endDate: string
phase: string
status: string
percentComplete: number
isCriticalPath?: boolean
}>
onTaskClick?: { type: string; payload?: Record<string, unknown> }
onAction: RendererProps["onAction"]
}) {
return (
<Card>
<CardHeader>
<CardTitle>{projectName} Schedule</CardTitle>
<CardDescription>{tasks.length} tasks</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{tasks.slice(0, 5).map((task) => (
<div
key={task.id}
className={cn(
"p-2 rounded border",
task.isCriticalPath && "border-red-500",
onTaskClick && "cursor-pointer hover:bg-muted"
)}
onClick={() =>
onTaskClick &&
onAction({
...onTaskClick,
payload: { ...onTaskClick.payload, taskId: task.id },
})
}
>
<div className="flex items-center justify-between">
<span className="font-medium">{task.title}</span>
<Badge variant="outline">{task.phase}</Badge>
</div>
<div className="mt-2">
<Progress value={task.percentComplete} className="h-1" />
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>{task.percentComplete}% complete</span>
<span>
{formatDate(task.startDate)} - {formatDate(task.endDate)}
</span>
</div>
</div>
</div>
))}
{tasks.length > 5 && (
<p className="text-sm text-muted-foreground text-center">
+{tasks.length - 5} more tasks
</p>
)}
</CardContent>
</Card>
)
}
// Project summary renderer
function ProjectSummaryRenderer({
project,
stats,
actions,
onAction,
}: {
project: {
id: string
name: string
status: string
address?: string
clientName?: string
projectManager?: string
}
stats?: {
tasksTotal: number
tasksComplete: number
daysRemaining?: number
budgetUsed?: number
}
actions?: Array<{ type: string; payload?: Record<string, unknown> }>
onAction: RendererProps["onAction"]
}) {
const completion = stats
? Math.round((stats.tasksComplete / stats.tasksTotal) * 100)
: 0
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{project.name}</CardTitle>
<Badge>{project.status}</Badge>
</div>
{project.address && (
<CardDescription>{project.address}</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-3">
{project.clientName && (
<p className="text-sm">Client: {project.clientName}</p>
)}
{project.projectManager && (
<p className="text-sm">PM: {project.projectManager}</p>
)}
{stats && (
<div>
<div className="flex justify-between text-sm mb-1">
<span>Progress</span>
<span>
{stats.tasksComplete}/{stats.tasksTotal} tasks ({completion}%)
</span>
</div>
<Progress value={completion} />
</div>
)}
</CardContent>
{actions && actions.length > 0 && (
<CardFooter className="gap-2">
{actions.map((action, i) => (
<Button
key={i}
variant="outline"
size="sm"
onClick={() => onAction(action)}
>
{action.type.replace(/_/g, " ")}
</Button>
))}
</CardFooter>
)}
</Card>
)
}
// Utility functions
function formatValue(value: unknown, format?: string): React.ReactNode {
if (value === null || value === undefined) return "-"
switch (format) {
case "currency":
return formatCurrency(Number(value))
case "date":
return formatDate(String(value))
case "badge":
return <Badge variant="outline">{String(value)}</Badge>
default:
return String(value)
}
}
function formatCurrency(amount: number): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
}
function formatDate(dateStr: string): string {
try {
return new Date(dateStr).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})
} catch {
return dateStr
}
}
export default DynamicUI

1297
src/components/ai/prompt-input.tsx Executable file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import {
IconFiles,
IconFolder,
IconHelp,
IconMessageCircle,
IconReceipt,
IconSearch,
IconSettings,
@ -23,6 +24,7 @@ import { NavProjects } from "@/components/nav-projects"
import { NavUser } from "@/components/nav-user"
import { useCommandMenu } from "@/components/command-menu-provider"
import { useSettings } from "@/components/settings-provider"
import { useAgentOptional } from "@/components/agent/agent-provider"
import type { SidebarUser } from "@/lib/auth"
import {
Sidebar,
@ -101,6 +103,7 @@ function SidebarNav({
const { state, setOpen } = useSidebar()
const { open: openSearch } = useCommandMenu()
const { open: openSettings } = useSettings()
const agent = useAgentOptional()
const isExpanded = state === "expanded"
const isFilesMode = pathname?.startsWith("/dashboard/files")
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
@ -128,6 +131,7 @@ function SidebarNav({
? { ...item, onClick: openSettings }
: item
),
...(agent ? [{ title: "Assistant", icon: IconMessageCircle, onClick: agent.open }] : []),
{ title: "Search", icon: IconSearch, onClick: openSearch },
]

View File

@ -5,9 +5,15 @@ import { CommandMenu } from "@/components/command-menu"
import { MobileSearch } from "@/components/mobile-search"
import { useIsMobile } from "@/hooks/use-mobile"
const CommandMenuContext = React.createContext<{
open: () => void
}>({ open: () => {} })
interface CommandMenuContextValue {
readonly open: () => void
readonly openWithQuery: (query: string) => void
}
const CommandMenuContext = React.createContext<CommandMenuContextValue>({
open: () => {},
openWithQuery: () => {},
})
export function useCommandMenu() {
return React.useContext(CommandMenuContext)
@ -22,6 +28,12 @@ export function CommandMenuProvider({
const [isOpen, setIsOpen] = React.useState(false)
const [mobileSearchOpen, setMobileSearchOpen] =
React.useState(false)
const [initialQuery, setInitialQuery] = React.useState("")
const handleSetOpen = React.useCallback((next: boolean) => {
setIsOpen(next)
if (!next) setInitialQuery("")
}, [])
const value = React.useMemo(
() => ({
@ -29,6 +41,15 @@ export function CommandMenuProvider({
if (isMobile) {
setMobileSearchOpen(true)
} else {
setInitialQuery("")
setIsOpen(true)
}
},
openWithQuery: (query: string) => {
if (isMobile) {
setMobileSearchOpen(true)
} else {
setInitialQuery(query)
setIsOpen(true)
}
},
@ -39,7 +60,11 @@ export function CommandMenuProvider({
return (
<CommandMenuContext.Provider value={value}>
{children}
<CommandMenu open={isOpen} setOpen={setIsOpen} />
<CommandMenu
open={isOpen}
setOpen={handleSetOpen}
initialQuery={initialQuery}
/>
<MobileSearch
open={mobileSearchOpen}
setOpen={setMobileSearchOpen}

View File

@ -8,9 +8,11 @@ import {
IconFolder,
IconFiles,
IconCalendarStats,
IconMessageCircle,
IconSun,
IconSearch,
} from "@tabler/icons-react"
import { useAgentOptional } from "@/components/agent/agent-provider"
import {
CommandDialog,
@ -24,12 +26,22 @@ import {
export function CommandMenu({
open,
setOpen,
initialQuery = "",
}: {
open: boolean
setOpen: (open: boolean) => void
readonly open: boolean
readonly setOpen: (open: boolean) => void
readonly initialQuery?: string
}) {
const router = useRouter()
const { theme, setTheme } = useTheme()
const agent = useAgentOptional()
const [query, setQuery] = React.useState("")
React.useEffect(() => {
if (open) {
setQuery(initialQuery)
}
}, [open, initialQuery])
React.useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
@ -49,7 +61,11 @@ export function CommandMenu({
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandInput
placeholder="Type a command or search..."
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Navigation">
@ -71,6 +87,12 @@ export function CommandMenu({
</CommandItem>
</CommandGroup>
<CommandGroup heading="Actions">
{agent && (
<CommandItem onSelect={() => runCommand(() => agent.open())}>
<IconMessageCircle />
Ask Assistant
</CommandItem>
)}
<CommandItem onSelect={() => runCommand(() => setTheme(theme === "dark" ? "light" : "dark"))}>
<IconSun />
Toggle theme

570
src/components/dashboard-chat.tsx Executable file
View File

@ -0,0 +1,570 @@
"use client"
import {
useState,
useCallback,
useRef,
useEffect,
} from "react"
import { usePathname } from "next/navigation"
import {
ArrowUp,
Plus,
SendHorizonal,
Square,
Copy,
ThumbsUp,
ThumbsDown,
RefreshCw,
Check,
} from "lucide-react"
import { toast } from "sonner"
import { cn } from "@/lib/utils"
import { MarkdownRenderer } from "@/components/ui/markdown-renderer"
import { TypingIndicator } from "@/components/ui/typing-indicator"
import { PromptSuggestions } from "@/components/ui/prompt-suggestions"
import {
useAutosizeTextArea,
} from "@/hooks/use-autosize-textarea"
import {
useElizaChat,
executeAction,
type AgentAction,
} from "@/lib/eliza/chat-adapter"
import {
IconBrandGithub,
IconExternalLink,
IconGitFork,
IconStar,
IconAlertCircle,
IconEye,
} from "@tabler/icons-react"
type RepoStats = {
readonly stargazers_count: number
readonly forks_count: number
readonly open_issues_count: number
readonly subscribers_count: number
}
const REPO = "High-Performance-Structures/compass"
const GITHUB_URL = `https://github.com/${REPO}`
interface DashboardChatProps {
readonly stats: RepoStats | null
}
const SUGGESTIONS = [
"What can you help me with?",
"Show me today's tasks",
"Navigate to customers",
]
const ANIMATED_PLACEHOLDERS = [
"Show me invoices from the Johnson project",
"What tasks are due this week?",
"Which vendors need payment?",
"Navigate to the schedule view",
"Find overdue invoices for Highland",
"Who is assigned to concrete pour?",
]
const LOGO_MASK = {
maskImage: "url(/logo-black.png)",
maskSize: "contain",
maskRepeat: "no-repeat",
WebkitMaskImage: "url(/logo-black.png)",
WebkitMaskSize: "contain",
WebkitMaskRepeat: "no-repeat",
} as React.CSSProperties
export function DashboardChat({ stats }: DashboardChatProps) {
const [isActive, setIsActive] = useState(false)
const [idleInput, setIdleInput] = useState("")
const scrollRef = useRef<HTMLDivElement>(null)
const pathname = usePathname()
const [chatInput, setChatInput] = useState("")
const chatTextareaRef = useRef<HTMLTextAreaElement>(null)
useAutosizeTextArea({
ref: chatTextareaRef,
maxHeight: 200,
borderWidth: 0,
dependencies: [chatInput],
})
const onAction = useCallback((action: AgentAction) => {
executeAction(action)
}, [])
const onError = useCallback((error: Error) => {
toast.error(error.message)
}, [])
const {
messages,
isGenerating,
stop,
append,
} = useElizaChat({
context: { view: pathname },
onAction,
onError,
})
const [copiedId, setCopiedId] = useState<string | null>(
null
)
const [animatedPlaceholder, setAnimatedPlaceholder] =
useState("")
const [animFading, setAnimFading] = useState(false)
const [isIdleFocused, setIsIdleFocused] = useState(false)
const animTimerRef =
useRef<ReturnType<typeof setTimeout>>(undefined)
// typewriter animation for idle input placeholder
useEffect(() => {
if (isIdleFocused || idleInput || isActive) {
setAnimatedPlaceholder("")
setAnimFading(false)
return
}
let msgIdx = 0
let charIdx = 0
let phase: "typing" | "pause" | "fading" = "typing"
const tick = () => {
const msg = ANIMATED_PLACEHOLDERS[msgIdx]
if (phase === "typing") {
charIdx++
setAnimatedPlaceholder(msg.slice(0, charIdx))
if (charIdx >= msg.length) {
phase = "pause"
animTimerRef.current = setTimeout(tick, 2500)
} else {
animTimerRef.current = setTimeout(
tick,
25 + Math.random() * 20
)
}
} else if (phase === "pause") {
phase = "fading"
setAnimFading(true)
animTimerRef.current = setTimeout(tick, 400)
} else {
// faded out — swap to next message while invisible
msgIdx =
(msgIdx + 1) % ANIMATED_PLACEHOLDERS.length
charIdx = 1
setAnimatedPlaceholder(
ANIMATED_PLACEHOLDERS[msgIdx].slice(0, 1)
)
setAnimFading(false)
phase = "typing"
animTimerRef.current = setTimeout(tick, 50)
}
}
animTimerRef.current = setTimeout(tick, 600)
return () => {
if (animTimerRef.current)
clearTimeout(animTimerRef.current)
}
}, [isIdleFocused, idleInput, isActive])
// auto-scroll on new messages
useEffect(() => {
if (!isActive) return
const el = scrollRef.current
if (!el) return
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" })
}, [messages.length, isActive])
// Escape to return to idle when no messages
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (
e.key === "Escape" &&
isActive &&
messages.length === 0
) {
setIsActive(false)
}
}
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [isActive, messages.length])
useEffect(() => {
if (!isActive) return
const timer = setTimeout(() => {
chatTextareaRef.current?.focus()
}, 300)
return () => clearTimeout(timer)
}, [isActive])
const handleIdleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault()
const value = idleInput.trim()
setIsActive(true)
if (value) {
append({ role: "user", content: value })
setIdleInput("")
}
},
[idleInput, append]
)
const handleCopy = useCallback(
(id: string, content: string) => {
navigator.clipboard.writeText(content)
setCopiedId(id)
setTimeout(() => setCopiedId(null), 2000)
},
[]
)
const handleSuggestion = useCallback(
(message: { role: "user"; content: string }) => {
setIsActive(true)
append(message)
},
[append]
)
return (
<div className="flex flex-1 flex-col min-h-0">
{/* Compact hero - active only */}
<div
className={cn(
"shrink-0 text-center transition-all duration-500 ease-in-out overflow-hidden",
isActive
? "py-3 sm:py-4 opacity-100 max-h-40"
: "py-0 opacity-0 max-h-0"
)}
>
<span
className="mx-auto mb-2 block bg-foreground size-7"
style={LOGO_MASK}
/>
<h1 className="text-base sm:text-lg font-bold tracking-tight">
Compass
</h1>
</div>
{/* Middle content area */}
<div className="flex flex-1 flex-col min-h-0 relative">
{/* Idle: hero + input + stats, all centered */}
<div
className={cn(
"absolute inset-0 flex flex-col items-center justify-center",
"transition-all duration-500 ease-in-out",
isActive
? "opacity-0 translate-y-4 pointer-events-none"
: "opacity-100 translate-y-0"
)}
>
<div className="w-full max-w-2xl px-5 space-y-5 text-center">
<div>
<span
className="mx-auto mb-2 block bg-foreground size-10"
style={LOGO_MASK}
/>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight">
Compass
</h1>
<p className="text-muted-foreground/60 mt-1.5 text-xs px-2">
Development preview features may be
incomplete or change without notice.
</p>
</div>
<form onSubmit={handleIdleSubmit}>
<label className="group flex w-full items-center gap-2 rounded-full border bg-background px-5 py-3 text-sm shadow-sm transition-colors hover:border-primary/30 hover:bg-muted/30 cursor-text">
<input
value={idleInput}
onChange={(e) =>
setIdleInput(e.target.value)
}
onFocus={() => setIsIdleFocused(true)}
onBlur={() => setIsIdleFocused(false)}
placeholder={
animatedPlaceholder ||
"Ask anything..."
}
className={cn(
"flex-1 bg-transparent text-foreground outline-none",
"placeholder:text-muted-foreground placeholder:transition-opacity placeholder:duration-300",
animFading
? "placeholder:opacity-0"
: "placeholder:opacity-100"
)}
/>
<button
type="submit"
className="shrink-0"
aria-label="Send"
>
<SendHorizonal className="size-4 text-muted-foreground/60 transition-colors group-hover:text-primary" />
</button>
</label>
</form>
{stats && (
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs text-muted-foreground/70">
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 transition-colors hover:text-foreground"
>
<IconBrandGithub className="size-4" />
<span>View on GitHub</span>
<IconExternalLink className="size-3" />
</a>
<span className="hidden sm:inline text-border">
|
</span>
<span className="text-xs">
{REPO}
</span>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<IconStar className="size-3.5" />
{stats.stargazers_count}
</span>
<span className="flex items-center gap-1">
<IconGitFork className="size-3.5" />
{stats.forks_count}
</span>
<span className="flex items-center gap-1">
<IconAlertCircle className="size-3.5" />
{stats.open_issues_count}
</span>
<span className="flex items-center gap-1">
<IconEye className="size-3.5" />
{stats.subscribers_count}
</span>
</div>
</div>
)}
</div>
</div>
{/* Active: messages or suggestions */}
<div
className={cn(
"absolute inset-0 flex flex-col",
"transition-all duration-500 ease-in-out delay-100",
isActive
? "opacity-100 translate-y-0"
: "opacity-0 -translate-y-4 pointer-events-none"
)}
>
{messages.length > 0 ? (
<div
ref={scrollRef}
className="flex-1 overflow-y-auto"
>
<div className="mx-auto w-full max-w-3xl px-4 py-4 space-y-6">
{messages.map((msg) => {
if (msg.role === "user") {
return (
<div
key={msg.id}
className="flex justify-end"
>
<div className="rounded-2xl border bg-background px-4 py-2.5 text-sm max-w-[80%] shadow-sm">
{msg.content}
</div>
</div>
)
}
return (
<div
key={msg.id}
className="flex flex-col items-start"
>
{msg.content ? (
<>
<div className="w-full text-sm leading-relaxed prose prose-sm prose-neutral dark:prose-invert max-w-none">
<MarkdownRenderer>
{msg.content}
</MarkdownRenderer>
</div>
<div className="mt-2 flex items-center gap-1">
<button
type="button"
onClick={() =>
handleCopy(
msg.id,
msg.content
)
}
className="rounded-md p-1.5 text-muted-foreground/60 transition-colors hover:bg-muted hover:text-foreground"
aria-label="Copy"
>
{copiedId === msg.id ? (
<Check className="size-3.5" />
) : (
<Copy className="size-3.5" />
)}
</button>
<button
type="button"
className="rounded-md p-1.5 text-muted-foreground/60 transition-colors hover:bg-muted hover:text-foreground"
aria-label="Good response"
>
<ThumbsUp className="size-3.5" />
</button>
<button
type="button"
className="rounded-md p-1.5 text-muted-foreground/60 transition-colors hover:bg-muted hover:text-foreground"
aria-label="Bad response"
>
<ThumbsDown className="size-3.5" />
</button>
<button
type="button"
onClick={() =>
append({
role: "user",
content:
messages.findLast(
(m) =>
m.role === "user"
)?.content ?? "",
})
}
className="rounded-md p-1.5 text-muted-foreground/60 transition-colors hover:bg-muted hover:text-foreground"
aria-label="Regenerate"
>
<RefreshCw className="size-3.5" />
</button>
</div>
</>
) : (
<TypingIndicator />
)}
</div>
)
})}
</div>
</div>
) : (
<div className="flex-1 flex items-end">
<div className="mx-auto w-full max-w-2xl">
<PromptSuggestions
label="Try these prompts"
append={handleSuggestion}
suggestions={SUGGESTIONS}
/>
</div>
</div>
)}
</div>
</div>
{/* Bottom input - active only */}
<div
className={cn(
"shrink-0 px-4 transition-all duration-500 ease-in-out",
isActive
? "opacity-100 translate-y-0 pt-2 pb-6"
: "opacity-0 translate-y-4 max-h-0 overflow-hidden pointer-events-none py-0"
)}
>
<form
className="mx-auto max-w-3xl"
onSubmit={(e) => {
e.preventDefault()
const trimmed = chatInput.trim()
if (!trimmed || isGenerating) return
append({ role: "user", content: trimmed })
setChatInput("")
}}
>
<div
className={cn(
"flex flex-col rounded-2xl border bg-background overflow-hidden",
"transition-[border-color,box-shadow] duration-200",
"focus-within:border-ring/40 focus-within:shadow-[0_0_0_3px_rgba(0,0,0,0.04)]",
)}
>
<textarea
ref={chatTextareaRef}
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
const trimmed = chatInput.trim()
if (!trimmed || isGenerating) return
append({
role: "user",
content: trimmed,
})
setChatInput("")
}
}}
placeholder="Ask follow-up..."
rows={1}
className={cn(
"w-full resize-none bg-transparent text-sm outline-none",
"overflow-y-auto px-5 pt-4 pb-2",
"placeholder:text-muted-foreground/60",
)}
/>
<div className="flex items-center justify-between px-3 pb-3">
<div className="flex items-center gap-1">
<button
type="button"
className={cn(
"flex size-8 items-center justify-center rounded-lg",
"text-muted-foreground/60 transition-colors",
"hover:bg-muted hover:text-foreground",
)}
aria-label="Add attachment"
>
<Plus className="size-4" />
</button>
</div>
{isGenerating ? (
<button
type="button"
onClick={stop}
className={cn(
"flex size-9 items-center justify-center rounded-full",
"bg-foreground text-background",
"transition-colors hover:bg-foreground/90",
)}
aria-label="Stop generating"
>
<Square className="size-4" />
</button>
) : (
<button
type="submit"
disabled={!chatInput.trim()}
className={cn(
"flex size-9 items-center justify-center rounded-full",
"transition-all duration-200",
chatInput.trim()
? "bg-foreground text-background hover:bg-foreground/90"
: "bg-muted/60 text-muted-foreground/40",
)}
aria-label="Send message"
>
<ArrowUp className="size-4" />
</button>
)}
</div>
</div>
</form>
</div>
</div>
)
}

View File

@ -0,0 +1,180 @@
"use client"
import * as React from "react"
import { usePathname, useRouter } from "next/navigation"
import { useTheme } from "next-themes"
import { toast } from "sonner"
import {
ArrowLeft,
ArrowRight,
Copy,
Search,
Sun,
Moon,
MessageCircle,
LayoutDashboard,
FolderKanban,
Users,
FolderOpen,
UserRound,
Building2,
DollarSign,
} from "lucide-react"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { usePageActionsContext } from "@/components/page-actions-provider"
import { useCommandMenu } from "@/components/command-menu-provider"
import { useFeedback } from "@/components/feedback-widget"
const NAV_ITEMS = [
{
label: "Dashboard",
href: "/dashboard",
icon: LayoutDashboard,
},
{
label: "Projects",
href: "/dashboard/projects",
icon: FolderKanban,
},
{
label: "People",
href: "/dashboard/people",
icon: Users,
},
{
label: "Files",
href: "/dashboard/files",
icon: FolderOpen,
},
{
label: "Customers",
href: "/dashboard/customers",
icon: UserRound,
},
{
label: "Vendors",
href: "/dashboard/vendors",
icon: Building2,
},
{
label: "Financials",
href: "/dashboard/financials",
icon: DollarSign,
},
] as const
export function DashboardContextMenu({
children,
}: {
readonly children: React.ReactNode
}) {
const pathname = usePathname()
const router = useRouter()
const { theme, setTheme } = useTheme()
const { actions: pageActions } = usePageActionsContext()
const { open: openCommandMenu } = useCommandMenu()
const { open: openFeedback } = useFeedback()
const handleCopyUrl = () => {
navigator.clipboard.writeText(window.location.href).then(
() => toast.success("URL copied to clipboard"),
() => toast.error("Failed to copy URL")
)
}
const isCurrentRoute = (href: string): boolean => {
if (href === "/dashboard") return pathname === "/dashboard"
return pathname.startsWith(href)
}
return (
<ContextMenu>
<ContextMenuTrigger asChild>
{children}
</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<ContextMenuItem onSelect={() => window.history.back()}>
<ArrowLeft />
Back
</ContextMenuItem>
<ContextMenuItem onSelect={() => window.history.forward()}>
<ArrowRight />
Forward
</ContextMenuItem>
{pageActions.length > 0 && (
<>
<ContextMenuSeparator />
<ContextMenuLabel>Page Actions</ContextMenuLabel>
{pageActions.map((action) => (
<ContextMenuItem
key={action.id}
onSelect={action.onSelect}
>
{action.icon &&
React.createElement(action.icon)}
{action.label}
</ContextMenuItem>
))}
</>
)}
<ContextMenuSeparator />
<ContextMenuItem onSelect={handleCopyUrl}>
<Copy />
Copy Page URL
</ContextMenuItem>
<ContextMenuItem onSelect={openCommandMenu}>
<Search />
Command Menu
<ContextMenuShortcut>
{"\u2318"}K
</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem
onSelect={() =>
setTheme(theme === "dark" ? "light" : "dark")
}
>
{theme === "dark" ? <Sun /> : <Moon />}
Toggle Theme
</ContextMenuItem>
<ContextMenuItem onSelect={openFeedback}>
<MessageCircle />
Send Feedback
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
Navigate to...
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
{NAV_ITEMS.map((item) => (
<ContextMenuItem
key={item.href}
disabled={isCurrentRoute(item.href)}
onSelect={() => router.push(item.href)}
>
<item.icon />
{item.label}
</ContextMenuItem>
))}
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuContent>
</ContextMenu>
)
}

View File

@ -2,9 +2,11 @@
import { createContext, useContext, useState } from "react"
import { usePathname } from "next/navigation"
import { useAgentOptional } from "@/components/agent/agent-provider"
import { MessageCircle } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
@ -50,6 +52,8 @@ export function FeedbackWidget({ children }: { children?: React.ReactNode }) {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const pathname = usePathname()
const agentContext = useAgentOptional()
const chatOpen = agentContext?.isOpen ?? false
function resetForm() {
setType("")
@ -101,7 +105,10 @@ export function FeedbackWidget({ children }: { children?: React.ReactNode }) {
<Button
onClick={() => setDialogOpen(true)}
size="icon-lg"
className="group fixed bottom-12 right-6 z-40 gap-0 rounded-full shadow-lg transition-all duration-200 hover:w-auto hover:gap-2 hover:px-4 overflow-hidden hidden md:flex"
className={cn(
"group fixed bottom-12 right-6 z-40 gap-0 rounded-full shadow-lg transition-all duration-200 hover:w-auto hover:gap-2 hover:px-4 overflow-hidden hidden md:flex",
chatOpen && "md:translate-x-20 md:opacity-0 md:pointer-events-none"
)}
>
<MessageCircle className="size-5 shrink-0" />
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 transition-all duration-200 group-hover:max-w-40 group-hover:opacity-100">

View File

@ -0,0 +1,56 @@
"use client"
import * as React from "react"
import type { LucideIcon } from "lucide-react"
interface PageAction {
readonly id: string
readonly label: string
readonly icon?: LucideIcon
readonly onSelect: () => void
}
interface PageActionsContextValue {
readonly actions: ReadonlyArray<PageAction>
readonly register: (
actions: ReadonlyArray<PageAction>
) => () => void
}
const PageActionsContext = React.createContext<PageActionsContextValue>({
actions: [],
register: () => () => {},
})
export function usePageActionsContext(): PageActionsContextValue {
return React.useContext(PageActionsContext)
}
export function PageActionsProvider({
children,
}: {
readonly children: React.ReactNode
}) {
const [actions, setActions] = React.useState<
ReadonlyArray<PageAction>
>([])
const register = React.useCallback(
(incoming: ReadonlyArray<PageAction>) => {
setActions(incoming)
return () => setActions([])
},
[]
)
const value = React.useMemo(
() => ({ actions, register }),
[actions, register]
)
return (
<PageActionsContext.Provider value={value}>
{children}
</PageActionsContext.Provider>
)
}

View File

@ -7,6 +7,7 @@ import {
IconMenu2,
IconMoon,
IconSearch,
IconSparkles,
IconSun,
IconUserCircle,
} from "@tabler/icons-react"
@ -27,6 +28,7 @@ import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar"
import { NotificationsPopover } from "@/components/notifications-popover"
import { useCommandMenu } from "@/components/command-menu-provider"
import { useFeedback } from "@/components/feedback-widget"
import { useAgentOptional } from "@/components/agent/agent-provider"
import { AccountModal } from "@/components/account-modal"
import { getInitials } from "@/lib/utils"
import type { SidebarUser } from "@/lib/auth"
@ -37,8 +39,11 @@ export function SiteHeader({
readonly user: SidebarUser | null
}) {
const { theme, setTheme } = useTheme()
const { open: openCommand } = useCommandMenu()
const { open: openCommand, openWithQuery } = useCommandMenu()
const [headerQuery, setHeaderQuery] = React.useState("")
const searchInputRef = React.useRef<HTMLInputElement>(null)
const { open: openFeedback } = useFeedback()
const agentContext = useAgentOptional()
const [accountOpen, setAccountOpen] = React.useState(false)
const { toggleSidebar } = useSidebar()
@ -49,7 +54,7 @@ export function SiteHeader({
}
return (
<header className="sticky top-0 z-40 flex shrink-0 items-center bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<header className="sticky top-0 z-40 flex shrink-0 items-center border-b border-border/40 bg-background/80 backdrop-blur-sm">
{/* mobile header: single unified pill */}
<div className="flex h-14 w-full items-center px-3 md:hidden">
<div
@ -112,35 +117,47 @@ export function SiteHeader({
</div>
</div>
{/* desktop header: traditional layout */}
<div className="hidden h-14 w-full items-center gap-2 border-b px-4 md:flex">
<SidebarTrigger className="-ml-1" />
<div
className="relative mx-auto w-full max-w-md cursor-pointer"
onClick={openCommand}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openCommand()
}}
>
<IconSearch className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<div className="bg-muted/50 border-input flex h-9 w-full items-center rounded-md border pl-9 pr-3 text-sm">
<span className="text-muted-foreground flex-1">
Search...
</span>
<kbd className="bg-muted text-muted-foreground pointer-events-none ml-2 hidden sm:inline-flex h-5 items-center gap-0.5 rounded border px-1.5 font-mono text-xs">
<span className="text-xs">&#x2318;</span>K
</kbd>
</div>
{/* desktop header: three-column grid for true center search */}
<div className="hidden h-12 w-full grid-cols-[1fr_minmax(0,28rem)_1fr] items-center px-4 md:grid">
<div className="flex items-center">
<SidebarTrigger className="-ml-1" />
</div>
<div className="flex shrink-0 items-center gap-1">
<div className="relative justify-self-center w-full">
<IconSearch className="text-muted-foreground/60 absolute top-1/2 left-3 size-4 -translate-y-1/2 pointer-events-none" />
<input
ref={searchInputRef}
type="text"
value={headerQuery}
onChange={(e) => setHeaderQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
const q = headerQuery.trim()
if (q) {
openWithQuery(q)
} else {
openCommand()
}
setHeaderQuery("")
searchInputRef.current?.blur()
}
}}
placeholder="Search..."
className="flex h-8 w-full items-center rounded-full border border-border/50 bg-muted/30 pl-9 pr-16 text-sm outline-none transition-colors placeholder:text-muted-foreground/60 hover:bg-muted/50 focus:bg-muted/50 focus:border-border"
/>
<kbd
className="text-muted-foreground/50 pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 hidden sm:inline-flex h-5 items-center gap-0.5 rounded-md border border-border/40 bg-background/50 px-1.5 font-mono text-[10px]"
>
<span>&#x2318;</span>K
</kbd>
</div>
<div className="flex shrink-0 items-center justify-end gap-0.5">
<Button
variant="ghost"
size="sm"
className="text-muted-foreground text-xs"
className="text-muted-foreground/70 hover:text-foreground text-xs h-7 px-2"
onClick={openFeedback}
>
Feedback
@ -149,18 +166,27 @@ export function SiteHeader({
<Button
variant="ghost"
size="icon"
className="size-8"
className="size-7 text-muted-foreground/70 hover:text-foreground"
onClick={() => agentContext?.toggle()}
aria-label="Toggle assistant"
>
<IconSparkles className="size-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="size-7 text-muted-foreground/70 hover:text-foreground"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<IconSun className="size-4 hidden dark:block" />
<IconMoon className="size-4 block dark:hidden" />
<IconSun className="size-3.5 hidden dark:block" />
<IconMoon className="size-3.5 block dark:hidden" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="ml-1 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Avatar className="size-7 grayscale">
<button className="ml-0.5 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Avatar className="size-6 grayscale">
{user?.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
<AvatarFallback className="text-xs">{initials}</AvatarFallback>
<AvatarFallback className="text-[10px]">{initials}</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>

View File

@ -0,0 +1,198 @@
"use client"
import { useEffect, useRef } from "react"
// Configuration constants for the audio analyzer
const AUDIO_CONFIG = {
FFT_SIZE: 512,
SMOOTHING: 0.8,
MIN_BAR_HEIGHT: 2,
MIN_BAR_WIDTH: 2,
BAR_SPACING: 1,
COLOR: {
MIN_INTENSITY: 100, // Minimum gray value (darker)
MAX_INTENSITY: 255, // Maximum gray value (brighter)
INTENSITY_RANGE: 155, // MAX_INTENSITY - MIN_INTENSITY
},
} as const
interface AudioVisualizerProps {
stream: MediaStream | null
isRecording: boolean
onClick: () => void
}
export function AudioVisualizer({
stream,
isRecording,
onClick,
}: AudioVisualizerProps) {
// Refs for managing audio context and animation
const canvasRef = useRef<HTMLCanvasElement>(null)
const audioContextRef = useRef<AudioContext | null>(null)
const analyserRef = useRef<AnalyserNode | null>(null)
const animationFrameRef = useRef<number | undefined>(undefined)
const containerRef = useRef<HTMLDivElement>(null)
// Cleanup function to stop visualization and close audio context
const cleanup = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
if (audioContextRef.current) {
audioContextRef.current.close()
}
}
// Cleanup on unmount
useEffect(() => {
return cleanup
}, [])
// Start or stop visualization based on recording state
useEffect(() => {
if (stream && isRecording) {
startVisualization()
} else {
cleanup()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stream, isRecording])
// Handle window resize
useEffect(() => {
const handleResize = () => {
if (canvasRef.current && containerRef.current) {
const container = containerRef.current
const canvas = canvasRef.current
const dpr = window.devicePixelRatio || 1
// Set canvas size based on container and device pixel ratio
const rect = container.getBoundingClientRect()
// Account for the 2px total margin (1px on each side)
canvas.width = (rect.width - 2) * dpr
canvas.height = (rect.height - 2) * dpr
// Scale canvas CSS size to match container minus margins
canvas.style.width = `${rect.width - 2}px`
canvas.style.height = `${rect.height - 2}px`
}
}
window.addEventListener("resize", handleResize)
// Initial setup
handleResize()
return () => window.removeEventListener("resize", handleResize)
}, [])
// Initialize audio context and start visualization
const startVisualization = async () => {
try {
const audioContext = new AudioContext()
audioContextRef.current = audioContext
const analyser = audioContext.createAnalyser()
analyser.fftSize = AUDIO_CONFIG.FFT_SIZE
analyser.smoothingTimeConstant = AUDIO_CONFIG.SMOOTHING
analyserRef.current = analyser
const source = audioContext.createMediaStreamSource(stream!)
source.connect(analyser)
draw()
} catch (error) {
console.error("Error starting visualization:", error)
}
}
// Calculate the color intensity based on bar height
const getBarColor = (normalizedHeight: number) => {
const intensity =
Math.floor(normalizedHeight * AUDIO_CONFIG.COLOR.INTENSITY_RANGE) +
AUDIO_CONFIG.COLOR.MIN_INTENSITY
return `rgb(${intensity}, ${intensity}, ${intensity})`
}
// Draw a single bar of the visualizer
const drawBar = (
ctx: CanvasRenderingContext2D,
x: number,
centerY: number,
width: number,
height: number,
color: string
) => {
ctx.fillStyle = color
// Draw upper bar (above center)
ctx.fillRect(x, centerY - height, width, height)
// Draw lower bar (below center)
ctx.fillRect(x, centerY, width, height)
}
// Main drawing function
const draw = () => {
if (!isRecording) return
const canvas = canvasRef.current
const ctx = canvas?.getContext("2d")
if (!canvas || !ctx || !analyserRef.current) return
const dpr = window.devicePixelRatio || 1
ctx.scale(dpr, dpr)
const analyser = analyserRef.current
const bufferLength = analyser.frequencyBinCount
const frequencyData = new Uint8Array(bufferLength)
const drawFrame = () => {
animationFrameRef.current = requestAnimationFrame(drawFrame)
// Get current frequency data
analyser.getByteFrequencyData(frequencyData)
// Clear canvas - use CSS pixels for clearing
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr)
// Calculate dimensions in CSS pixels
const barWidth = Math.max(
AUDIO_CONFIG.MIN_BAR_WIDTH,
canvas.width / dpr / bufferLength - AUDIO_CONFIG.BAR_SPACING
)
const centerY = canvas.height / dpr / 2
let x = 0
// Draw each frequency bar
for (let i = 0; i < bufferLength; i++) {
const normalizedHeight = frequencyData[i] / 255 // Convert to 0-1 range
const barHeight = Math.max(
AUDIO_CONFIG.MIN_BAR_HEIGHT,
normalizedHeight * centerY
)
drawBar(
ctx,
x,
centerY,
barWidth,
barHeight,
getBarColor(normalizedHeight)
)
x += barWidth + AUDIO_CONFIG.BAR_SPACING
}
}
drawFrame()
}
return (
<div
ref={containerRef}
className="h-full w-full cursor-pointer rounded-lg bg-background/80 backdrop-blur"
onClick={onClick}
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
)
}

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
@ -22,9 +22,11 @@ const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
@ -46,7 +48,7 @@ function Button({
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot.Root : "button"
return (
<Comp

View File

@ -0,0 +1,405 @@
"use client"
import React, { useMemo, useState } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { motion } from "framer-motion"
import { Ban, ChevronRight, Code2, Loader2, Terminal } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { FilePreview } from "@/components/ui/file-preview"
import { MarkdownRenderer } from "@/components/ui/markdown-renderer"
const chatBubbleVariants = cva(
"group/message relative break-words rounded-lg p-4 text-sm max-w-[85%] sm:max-w-[75%]",
{
variants: {
isUser: {
true: "bg-primary text-primary-foreground",
false: "bg-muted text-foreground",
},
animation: {
none: "",
slide: "duration-300 animate-in fade-in-0",
scale: "duration-300 animate-in fade-in-0 zoom-in-75",
fade: "duration-500 animate-in fade-in-0",
},
},
compoundVariants: [
{
isUser: true,
animation: "slide",
class: "slide-in-from-right",
},
{
isUser: false,
animation: "slide",
class: "slide-in-from-left",
},
{
isUser: true,
animation: "scale",
class: "origin-bottom-right",
},
{
isUser: false,
animation: "scale",
class: "origin-bottom-left",
},
],
}
)
type Animation = VariantProps<typeof chatBubbleVariants>["animation"]
interface Attachment {
name?: string
contentType?: string
url: string
}
interface PartialToolCall {
state: "partial-call"
toolName: string
}
interface ToolCall {
state: "call"
toolName: string
}
interface ToolResult {
state: "result"
toolName: string
result: {
__cancelled?: boolean
[key: string]: unknown
}
}
type ToolInvocation = PartialToolCall | ToolCall | ToolResult
interface ReasoningPart {
type: "reasoning"
reasoning: string
}
interface ToolInvocationPart {
type: "tool-invocation"
toolInvocation: ToolInvocation
}
interface TextPart {
type: "text"
text: string
}
// For compatibility with AI SDK types, not used
interface SourcePart {
type: "source"
source?: unknown
}
interface FilePart {
type: "file"
mimeType: string
data: string
}
interface StepStartPart {
type: "step-start"
}
type MessagePart =
| TextPart
| ReasoningPart
| ToolInvocationPart
| SourcePart
| FilePart
| StepStartPart
export interface Message {
id: string
role: "user" | "assistant" | (string & {})
content: string
createdAt?: Date
experimental_attachments?: Attachment[]
toolInvocations?: ToolInvocation[]
parts?: MessagePart[]
}
export interface ChatMessageProps extends Message {
showTimeStamp?: boolean
animation?: Animation
actions?: React.ReactNode
}
export const ChatMessage: React.FC<ChatMessageProps> = ({
role,
content,
createdAt,
showTimeStamp = false,
animation = "scale",
actions,
experimental_attachments,
toolInvocations,
parts,
}) => {
const files = useMemo(() => {
return experimental_attachments?.map((attachment) => {
const dataArray = dataUrlToUint8Array(attachment.url)
const file = new File([dataArray], attachment.name ?? "Unknown", {
type: attachment.contentType,
})
return file
})
}, [experimental_attachments])
const isUser = role === "user"
const formattedTime = createdAt?.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
})
if (isUser) {
return (
<div
className={cn("flex flex-col", isUser ? "items-end" : "items-start")}
>
{files ? (
<div className="mb-1 flex flex-wrap gap-2">
{files.map((file, index) => {
return <FilePreview file={file} key={index} />
})}
</div>
) : null}
<div className={cn(chatBubbleVariants({ isUser, animation }))}>
<MarkdownRenderer>{content}</MarkdownRenderer>
</div>
{showTimeStamp && createdAt ? (
<time
dateTime={createdAt.toISOString()}
className={cn(
"mt-1 block px-1 text-xs text-muted-foreground",
animation !== "none" && "duration-500 animate-in fade-in-0"
)}
>
{formattedTime}
</time>
) : null}
</div>
)
}
if (parts && parts.length > 0) {
return parts.map((part, index) => {
if (part.type === "text") {
return (
<div
className={cn(
"flex flex-col",
isUser ? "items-end" : "items-start"
)}
key={`text-${index}`}
>
<div className={cn(chatBubbleVariants({ isUser, animation }))}>
<MarkdownRenderer>{part.text}</MarkdownRenderer>
{actions ? (
<div className="absolute -bottom-4 right-2 flex space-x-1 rounded-lg border bg-background p-1 text-foreground opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100">
{actions}
</div>
) : null}
</div>
{showTimeStamp && createdAt ? (
<time
dateTime={createdAt.toISOString()}
className={cn(
"mt-1 block px-1 text-xs text-muted-foreground",
animation !== "none" && "duration-500 animate-in fade-in-0"
)}
>
{formattedTime}
</time>
) : null}
</div>
)
} else if (part.type === "reasoning") {
return <ReasoningBlock key={`reasoning-${index}`} part={part} />
} else if (part.type === "tool-invocation") {
return (
<ToolCall
key={`tool-${index}`}
toolInvocations={[part.toolInvocation]}
/>
)
}
return null
})
}
if (toolInvocations && toolInvocations.length > 0) {
return <ToolCall toolInvocations={toolInvocations} />
}
return (
<div className={cn("flex flex-col", isUser ? "items-end" : "items-start")}>
<div className={cn(chatBubbleVariants({ isUser, animation }))}>
<MarkdownRenderer>{content}</MarkdownRenderer>
{actions ? (
<div className="absolute -bottom-4 right-2 flex space-x-1 rounded-lg border bg-background p-1 text-foreground opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100">
{actions}
</div>
) : null}
</div>
{showTimeStamp && createdAt ? (
<time
dateTime={createdAt.toISOString()}
className={cn(
"mt-1 block px-1 text-xs text-muted-foreground",
animation !== "none" && "duration-500 animate-in fade-in-0"
)}
>
{formattedTime}
</time>
) : null}
</div>
)
}
function dataUrlToUint8Array(data: string) {
const base64 = data.split(",")[1]
const buf = Buffer.from(base64, "base64")
return new Uint8Array(buf)
}
const ReasoningBlock = ({ part }: { part: ReasoningPart }) => {
const [isOpen, setIsOpen] = useState(false)
return (
<div className="mb-2 flex flex-col items-start sm:max-w-[70%]">
<Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="group w-full overflow-hidden rounded-lg border bg-muted"
>
<div className="flex items-center p-2">
<CollapsibleTrigger asChild>
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<ChevronRight className="h-4 w-4 transition-transform group-data-[state=open]:rotate-90" />
<span>Thinking</span>
</button>
</CollapsibleTrigger>
</div>
<CollapsibleContent forceMount>
<motion.div
initial={false}
animate={isOpen ? "open" : "closed"}
variants={{
open: { height: "auto", opacity: 1 },
closed: { height: 0, opacity: 0 },
}}
transition={{ duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] }}
className="border-t"
>
<div className="p-2">
<div className="whitespace-pre-wrap text-xs">
{part.reasoning}
</div>
</div>
</motion.div>
</CollapsibleContent>
</Collapsible>
</div>
)
}
function ToolCall({
toolInvocations,
}: Pick<ChatMessageProps, "toolInvocations">) {
if (!toolInvocations?.length) return null
return (
<div className="flex flex-col items-start gap-2" aria-live="polite">
{toolInvocations.map((invocation, index) => {
const isCancelled =
invocation.state === "result" &&
invocation.result.__cancelled === true
if (isCancelled) {
return (
<div
key={index}
className="flex items-center gap-2 rounded-lg border bg-muted px-3 py-2 text-sm text-muted-foreground"
>
<Ban className="h-4 w-4" />
<span>
Cancelled{" "}
<span className="font-mono">
{"`"}
{invocation.toolName}
{"`"}
</span>
</span>
</div>
)
}
switch (invocation.state) {
case "partial-call":
case "call":
return (
<div
key={index}
className="flex items-center gap-2 rounded-lg border bg-muted px-3 py-2 text-sm text-muted-foreground"
>
<Terminal className="h-4 w-4" />
<span>
Calling{" "}
<span className="font-mono">
{"`"}
{invocation.toolName}
{"`"}
</span>
...
</span>
<Loader2 className="h-3 w-3 animate-spin" />
</div>
)
case "result":
return (
<div
key={index}
className="flex flex-col gap-1.5 rounded-lg border bg-muted px-3 py-2 text-sm"
>
<div className="flex items-center gap-2 text-muted-foreground">
<Code2 className="h-4 w-4" />
<span>
Result from{" "}
<span className="font-mono">
{"`"}
{invocation.toolName}
{"`"}
</span>
</span>
</div>
<pre className="overflow-x-auto whitespace-pre-wrap text-foreground">
{JSON.stringify(invocation.result, null, 2)}
</pre>
</div>
)
default:
return null
}
})}
</div>
)
}

310
src/components/ui/chat.tsx Executable file
View File

@ -0,0 +1,310 @@
"use client"
import {
forwardRef,
useCallback,
useRef,
type PropsWithChildren,
} from "react"
import { ArrowDown, PaperclipIcon, SquareIcon, ThumbsDown, ThumbsUp } from "lucide-react"
import { cn } from "@/lib/utils"
import { useAutoScroll } from "@/hooks/use-auto-scroll"
import { Button } from "@/components/ui/button"
import { type Message } from "@/components/ui/chat-message"
import { CopyButton } from "@/components/ui/copy-button"
import { MessageList } from "@/components/ui/message-list"
import { PromptSuggestions } from "@/components/ui/prompt-suggestions"
import {
PromptInput,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputTextarea,
PromptInputFooter,
PromptInputTools,
PromptInputButton,
PromptInputSubmit,
PromptInputActionMenu,
PromptInputActionMenuTrigger,
PromptInputActionMenuContent,
PromptInputActionAddAttachments,
} from "@/components/ai/prompt-input"
interface ChatPropsBase {
messages: Array<Message>
className?: string
isGenerating: boolean
stop?: () => void
onRateResponse?: (
messageId: string,
rating: "thumbs-up" | "thumbs-down"
) => void
setMessages?: (messages: Message[]) => void
append: (message: { role: "user"; content: string }) => void
}
interface ChatPropsWithoutSuggestions extends ChatPropsBase {
suggestions?: never
}
interface ChatPropsWithSuggestions extends ChatPropsBase {
suggestions: string[]
}
type ChatProps = ChatPropsWithoutSuggestions | ChatPropsWithSuggestions
export function Chat({
messages,
stop,
isGenerating,
append,
suggestions,
className,
onRateResponse,
setMessages,
}: ChatProps) {
const isEmpty = messages.length === 0
const isTyping = messages.at(-1)?.role === "user"
const messagesRef = useRef(messages)
messagesRef.current = messages
const handleStop = useCallback(() => {
stop?.()
if (!setMessages) return
const latestMessages = [...messagesRef.current]
const lastAssistantMessage = latestMessages.findLast(
(m) => m.role === "assistant"
)
if (!lastAssistantMessage) return
let needsUpdate = false
let updatedMessage = { ...lastAssistantMessage }
if (lastAssistantMessage.toolInvocations) {
const updatedToolInvocations = lastAssistantMessage.toolInvocations.map(
(toolInvocation) => {
if (toolInvocation.state === "call") {
needsUpdate = true
return {
...toolInvocation,
state: "result",
result: {
content: "Tool execution was cancelled",
__cancelled: true,
},
} as const
}
return toolInvocation
}
)
if (needsUpdate) {
updatedMessage = {
...updatedMessage,
toolInvocations: updatedToolInvocations,
}
}
}
if (lastAssistantMessage.parts && lastAssistantMessage.parts.length > 0) {
const updatedParts = lastAssistantMessage.parts.map((part) => {
const p = part as { type: string; toolInvocation?: { state: string } }
if (
p.type === "tool-invocation" &&
p.toolInvocation &&
p.toolInvocation.state === "call"
) {
needsUpdate = true
return {
...part,
toolInvocation: {
...p.toolInvocation,
state: "result" as const,
result: {
content: "Tool execution was cancelled",
__cancelled: true,
},
},
}
}
return part
})
if (needsUpdate) {
updatedMessage = {
...updatedMessage,
parts: updatedParts as Message["parts"],
}
}
}
if (needsUpdate) {
const messageIndex = latestMessages.findIndex(
(m) => m.id === lastAssistantMessage.id
)
if (messageIndex !== -1) {
latestMessages[messageIndex] = updatedMessage
setMessages(latestMessages)
}
}
}, [stop, setMessages, messagesRef])
const messageOptions = useCallback(
(message: Message) => ({
actions: onRateResponse ? (
<>
<div className="border-r pr-1">
<CopyButton
content={message.content}
copyMessage="Copied response to clipboard!"
/>
</div>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => onRateResponse(message.id, "thumbs-up")}
>
<ThumbsUp className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => onRateResponse(message.id, "thumbs-down")}
>
<ThumbsDown className="h-4 w-4" />
</Button>
</>
) : (
<CopyButton
content={message.content}
copyMessage="Copied response to clipboard!"
/>
),
}),
[onRateResponse]
)
return (
<ChatContainer className={className}>
{isEmpty && suggestions?.length ? (
<PromptSuggestions
label="Try these prompts"
append={append}
suggestions={suggestions}
/>
) : null}
{messages.length > 0 ? (
<ChatMessages messages={messages}>
<MessageList
messages={messages}
isTyping={isTyping}
messageOptions={messageOptions}
/>
</ChatMessages>
) : null}
<div className="mt-auto px-4 py-3">
<PromptInput
multiple
onSubmit={({ text }) => {
if (text.trim()) {
append({ role: "user", content: text })
}
}}
>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputBody>
<PromptInputTextarea placeholder="Ask Compass..." />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger>
<PaperclipIcon className="size-4" />
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
{isGenerating ? (
<PromptInputButton onClick={handleStop}>
<SquareIcon className="size-4" />
</PromptInputButton>
) : (
<PromptInputSubmit />
)}
</PromptInputFooter>
</PromptInput>
</div>
</ChatContainer>
)
}
Chat.displayName = "Chat"
export function ChatMessages({
messages,
children,
}: PropsWithChildren<{
messages: Message[]
}>) {
const {
containerRef,
scrollToBottom,
handleScroll,
shouldAutoScroll,
handleTouchStart,
} = useAutoScroll([messages])
return (
<div
className="grid grid-cols-1 overflow-y-auto pb-4"
ref={containerRef}
onScroll={handleScroll}
onTouchStart={handleTouchStart}
>
<div className="max-w-full px-4 pt-4 [grid-column:1/1] [grid-row:1/1]">
{children}
</div>
{!shouldAutoScroll && (
<div className="pointer-events-none flex flex-1 items-end justify-end [grid-column:1/1] [grid-row:1/1]">
<div className="sticky bottom-0 left-0 flex w-full justify-end">
<Button
onClick={scrollToBottom}
className="pointer-events-auto h-8 w-8 rounded-full ease-in-out animate-in fade-in-0 slide-in-from-bottom-1"
size="icon"
variant="ghost"
>
<ArrowDown className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
)
}
export const ChatContainer = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn("grid max-h-full w-full grid-rows-[1fr_auto]", className)}
{...props}
/>
)
})
ChatContainer.displayName = "ChatContainer"

View File

@ -1,6 +1,6 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
function Collapsible({
...props

View File

@ -0,0 +1,44 @@
"use client"
import { Check, Copy } from "lucide-react"
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Button } from "@/components/ui/button"
type CopyButtonProps = {
content: string
copyMessage?: string
}
export function CopyButton({ content, copyMessage }: CopyButtonProps) {
const { isCopied, handleCopy } = useCopyToClipboard({
text: content,
copyMessage,
})
return (
<Button
variant="ghost"
size="icon"
className="relative h-6 w-6"
aria-label="Copy to clipboard"
onClick={handleCopy}
>
<div className="absolute inset-0 flex items-center justify-center">
<Check
className={cn(
"h-4 w-4 transition-transform ease-in-out",
isCopied ? "scale-100" : "scale-0"
)}
/>
</div>
<Copy
className={cn(
"h-4 w-4 transition-transform ease-in-out",
isCopied ? "scale-0" : "scale-100"
)}
/>
</Button>
)
}

View File

@ -0,0 +1,153 @@
"use client"
import React, { useEffect } from "react"
import { motion } from "framer-motion"
import { FileIcon, X } from "lucide-react"
interface FilePreviewProps {
file: File
onRemove?: () => void
}
export const FilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
(props, ref) => {
if (props.file.type.startsWith("image/")) {
return <ImageFilePreview {...props} ref={ref} />
}
if (
props.file.type.startsWith("text/") ||
props.file.name.endsWith(".txt") ||
props.file.name.endsWith(".md")
) {
return <TextFilePreview {...props} ref={ref} />
}
return <GenericFilePreview {...props} ref={ref} />
}
)
FilePreview.displayName = "FilePreview"
const ImageFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
return (
<motion.div
ref={ref}
className="relative flex max-w-[200px] rounded-md border p-1.5 pr-2 text-xs"
layout
initial={{ opacity: 0, y: "100%" }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "100%" }}
>
<div className="flex w-full items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt={`Attachment ${file.name}`}
className="grid h-10 w-10 shrink-0 place-items-center rounded-sm border bg-muted object-cover"
src={URL.createObjectURL(file)}
/>
<span className="w-full truncate text-muted-foreground">
{file.name}
</span>
</div>
{onRemove ? (
<button
className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full border bg-background"
type="button"
onClick={onRemove}
aria-label="Remove attachment"
>
<X className="h-2.5 w-2.5" />
</button>
) : null}
</motion.div>
)
}
)
ImageFilePreview.displayName = "ImageFilePreview"
const TextFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
const [preview, setPreview] = React.useState<string>("")
useEffect(() => {
const reader = new FileReader()
reader.onload = (e) => {
const text = e.target?.result as string
setPreview(text.slice(0, 50) + (text.length > 50 ? "..." : ""))
}
reader.readAsText(file)
}, [file])
return (
<motion.div
ref={ref}
className="relative flex max-w-[200px] rounded-md border p-1.5 pr-2 text-xs"
layout
initial={{ opacity: 0, y: "100%" }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "100%" }}
>
<div className="flex w-full items-center space-x-2">
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-sm border bg-muted p-0.5">
<div className="h-full w-full overflow-hidden text-[6px] leading-none text-muted-foreground">
{preview || "Loading..."}
</div>
</div>
<span className="w-full truncate text-muted-foreground">
{file.name}
</span>
</div>
{onRemove ? (
<button
className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full border bg-background"
type="button"
onClick={onRemove}
aria-label="Remove attachment"
>
<X className="h-2.5 w-2.5" />
</button>
) : null}
</motion.div>
)
}
)
TextFilePreview.displayName = "TextFilePreview"
const GenericFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
return (
<motion.div
ref={ref}
className="relative flex max-w-[200px] rounded-md border p-1.5 pr-2 text-xs"
layout
initial={{ opacity: 0, y: "100%" }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "100%" }}
>
<div className="flex w-full items-center space-x-2">
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-sm border bg-muted">
<FileIcon className="h-6 w-6 text-foreground" />
</div>
<span className="w-full truncate text-muted-foreground">
{file.name}
</span>
</div>
{onRemove ? (
<button
className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full border bg-background"
type="button"
onClick={onRemove}
aria-label="Remove attachment"
>
<X className="h-2.5 w-2.5" />
</button>
) : null}
</motion.div>
)
}
)
GenericFilePreview.displayName = "GenericFilePreview"

170
src/components/ui/input-group.tsx Executable file
View File

@ -0,0 +1,170 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}

View File

@ -0,0 +1,41 @@
"use client"
import { AnimatePresence, motion } from "framer-motion"
import { X } from "lucide-react"
interface InterruptPromptProps {
isOpen: boolean
close: () => void
}
export function InterruptPrompt({ isOpen, close }: InterruptPromptProps) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ top: 0, filter: "blur(5px)" }}
animate={{
top: -40,
filter: "blur(0px)",
transition: {
type: "spring",
filter: { type: "tween" },
},
}}
exit={{ top: 0, filter: "blur(5px)" }}
className="absolute left-1/2 flex -translate-x-1/2 overflow-hidden whitespace-nowrap rounded-full border bg-background py-1 text-center text-sm text-muted-foreground"
>
<span className="ml-2.5">Press Enter again to interrupt</span>
<button
className="ml-1 mr-2.5 flex items-center"
type="button"
onClick={close}
aria-label="Close"
>
<X className="h-3 w-3" />
</button>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@ -0,0 +1,237 @@
import React, { Suspense } from "react"
import Markdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { createHighlighterCore, type HighlighterCore } from "shiki/core"
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
import { cn } from "@/lib/utils"
import { CopyButton } from "@/components/ui/copy-button"
const SUPPORTED_LANGS = [
"typescript", "javascript", "json", "bash",
"css", "html", "sql", "yaml", "markdown",
] as const
type SupportedLang = typeof SUPPORTED_LANGS[number]
let highlighterPromise: Promise<HighlighterCore> | null = null
function getHighlighter(): Promise<HighlighterCore> {
if (!highlighterPromise) {
highlighterPromise = createHighlighterCore({
themes: [
import("@shikijs/themes/github-light"),
import("@shikijs/themes/github-dark"),
],
langs: [
import("@shikijs/langs/typescript"),
import("@shikijs/langs/javascript"),
import("@shikijs/langs/json"),
import("@shikijs/langs/bash"),
import("@shikijs/langs/css"),
import("@shikijs/langs/html"),
import("@shikijs/langs/sql"),
import("@shikijs/langs/yaml"),
import("@shikijs/langs/markdown"),
],
engine: createJavaScriptRegexEngine(),
})
}
return highlighterPromise
}
function isSupportedLang(lang: string): lang is SupportedLang {
return (SUPPORTED_LANGS as readonly string[]).includes(lang)
}
interface MarkdownRendererProps {
children: string
}
export function MarkdownRenderer({ children }: MarkdownRendererProps) {
return (
<div className="space-y-3">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Markdown remarkPlugins={[remarkGfm]} components={COMPONENTS as any}>
{children}
</Markdown>
</div>
)
}
interface HighlightedPre extends React.HTMLAttributes<HTMLPreElement> {
children: string
language: string
}
const HighlightedPre = React.memo(
async ({ children, language, ...props }: HighlightedPre) => {
if (!isSupportedLang(language)) {
return <pre {...props}>{children}</pre>
}
const highlighter = await getHighlighter()
const { tokens } = highlighter.codeToTokens(children, {
lang: language,
defaultColor: false,
themes: {
light: "github-light",
dark: "github-dark",
},
})
return (
<pre {...props}>
<code>
{tokens.map((line, lineIndex) => (
<>
<span key={lineIndex}>
{line.map((token, tokenIndex) => {
const style =
typeof token.htmlStyle === "string"
? undefined
: token.htmlStyle
return (
<span
key={tokenIndex}
className="text-shiki-light bg-shiki-light-bg dark:text-shiki-dark dark:bg-shiki-dark-bg"
style={style}
>
{token.content}
</span>
)
})}
</span>
{lineIndex !== tokens.length - 1 && "\n"}
</>
))}
</code>
</pre>
)
}
)
HighlightedPre.displayName = "HighlightedCode"
interface CodeBlockProps extends React.HTMLAttributes<HTMLPreElement> {
children: React.ReactNode
className?: string
language: string
}
const CodeBlock = ({
children,
className,
language,
...restProps
}: CodeBlockProps) => {
const code =
typeof children === "string"
? children
: childrenTakeAllStringContents(children)
const preClass = cn(
"overflow-x-scroll rounded-md border bg-background/50 p-4 font-mono text-sm [scrollbar-width:none]",
className
)
return (
<div className="group/code relative mb-4">
<Suspense
fallback={
<pre className={preClass} {...restProps}>
{children}
</pre>
}
>
<HighlightedPre language={language} className={preClass}>
{code}
</HighlightedPre>
</Suspense>
<div className="invisible absolute right-2 top-2 flex space-x-1 rounded-lg p-1 opacity-0 transition-all duration-200 group-hover/code:visible group-hover/code:opacity-100">
<CopyButton content={code} copyMessage="Copied code to clipboard" />
</div>
</div>
)
}
function childrenTakeAllStringContents(element: unknown): string {
if (typeof element === "string") {
return element
}
const el = element as { props?: { children?: unknown } } | null
if (el?.props?.children) {
const children = el.props.children
if (Array.isArray(children)) {
return children
.map((child: unknown) => childrenTakeAllStringContents(child))
.join("")
} else {
return childrenTakeAllStringContents(children)
}
}
return ""
}
const COMPONENTS = {
h1: withClass("h1", "text-2xl font-semibold"),
h2: withClass("h2", "font-semibold text-xl"),
h3: withClass("h3", "font-semibold text-lg"),
h4: withClass("h4", "font-semibold text-base"),
h5: withClass("h5", "font-medium"),
strong: withClass("strong", "font-semibold"),
a: withClass("a", "text-primary underline underline-offset-2"),
blockquote: withClass("blockquote", "border-l-2 border-primary pl-4"),
code: ({ children, className, node: _node, ...rest }: { children?: React.ReactNode; className?: string; node?: unknown } & Record<string, unknown>) => {
const match = /language-(\w+)/.exec(className || "")
return match ? (
<CodeBlock className={className} language={match[1]} {...rest}>
{children}
</CodeBlock>
) : (
<code
className={cn(
"font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:bg-background/50 [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5"
)}
{...rest}
>
{children}
</code>
)
},
pre: ({ children }: { children?: React.ReactNode }) => children,
ol: withClass("ol", "list-decimal space-y-2 pl-6"),
ul: withClass("ul", "list-disc space-y-2 pl-6"),
li: withClass("li", "my-1.5"),
table: withClass(
"table",
"w-full border-collapse overflow-y-auto rounded-md border border-foreground/20"
),
th: withClass(
"th",
"border border-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"
),
td: withClass(
"td",
"border border-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
),
tr: withClass("tr", "m-0 border-t p-0 even:bg-muted"),
p: withClass("p", "whitespace-pre-wrap"),
hr: withClass("hr", "border-foreground/20"),
}
function withClass(Tag: keyof React.JSX.IntrinsicElements, classes: string) {
const Component = ({ node, ...props }: { node?: unknown } & Record<string, unknown>) => {
const Element = Tag as React.ElementType
return <Element className={classes} {...props} />
}
Component.displayName = String(Tag)
return Component
}
export default MarkdownRenderer

View File

@ -0,0 +1,429 @@
"use client"
import React, { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { ArrowUp, Info, Loader2, Mic, Paperclip, Square } from "lucide-react"
import { omit } from "remeda"
import { cn } from "@/lib/utils"
import { useAudioRecording } from "@/hooks/use-audio-recording"
import { useAutosizeTextArea } from "@/hooks/use-autosize-textarea"
import { AudioVisualizer } from "@/components/ui/audio-visualizer"
import { Button } from "@/components/ui/button"
import { FilePreview } from "@/components/ui/file-preview"
import { InterruptPrompt } from "@/components/ui/interrupt-prompt"
interface MessageInputBaseProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
value: string
submitOnEnter?: boolean
stop?: () => void
isGenerating: boolean
enableInterrupt?: boolean
transcribeAudio?: (blob: Blob) => Promise<string>
}
interface MessageInputWithoutAttachmentProps extends MessageInputBaseProps {
allowAttachments?: false
}
interface MessageInputWithAttachmentsProps extends MessageInputBaseProps {
allowAttachments: true
files: File[] | null
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>
}
type MessageInputProps =
| MessageInputWithoutAttachmentProps
| MessageInputWithAttachmentsProps
export function MessageInput({
placeholder = "Message Compass...",
className,
onKeyDown: onKeyDownProp,
submitOnEnter = true,
stop,
isGenerating,
enableInterrupt = true,
transcribeAudio,
...props
}: MessageInputProps) {
const [isDragging, setIsDragging] = useState(false)
const [showInterruptPrompt, setShowInterruptPrompt] = useState(false)
const {
isListening,
isSpeechSupported,
isRecording,
isTranscribing,
audioStream,
toggleListening,
stopRecording,
} = useAudioRecording({
transcribeAudio,
onTranscriptionComplete: (text) => {
props.onChange?.({ target: { value: text } } as React.ChangeEvent<HTMLTextAreaElement>)
},
})
useEffect(() => {
if (!isGenerating) {
setShowInterruptPrompt(false)
}
}, [isGenerating])
const addFiles = (files: File[] | null) => {
if (props.allowAttachments) {
props.setFiles((currentFiles) => {
if (currentFiles === null) return files
if (files === null) return currentFiles
return [...currentFiles, ...files]
})
}
}
const onDragOver = (event: React.DragEvent) => {
if (props.allowAttachments !== true) return
event.preventDefault()
setIsDragging(true)
}
const onDragLeave = (event: React.DragEvent) => {
if (props.allowAttachments !== true) return
event.preventDefault()
setIsDragging(false)
}
const onDrop = (event: React.DragEvent) => {
setIsDragging(false)
if (props.allowAttachments !== true) return
event.preventDefault()
const dataTransfer = event.dataTransfer
if (dataTransfer.files.length) {
addFiles(Array.from(dataTransfer.files))
}
}
const onPaste = (event: React.ClipboardEvent) => {
const items = event.clipboardData?.items
if (!items) return
const text = event.clipboardData.getData("text")
if (text && text.length > 500 && props.allowAttachments) {
event.preventDefault()
const blob = new Blob([text], { type: "text/plain" })
const file = new File([blob], "Pasted text", {
type: "text/plain",
lastModified: Date.now(),
})
addFiles([file])
return
}
const files = Array.from(items)
.map((item) => item.getAsFile())
.filter((file) => file !== null)
if (props.allowAttachments && files.length > 0) {
addFiles(files)
}
}
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
if (isGenerating && stop && enableInterrupt) {
if (showInterruptPrompt) {
stop()
setShowInterruptPrompt(false)
event.currentTarget.form?.requestSubmit()
} else if (
props.value ||
(props.allowAttachments && props.files?.length)
) {
setShowInterruptPrompt(true)
return
}
}
event.currentTarget.form?.requestSubmit()
}
onKeyDownProp?.(event)
}
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const showFileList =
props.allowAttachments && props.files && props.files.length > 0
useAutosizeTextArea({
ref: textAreaRef,
maxHeight: 240,
borderWidth: 0,
dependencies: [props.value],
})
return (
<div
className="relative flex w-full"
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
>
{enableInterrupt && (
<InterruptPrompt
isOpen={showInterruptPrompt}
close={() => setShowInterruptPrompt(false)}
/>
)}
<RecordingPrompt
isVisible={isRecording}
onStopRecording={stopRecording}
/>
<div
className={cn(
"relative flex w-full flex-col overflow-hidden rounded-md border border-input bg-background shadow-xs",
"transition-[color,box-shadow]",
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
className
)}
>
<textarea
aria-label="Write your prompt here"
placeholder={placeholder}
ref={textAreaRef}
onPaste={onPaste}
onKeyDown={onKeyDown}
className="w-full grow resize-none bg-transparent px-3 pt-3 pb-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
{...(props.allowAttachments
? omit(props, ["allowAttachments", "files", "setFiles"])
: omit(props, ["allowAttachments"]))}
/>
{showFileList && (
<div className="overflow-x-auto px-3 pb-1">
<div className="flex gap-2">
<AnimatePresence mode="popLayout">
{props.allowAttachments &&
props.files?.map((file) => (
<FilePreview
key={file.name + String(file.lastModified)}
file={file}
onRemove={() => {
props.setFiles((files) => {
if (!files) return null
const filtered = Array.from(files).filter(
(f) => f !== file
)
if (filtered.length === 0) return null
return filtered
})
}}
/>
))}
</AnimatePresence>
</div>
</div>
)}
<div className="flex items-center justify-between px-2 pb-2">
<div className="flex items-center gap-1">
{props.allowAttachments && (
<Button
type="button"
size="icon-sm"
variant="ghost"
aria-label="Attach a file"
onClick={async () => {
const files = await showFileUploadDialog()
addFiles(files)
}}
>
<Paperclip className="h-4 w-4" />
</Button>
)}
{isSpeechSupported && (
<Button
type="button"
variant="ghost"
size="icon-sm"
className={cn(isListening && "text-primary")}
aria-label="Voice input"
onClick={toggleListening}
>
<Mic className="h-4 w-4" />
</Button>
)}
</div>
<div className="flex items-center gap-1">
{isGenerating && stop ? (
<Button
type="button"
size="icon-sm"
aria-label="Stop generating"
onClick={stop}
>
<Square className="h-3 w-3" fill="currentColor" />
</Button>
) : (
<Button
type="submit"
size="icon-sm"
aria-label="Send message"
disabled={props.value === "" || isGenerating}
>
<ArrowUp className="h-4 w-4" />
</Button>
)}
</div>
</div>
{props.allowAttachments && <FileUploadOverlay isDragging={isDragging} />}
<RecordingControls
isRecording={isRecording}
isTranscribing={isTranscribing}
audioStream={audioStream}
onStopRecording={stopRecording}
/>
</div>
</div>
)
}
MessageInput.displayName = "MessageInput"
interface FileUploadOverlayProps {
isDragging: boolean
}
function FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {
return (
<AnimatePresence>
{isDragging && (
<motion.div
className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center space-x-2 rounded-md border border-dashed border-border bg-background text-sm text-muted-foreground"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
aria-hidden
>
<Paperclip className="h-4 w-4" />
<span>Drop your files here to attach them.</span>
</motion.div>
)}
</AnimatePresence>
)
}
function showFileUploadDialog() {
const input = document.createElement("input")
input.type = "file"
input.multiple = true
input.accept = "*/*"
input.click()
return new Promise<File[] | null>((resolve) => {
input.onchange = (e) => {
const files = (e.currentTarget as HTMLInputElement).files
if (files) {
resolve(Array.from(files))
return
}
resolve(null)
}
})
}
function TranscribingOverlay() {
return (
<motion.div
className="flex h-full w-full flex-col items-center justify-center rounded-md bg-background/80 backdrop-blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="mt-4 text-sm font-medium text-muted-foreground">
Transcribing audio...
</p>
</motion.div>
)
}
interface RecordingPromptProps {
isVisible: boolean
onStopRecording: () => void
}
function RecordingPrompt({ isVisible, onStopRecording }: RecordingPromptProps) {
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ top: 0, filter: "blur(5px)" }}
animate={{
top: -40,
filter: "blur(0px)",
transition: {
type: "spring",
filter: { type: "tween" },
},
}}
exit={{ top: 0, filter: "blur(5px)" }}
className="absolute left-1/2 flex -translate-x-1/2 cursor-pointer overflow-hidden whitespace-nowrap rounded-full border bg-background py-1 text-center text-sm text-muted-foreground"
onClick={onStopRecording}
>
<span className="mx-2.5 flex items-center">
<Info className="mr-2 h-3 w-3" />
Click to finish recording
</span>
</motion.div>
)}
</AnimatePresence>
)
}
interface RecordingControlsProps {
isRecording: boolean
isTranscribing: boolean
audioStream: MediaStream | null
onStopRecording: () => void
}
function RecordingControls({
isRecording,
isTranscribing,
audioStream,
onStopRecording,
}: RecordingControlsProps) {
if (isRecording) {
return (
<div className="absolute inset-0 z-50 overflow-hidden rounded-md">
<AudioVisualizer
stream={audioStream}
isRecording={isRecording}
onClick={onStopRecording}
/>
</div>
)
}
if (isTranscribing) {
return (
<div className="absolute inset-0 z-50 overflow-hidden rounded-md">
<TranscribingOverlay />
</div>
)
}
return null
}

View File

@ -0,0 +1,45 @@
import {
ChatMessage,
type ChatMessageProps,
type Message,
} from "@/components/ui/chat-message"
import { TypingIndicator } from "@/components/ui/typing-indicator"
type AdditionalMessageOptions = Omit<ChatMessageProps, keyof Message>
interface MessageListProps {
messages: Message[]
showTimeStamps?: boolean
isTyping?: boolean
messageOptions?:
| AdditionalMessageOptions
| ((message: Message) => AdditionalMessageOptions)
}
export function MessageList({
messages,
showTimeStamps = true,
isTyping = false,
messageOptions,
}: MessageListProps) {
return (
<div className="space-y-6 overflow-visible">
{messages.map((message, index) => {
const additionalOptions =
typeof messageOptions === "function"
? messageOptions(message)
: messageOptions
return (
<ChatMessage
key={index}
showTimeStamp={showTimeStamps}
{...message}
{...additionalOptions}
/>
)
})}
{isTyping && <TypingIndicator />}
</div>
)
}

View File

@ -0,0 +1,38 @@
import { cn } from "@/lib/utils"
interface PromptSuggestionsProps {
label: string
append: (message: { role: "user"; content: string }) => void
suggestions: string[]
}
export function PromptSuggestions({
label,
append,
suggestions,
}: PromptSuggestionsProps) {
return (
<div className="flex flex-1 flex-col justify-end gap-3 px-4 pb-4">
<p className="text-xs text-muted-foreground">{label}</p>
<div className="flex flex-col gap-2">
{suggestions.map((suggestion) => (
<button
key={suggestion}
onClick={() =>
append({ role: "user", content: suggestion })
}
className={cn(
"rounded-lg border px-3 py-2.5 text-left text-sm",
"text-foreground",
"transition-colors hover:bg-muted",
"focus-visible:border-ring focus-visible:ring-ring/50",
"focus-visible:outline-none focus-visible:ring-[3px]"
)}
>
{suggestion}
</button>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,15 @@
import { Dot } from "lucide-react"
export function TypingIndicator() {
return (
<div className="justify-left flex space-x-1">
<div className="rounded-lg bg-muted p-3">
<div className="flex -space-x-2.5">
<Dot className="h-5 w-5 animate-typing-dot-bounce" />
<Dot className="h-5 w-5 animate-typing-dot-bounce [animation-delay:90ms]" />
<Dot className="h-5 w-5 animate-typing-dot-bounce [animation-delay:180ms]" />
</div>
</div>
</div>
)
}

View File

@ -244,3 +244,34 @@ export type GroupMember = typeof groupMembers.$inferSelect
export type NewGroupMember = typeof groupMembers.$inferInsert
export type ProjectMember = typeof projectMembers.$inferSelect
export type NewProjectMember = typeof projectMembers.$inferInsert
// Agent memory tables for ElizaOS
export const agentConversations = sqliteTable("agent_conversations", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
title: text("title"),
lastMessageAt: text("last_message_at").notNull(),
createdAt: text("created_at").notNull(),
})
export const agentMemories = sqliteTable("agent_memories", {
id: text("id").primaryKey(),
conversationId: text("conversation_id")
.notNull()
.references(() => agentConversations.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
role: text("role").notNull(), // "user" | "assistant"
content: text("content").notNull(),
embedding: text("embedding"), // JSON array of floats for vector search
metadata: text("metadata"), // JSON object for action results, ui specs, etc.
createdAt: text("created_at").notNull(),
})
export type AgentConversation = typeof agentConversations.$inferSelect
export type NewAgentConversation = typeof agentConversations.$inferInsert
export type AgentMemory = typeof agentMemories.$inferSelect
export type NewAgentMemory = typeof agentMemories.$inferInsert

View File

@ -0,0 +1,93 @@
import { useEffect, useRef, useState } from "react"
import { recordAudio } from "@/lib/audio-utils"
interface UseAudioRecordingOptions {
transcribeAudio?: (blob: Blob) => Promise<string>
onTranscriptionComplete?: (text: string) => void
}
export function useAudioRecording({
transcribeAudio,
onTranscriptionComplete,
}: UseAudioRecordingOptions) {
const [isListening, setIsListening] = useState(false)
const [isSpeechSupported, setIsSpeechSupported] = useState(!!transcribeAudio)
const [isRecording, setIsRecording] = useState(false)
const [isTranscribing, setIsTranscribing] = useState(false)
const [audioStream, setAudioStream] = useState<MediaStream | null>(null)
const activeRecordingRef = useRef<Promise<Blob> | null>(null)
useEffect(() => {
const checkSpeechSupport = async () => {
const hasMediaDevices = !!(
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
)
setIsSpeechSupported(hasMediaDevices && !!transcribeAudio)
}
checkSpeechSupport()
}, [transcribeAudio])
const stopRecording = async () => {
setIsRecording(false)
setIsTranscribing(true)
try {
// First stop the recording to get the final blob
recordAudio.stop()
// Wait for the recording promise to resolve with the final blob
const recording = await activeRecordingRef.current
if (transcribeAudio && recording) {
const text = await transcribeAudio(recording)
onTranscriptionComplete?.(text)
}
} catch (error) {
console.error("Error transcribing audio:", error)
} finally {
setIsTranscribing(false)
setIsListening(false)
if (audioStream) {
audioStream.getTracks().forEach((track) => track.stop())
setAudioStream(null)
}
activeRecordingRef.current = null
}
}
const toggleListening = async () => {
if (!isListening) {
try {
setIsListening(true)
setIsRecording(true)
// Get audio stream first
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
})
setAudioStream(stream)
// Start recording with the stream
activeRecordingRef.current = recordAudio(stream)
} catch (error) {
console.error("Error recording audio:", error)
setIsListening(false)
setIsRecording(false)
if (audioStream) {
audioStream.getTracks().forEach((track) => track.stop())
setAudioStream(null)
}
}
} else {
await stopRecording()
}
}
return {
isListening,
isSpeechSupported,
isRecording,
isTranscribing,
audioStream,
toggleListening,
stopRecording,
}
}

73
src/hooks/use-auto-scroll.ts Executable file
View File

@ -0,0 +1,73 @@
import { useEffect, useRef, useState } from "react"
// How many pixels from the bottom of the container to enable auto-scroll
const ACTIVATION_THRESHOLD = 50
// Minimum pixels of scroll-up movement required to disable auto-scroll
const MIN_SCROLL_UP_THRESHOLD = 10
export function useAutoScroll(dependencies: React.DependencyList) {
const containerRef = useRef<HTMLDivElement | null>(null)
const previousScrollTop = useRef<number | null>(null)
const [shouldAutoScroll, setShouldAutoScroll] = useState(true)
const scrollToBottom = () => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
}
const handleScroll = () => {
if (containerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = containerRef.current
const distanceFromBottom = Math.abs(
scrollHeight - scrollTop - clientHeight
)
const isScrollingUp = previousScrollTop.current
? scrollTop < previousScrollTop.current
: false
const scrollUpDistance = previousScrollTop.current
? previousScrollTop.current - scrollTop
: 0
const isDeliberateScrollUp =
isScrollingUp && scrollUpDistance > MIN_SCROLL_UP_THRESHOLD
if (isDeliberateScrollUp) {
setShouldAutoScroll(false)
} else {
const isScrolledToBottom = distanceFromBottom < ACTIVATION_THRESHOLD
setShouldAutoScroll(isScrolledToBottom)
}
previousScrollTop.current = scrollTop
}
}
const handleTouchStart = () => {
setShouldAutoScroll(false)
}
useEffect(() => {
if (containerRef.current) {
previousScrollTop.current = containerRef.current.scrollTop
}
}, [])
useEffect(() => {
if (shouldAutoScroll) {
scrollToBottom()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies)
return {
containerRef,
scrollToBottom,
handleScroll,
shouldAutoScroll,
handleTouchStart,
}
}

View File

@ -0,0 +1,39 @@
import { useLayoutEffect, useRef } from "react"
interface UseAutosizeTextAreaProps {
ref: React.RefObject<HTMLTextAreaElement | null>
maxHeight?: number
borderWidth?: number
dependencies: React.DependencyList
}
export function useAutosizeTextArea({
ref,
maxHeight = Number.MAX_SAFE_INTEGER,
borderWidth = 0,
dependencies,
}: UseAutosizeTextAreaProps) {
const originalHeight = useRef<number | null>(null)
useLayoutEffect(() => {
if (!ref.current) return
const currentRef = ref.current
const borderAdjustment = borderWidth * 2
if (originalHeight.current === null) {
originalHeight.current = currentRef.scrollHeight - borderAdjustment
}
currentRef.style.removeProperty("height")
const scrollHeight = currentRef.scrollHeight
// Make sure we don't go over maxHeight
const clampedToMax = Math.min(scrollHeight, maxHeight)
// Make sure we don't go less than the original height
const clampedToMin = Math.max(clampedToMax, originalHeight.current)
currentRef.style.height = `${clampedToMin + borderAdjustment}px`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxHeight, ref, ...dependencies])
}

View File

@ -0,0 +1,36 @@
import { useCallback, useRef, useState } from "react"
import { toast } from "sonner"
type UseCopyToClipboardProps = {
text: string
copyMessage?: string
}
export function useCopyToClipboard({
text,
copyMessage = "Copied to clipboard!",
}: UseCopyToClipboardProps) {
const [isCopied, setIsCopied] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const handleCopy = useCallback(() => {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success(copyMessage)
setIsCopied(true)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
timeoutRef.current = setTimeout(() => {
setIsCopied(false)
}, 2000)
})
.catch(() => {
toast.error("Failed to copy to clipboard.")
})
}, [text, copyMessage])
return { isCopied, handleCopy }
}

View File

@ -0,0 +1,23 @@
"use client"
import { useEffect } from "react"
import type { LucideIcon } from "lucide-react"
import { usePageActionsContext } from "@/components/page-actions-provider"
interface PageAction {
readonly id: string
readonly label: string
readonly icon?: LucideIcon
readonly onSelect: () => void
}
export function useRegisterPageActions(
actions: ReadonlyArray<PageAction>
): void {
const { register } = usePageActionsContext()
useEffect(() => {
if (actions.length === 0) return
return register(actions)
}, [actions, register])
}

50
src/lib/audio-utils.ts Executable file
View File

@ -0,0 +1,50 @@
type RecordAudioType = {
(stream: MediaStream): Promise<Blob>
stop: () => void
currentRecorder?: MediaRecorder
}
export const recordAudio = (function (): RecordAudioType {
const func = async function recordAudio(stream: MediaStream): Promise<Blob> {
try {
const mediaRecorder = new MediaRecorder(stream, {
mimeType: "audio/webm;codecs=opus",
})
const audioChunks: Blob[] = []
return new Promise((resolve, reject) => {
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: "audio/webm" })
resolve(audioBlob)
}
mediaRecorder.onerror = () => {
reject(new Error("MediaRecorder error occurred"))
}
mediaRecorder.start(1000)
;(func as RecordAudioType).currentRecorder = mediaRecorder
})
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred"
throw new Error("Failed to start recording: " + errorMessage)
}
}
;(func as RecordAudioType).stop = () => {
const recorder = (func as RecordAudioType).currentRecorder
if (recorder && recorder.state !== "inactive") {
recorder.stop()
}
delete (func as RecordAudioType).currentRecorder
}
return func as RecordAudioType
})()

340
src/lib/eliza/chat-adapter.ts Executable file
View File

@ -0,0 +1,340 @@
/**
* ElizaOS Chat Adapter
*
* useChat-like hook for the shadcn Chat component.
* Communicates with ElizaOS via the /api/agent proxy route.
*
* Bug fixes from original:
* 1. initializeActionHandlers accepts getter fn (not stale router ref)
* 2. context option passed in POST body
* 3. useEffect cleanup for handler unregistration
* 4. options stored in ref to avoid stale closures in sendMessage
*/
"use client"
import { useState, useCallback, useRef } from "react"
export interface ChatMessage {
id: string
role: "user" | "assistant"
content: string
createdAt?: Date
actions?: ReadonlyArray<AgentAction>
isLoading?: boolean
}
export interface AgentAction {
type: string
payload?: Record<string, unknown>
}
export interface UseElizaChatOptions {
conversationId?: string
context?: { view?: string; projectId?: string }
onConversationCreate?: (id: string) => void
onAction?: (action: AgentAction) => void
onError?: (error: Error) => void
}
export interface UseElizaChatReturn {
messages: ReadonlyArray<ChatMessage>
input: string
setInput: (value: string) => void
handleInputChange: (
e: React.ChangeEvent<HTMLTextAreaElement>
) => void
handleSubmit: (e?: React.FormEvent) => Promise<void>
isGenerating: boolean
stop: () => void
append: (message: { role: "user"; content: string }) => Promise<void>
setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>
conversationId: string | null
reload: () => Promise<void>
}
interface AgentResponse {
id: string
text: string
actions?: ReadonlyArray<AgentAction>
conversationId: string
}
export function useElizaChat(
options: UseElizaChatOptions = {}
): UseElizaChatReturn {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [input, setInput] = useState("")
const [isGenerating, setIsGenerating] = useState(false)
const [conversationId, setConversationId] = useState<string | null>(
options.conversationId ?? null
)
const abortControllerRef = useRef<AbortController | null>(null)
// Fix bug 4: store options in ref so sendMessage doesn't
// close over a stale options object
const optionsRef = useRef(options)
optionsRef.current = options
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value)
},
[]
)
const sendMessage = useCallback(
async (content: string) => {
if (!content.trim()) return
const userMessage: ChatMessage = {
id: crypto.randomUUID(),
role: "user",
content,
createdAt: new Date(),
}
setMessages((prev) => [...prev, userMessage])
const loadingMessage: ChatMessage = {
id: crypto.randomUUID(),
role: "assistant",
content: "",
isLoading: true,
createdAt: new Date(),
}
setMessages((prev) => [...prev, loadingMessage])
setIsGenerating(true)
abortControllerRef.current = new AbortController()
try {
const response = await fetch("/api/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: content,
conversationId,
// Fix bug 2: include context in POST body
context: optionsRef.current.context,
}),
signal: abortControllerRef.current.signal,
})
if (!response.ok) {
const errorData = (await response.json()) as {
error?: string
}
throw new Error(errorData.error ?? "Failed to get response")
}
const data: AgentResponse = await response.json()
if (
data.conversationId &&
data.conversationId !== conversationId
) {
setConversationId(data.conversationId)
optionsRef.current.onConversationCreate?.(
data.conversationId
)
}
setMessages((prev) =>
prev.map((msg) =>
msg.id === loadingMessage.id
? {
...msg,
id: data.id,
content: data.text,
actions: data.actions
? [...data.actions]
: undefined,
isLoading: false,
}
: msg
)
)
if (data.actions) {
for (const action of data.actions) {
optionsRef.current.onAction?.(action)
}
}
} catch (err) {
const error = err as Error
if (error.name === "AbortError") {
setMessages((prev) =>
prev.filter((msg) => msg.id !== loadingMessage.id)
)
} else {
setMessages((prev) =>
prev.map((msg) =>
msg.id === loadingMessage.id
? {
...msg,
content:
"Sorry, I encountered an error. Please try again.",
isLoading: false,
}
: msg
)
)
optionsRef.current.onError?.(error)
}
} finally {
setIsGenerating(false)
abortControllerRef.current = null
}
},
// Fix bug 4: only depend on conversationId, not options
[conversationId]
)
const handleSubmit = useCallback(
async (e?: React.FormEvent) => {
e?.preventDefault()
const content = input.trim()
setInput("")
await sendMessage(content)
},
[input, sendMessage]
)
const stop = useCallback(() => {
abortControllerRef.current?.abort()
setIsGenerating(false)
}, [])
const append = useCallback(
async (message: { role: "user"; content: string }) => {
await sendMessage(message.content)
},
[sendMessage]
)
const reload = useCallback(async () => {
const lastUserMessage = [...messages]
.reverse()
.find((m) => m.role === "user")
if (lastUserMessage) {
setMessages((prev) => {
const lastIndex = prev.findLastIndex(
(m) => m.role === "assistant"
)
if (lastIndex >= 0) {
return prev.filter((_, i) => i !== lastIndex)
}
return prev
})
await sendMessage(lastUserMessage.content)
}
}, [messages, sendMessage])
return {
messages,
input,
setInput,
handleInputChange,
handleSubmit,
isGenerating,
stop,
append,
setMessages,
conversationId,
reload,
}
}
// --- Action handler registry ---
export type ActionHandler = (
payload?: Record<string, unknown>
) => void | Promise<void>
const actionHandlers = new Map<string, ActionHandler>()
export function registerActionHandler(
type: string,
handler: ActionHandler
): void {
actionHandlers.set(type, handler)
}
export function unregisterActionHandler(type: string): void {
actionHandlers.delete(type)
}
export async function executeAction(
action: AgentAction
): Promise<void> {
const handler = actionHandlers.get(action.type)
if (handler) {
await handler(action.payload)
} else {
console.warn(
`No handler registered for action type: ${action.type}`
)
}
}
// Fix bug 1: accept getter function instead of direct router ref
// so the handler always uses the current router instance
export function initializeActionHandlers(
getRouter: () => { push: (path: string) => void }
): void {
registerActionHandler("NAVIGATE_TO", (payload) => {
if (payload?.path && typeof payload.path === "string") {
getRouter().push(payload.path)
}
})
registerActionHandler("SHOW_TOAST", (payload) => {
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("agent-toast", { detail: payload })
)
}
})
registerActionHandler("OPEN_MODAL", (payload) => {
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("agent-modal", { detail: payload })
)
}
})
registerActionHandler("CLOSE_MODAL", () => {
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent-modal-close"))
}
})
registerActionHandler("SCROLL_TO", (payload) => {
if (payload?.target && typeof payload.target === "string") {
const el = document.querySelector(
`[data-section="${payload.target}"], #${payload.target}`
)
el?.scrollIntoView({ behavior: "smooth" })
}
})
registerActionHandler("FOCUS_ELEMENT", (payload) => {
if (payload?.selector && typeof payload.selector === "string") {
const el = document.querySelector(
payload.selector
) as HTMLElement | null
el?.focus()
}
})
}
// All registered handler types for cleanup
export const ALL_HANDLER_TYPES = [
"NAVIGATE_TO",
"SHOW_TOAST",
"OPEN_MODAL",
"CLOSE_MODAL",
"SCROLL_TO",
"FOCUS_ELEMENT",
] as const

View File

@ -0,0 +1,393 @@
/**
* json-render Component Catalog for Compass
*
* Defines the components the agent can render as dynamic UI.
* Each component maps to an existing shadcn/ui component or
* Compass-specific component wrapper.
*/
import { z } from "zod"
// Shared schemas
const ActionSchema = z.object({
type: z.string(),
payload: z.record(z.string(), z.unknown()).optional(),
})
// Data display components
export const DataTableSchema = z.object({
type: z.literal("DataTable"),
props: z.object({
columns: z.array(
z.object({
key: z.string(),
header: z.string(),
format: z.enum(["text", "currency", "date", "badge"]).optional(),
})
),
data: z.array(z.record(z.string(), z.unknown())),
onRowClick: ActionSchema.optional(),
}),
})
export const CardSchema = z.object({
type: z.literal("Card"),
props: z.object({
title: z.string(),
description: z.string().optional(),
children: z.array(z.unknown()).optional(),
footer: z.string().optional(),
}),
})
export const BadgeSchema = z.object({
type: z.literal("Badge"),
props: z.object({
label: z.string(),
variant: z
.enum(["default", "secondary", "destructive", "outline"])
.optional(),
}),
})
export const StatCardSchema = z.object({
type: z.literal("StatCard"),
props: z.object({
title: z.string(),
value: z.union([z.string(), z.number()]),
change: z.number().optional(),
changeLabel: z.string().optional(),
icon: z.string().optional(),
}),
})
// Action components
export const ButtonSchema = z.object({
type: z.literal("Button"),
props: z.object({
label: z.string(),
action: ActionSchema,
variant: z
.enum(["default", "secondary", "destructive", "outline", "ghost", "link"])
.optional(),
size: z.enum(["default", "sm", "lg", "icon"]).optional(),
}),
})
export const ButtonGroupSchema = z.object({
type: z.literal("ButtonGroup"),
props: z.object({
buttons: z.array(ButtonSchema.shape.props),
}),
})
// Chart components
export const BarChartSchema = z.object({
type: z.literal("BarChart"),
props: z.object({
data: z.array(z.record(z.string(), z.union([z.string(), z.number()]))),
xKey: z.string(),
yKey: z.string(),
height: z.number().optional(),
}),
})
export const LineChartSchema = z.object({
type: z.literal("LineChart"),
props: z.object({
data: z.array(z.record(z.string(), z.union([z.string(), z.number()]))),
xKey: z.string(),
yKey: z.string(),
height: z.number().optional(),
}),
})
export const PieChartSchema = z.object({
type: z.literal("PieChart"),
props: z.object({
data: z.array(
z.object({
name: z.string(),
value: z.number(),
color: z.string().optional(),
})
),
height: z.number().optional(),
}),
})
// Domain-specific components
export const InvoiceTableSchema = z.object({
type: z.literal("InvoiceTable"),
props: z.object({
invoices: z.array(
z.object({
id: z.string(),
number: z.string(),
customer: z.string(),
amount: z.number(),
dueDate: z.string(),
status: z.enum(["draft", "sent", "paid", "overdue"]),
})
),
onRowClick: ActionSchema.optional(),
}),
})
export const CustomerCardSchema = z.object({
type: z.literal("CustomerCard"),
props: z.object({
customer: z.object({
id: z.string(),
name: z.string(),
company: z.string().optional(),
email: z.string().optional(),
phone: z.string().optional(),
}),
actions: z.array(ActionSchema).optional(),
}),
})
export const VendorCardSchema = z.object({
type: z.literal("VendorCard"),
props: z.object({
vendor: z.object({
id: z.string(),
name: z.string(),
category: z.string(),
email: z.string().optional(),
phone: z.string().optional(),
}),
actions: z.array(ActionSchema).optional(),
}),
})
export const SchedulePreviewSchema = z.object({
type: z.literal("SchedulePreview"),
props: z.object({
projectId: z.string(),
projectName: z.string(),
tasks: z.array(
z.object({
id: z.string(),
title: z.string(),
startDate: z.string(),
endDate: z.string(),
phase: z.string(),
status: z.string(),
percentComplete: z.number(),
isCriticalPath: z.boolean().optional(),
})
),
onTaskClick: ActionSchema.optional(),
}),
})
export const ProjectSummarySchema = z.object({
type: z.literal("ProjectSummary"),
props: z.object({
project: z.object({
id: z.string(),
name: z.string(),
status: z.string(),
address: z.string().optional(),
clientName: z.string().optional(),
projectManager: z.string().optional(),
}),
stats: z
.object({
tasksTotal: z.number(),
tasksComplete: z.number(),
daysRemaining: z.number().optional(),
budgetUsed: z.number().optional(),
})
.optional(),
actions: z.array(ActionSchema).optional(),
}),
})
// Layout components
export const GridSchema = z.object({
type: z.literal("Grid"),
props: z.object({
columns: z.number().min(1).max(4),
gap: z.number().optional(),
children: z.array(z.unknown()),
}),
})
export const StackSchema = z.object({
type: z.literal("Stack"),
props: z.object({
direction: z.enum(["horizontal", "vertical"]).optional(),
gap: z.number().optional(),
children: z.array(z.unknown()),
}),
})
// Union of all component schemas
export const ComponentSchema = z.discriminatedUnion("type", [
DataTableSchema,
CardSchema,
BadgeSchema,
StatCardSchema,
ButtonSchema,
ButtonGroupSchema,
BarChartSchema,
LineChartSchema,
PieChartSchema,
InvoiceTableSchema,
CustomerCardSchema,
VendorCardSchema,
SchedulePreviewSchema,
ProjectSummarySchema,
GridSchema,
StackSchema,
])
export type ComponentSpec = z.infer<typeof ComponentSchema>
export type ComponentType = ComponentSpec["type"]
// Catalog for agent reference
export const componentCatalog = {
DataTable: {
description:
"Display tabular data with sortable columns. Best for lists of items.",
example: {
type: "DataTable",
props: {
columns: [
{ key: "name", header: "Name" },
{ key: "status", header: "Status", format: "badge" },
],
data: [{ name: "Item 1", status: "active" }],
},
},
},
Card: {
description: "A container for related information with optional title.",
example: {
type: "Card",
props: { title: "Summary", description: "Key metrics at a glance" },
},
},
Badge: {
description: "Small label for status or category indication.",
example: {
type: "Badge",
props: { label: "Active", variant: "default" },
},
},
StatCard: {
description: "Display a single metric with optional trend indicator.",
example: {
type: "StatCard",
props: { title: "Revenue", value: "$12,500", change: 12 },
},
},
Button: {
description: "Clickable action button.",
example: {
type: "Button",
props: {
label: "View Details",
action: { type: "NAVIGATE_TO", payload: { path: "/details" } },
},
},
},
BarChart: {
description: "Vertical bar chart for comparing values.",
example: {
type: "BarChart",
props: {
data: [
{ month: "Jan", sales: 100 },
{ month: "Feb", sales: 150 },
],
xKey: "month",
yKey: "sales",
},
},
},
InvoiceTable: {
description: "Specialized table for displaying invoices with status.",
example: {
type: "InvoiceTable",
props: {
invoices: [
{
id: "1",
number: "INV-001",
customer: "Acme",
amount: 5000,
dueDate: "2024-01-15",
status: "overdue",
},
],
},
},
},
CustomerCard: {
description: "Display customer information in a card format.",
example: {
type: "CustomerCard",
props: {
customer: { id: "1", name: "John Doe", company: "Acme Corp" },
},
},
},
SchedulePreview: {
description: "Preview of project schedule tasks.",
example: {
type: "SchedulePreview",
props: {
projectId: "1",
projectName: "Highland Park",
tasks: [
{
id: "t1",
title: "Foundation",
startDate: "2024-01-01",
endDate: "2024-01-15",
phase: "Foundation",
status: "complete",
percentComplete: 100,
},
],
},
},
},
ProjectSummary: {
description: "Overview card for a project with key stats.",
example: {
type: "ProjectSummary",
props: {
project: { id: "1", name: "Highland Park", status: "OPEN" },
stats: { tasksTotal: 24, tasksComplete: 12 },
},
},
},
Grid: {
description: "Grid layout for arranging multiple components.",
example: {
type: "Grid",
props: { columns: 2, children: [] },
},
},
Stack: {
description: "Stack layout for vertical or horizontal arrangement.",
example: {
type: "Stack",
props: { direction: "vertical", children: [] },
},
},
} as const
export type CatalogKey = keyof typeof componentCatalog
// Helper to generate component catalog description for agent prompts
export function getComponentCatalogPrompt(): string {
return Object.entries(componentCatalog)
.map(([name, info]) => `- ${name}: ${info.description}`)
.join("\n")
}

View File

@ -43,6 +43,6 @@
}
],
"vars": {
"WORKOS_REDIRECT_URI": "https://compass.openrangeconstruction.ltd/api/auth/callback"
"WORKOS_REDIRECT_URI": "https://compass.openrangeconstruction.ltd/callback"
}
}