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:
parent
a0dd50f59b
commit
a0f7852845
3
.gitignore
vendored
3
.gitignore
vendored
@ -26,4 +26,7 @@ dist/
|
||||
.playwright-mcp
|
||||
mobile-ui-references/
|
||||
.fuse_*
|
||||
|
||||
# directories
|
||||
tmp/
|
||||
references/
|
||||
|
||||
304
bun.lock
304
bun.lock
@ -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
2725
docs/spec.json
Executable file
File diff suppressed because it is too large
Load Diff
21
drizzle/0008_superb_lifeguard.sql
Executable file
21
drizzle/0008_superb_lifeguard.sql
Executable 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
2324
drizzle/meta/0008_snapshot.json
Executable file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
12
package.json
12
package.json
@ -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",
|
||||
|
||||
@ -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
196
src/app/api/agent/route.ts
Executable 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
})
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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} />
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}, [])
|
||||
|
||||
27
src/app/dashboard/vendors/page.tsx
vendored
27
src/app/dashboard/vendors/page.tsx
vendored
@ -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>
|
||||
|
||||
50
src/components/agent/agent-provider.tsx
Executable file
50
src/components/agent/agent-provider.tsx
Executable 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)
|
||||
}
|
||||
303
src/components/agent/chat-panel.tsx
Executable file
303
src/components/agent/chat-panel.tsx
Executable 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
|
||||
622
src/components/agent/dynamic-ui.tsx
Executable file
622
src/components/agent/dynamic-ui.tsx
Executable 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
1297
src/components/ai/prompt-input.tsx
Executable file
File diff suppressed because it is too large
Load Diff
@ -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 },
|
||||
]
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
570
src/components/dashboard-chat.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
180
src/components/dashboard-context-menu.tsx
Executable file
180
src/components/dashboard-context-menu.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
@ -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">
|
||||
|
||||
56
src/components/page-actions-provider.tsx
Executable file
56
src/components/page-actions-provider.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
@ -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">⌘</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>⌘</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>
|
||||
|
||||
198
src/components/ui/audio-visualizer.tsx
Executable file
198
src/components/ui/audio-visualizer.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
405
src/components/ui/chat-message.tsx
Executable file
405
src/components/ui/chat-message.tsx
Executable 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
310
src/components/ui/chat.tsx
Executable 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"
|
||||
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
|
||||
44
src/components/ui/copy-button.tsx
Executable file
44
src/components/ui/copy-button.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
153
src/components/ui/file-preview.tsx
Executable file
153
src/components/ui/file-preview.tsx
Executable 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
170
src/components/ui/input-group.tsx
Executable 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,
|
||||
}
|
||||
41
src/components/ui/interrupt-prompt.tsx
Executable file
41
src/components/ui/interrupt-prompt.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
237
src/components/ui/markdown-renderer.tsx
Executable file
237
src/components/ui/markdown-renderer.tsx
Executable 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
|
||||
429
src/components/ui/message-input.tsx
Executable file
429
src/components/ui/message-input.tsx
Executable 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
|
||||
}
|
||||
45
src/components/ui/message-list.tsx
Executable file
45
src/components/ui/message-list.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
38
src/components/ui/prompt-suggestions.tsx
Executable file
38
src/components/ui/prompt-suggestions.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
15
src/components/ui/typing-indicator.tsx
Executable file
15
src/components/ui/typing-indicator.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
93
src/hooks/use-audio-recording.ts
Executable file
93
src/hooks/use-audio-recording.ts
Executable 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
73
src/hooks/use-auto-scroll.ts
Executable 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,
|
||||
}
|
||||
}
|
||||
39
src/hooks/use-autosize-textarea.ts
Executable file
39
src/hooks/use-autosize-textarea.ts
Executable 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])
|
||||
}
|
||||
36
src/hooks/use-copy-to-clipboard.ts
Executable file
36
src/hooks/use-copy-to-clipboard.ts
Executable 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 }
|
||||
}
|
||||
23
src/hooks/use-register-page-actions.ts
Executable file
23
src/hooks/use-register-page-actions.ts
Executable 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
50
src/lib/audio-utils.ts
Executable 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
340
src/lib/eliza/chat-adapter.ts
Executable 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
|
||||
393
src/lib/eliza/json-render/catalog.ts
Executable file
393
src/lib/eliza/json-render/catalog.ts
Executable 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")
|
||||
}
|
||||
@ -43,6 +43,6 @@
|
||||
}
|
||||
],
|
||||
"vars": {
|
||||
"WORKOS_REDIRECT_URI": "https://compass.openrangeconstruction.ltd/api/auth/callback"
|
||||
"WORKOS_REDIRECT_URI": "https://compass.openrangeconstruction.ltd/callback"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user