From a0f7852845be0f699185cdfeda6f618f2dd5edca Mon Sep 17 00:00:00 2001 From: Nicholai Date: Thu, 5 Feb 2026 15:56:06 -0700 Subject: [PATCH] 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 --- .gitignore | 3 + bun.lock | 304 ++- docs/spec.json | 2725 +++++++++++++++++++++ drizzle/0008_superb_lifeguard.sql | 21 + drizzle/meta/0008_snapshot.json | 2324 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/seed-users.sql | 9 +- drizzle/seed.sql | 10 + next.config.ts | 16 +- package.json | 12 +- src/app/(auth)/layout.tsx | 8 +- src/app/api/agent/route.ts | 196 ++ src/app/api/auth/login/route.ts | 5 +- src/app/api/auth/sso/route.ts | 8 +- src/app/dashboard/customers/page.tsx | 27 +- src/app/dashboard/financials/page.tsx | 94 +- src/app/dashboard/layout.tsx | 19 +- src/app/dashboard/page.tsx | 263 +- src/app/dashboard/people/page.tsx | 15 + src/app/dashboard/vendors/page.tsx | 27 +- src/components/agent/agent-provider.tsx | 50 + src/components/agent/chat-panel.tsx | 303 +++ src/components/agent/dynamic-ui.tsx | 622 +++++ src/components/ai/prompt-input.tsx | 1297 ++++++++++ src/components/app-sidebar.tsx | 4 + src/components/command-menu-provider.tsx | 33 +- src/components/command-menu.tsx | 28 +- src/components/dashboard-chat.tsx | 570 +++++ src/components/dashboard-context-menu.tsx | 180 ++ src/components/feedback-widget.tsx | 9 +- src/components/page-actions-provider.tsx | 56 + src/components/site-header.tsx | 90 +- src/components/ui/audio-visualizer.tsx | 198 ++ src/components/ui/button.tsx | 6 +- src/components/ui/chat-message.tsx | 405 +++ src/components/ui/chat.tsx | 310 +++ src/components/ui/collapsible.tsx | 2 +- src/components/ui/copy-button.tsx | 44 + src/components/ui/file-preview.tsx | 153 ++ src/components/ui/input-group.tsx | 170 ++ src/components/ui/interrupt-prompt.tsx | 41 + src/components/ui/markdown-renderer.tsx | 237 ++ src/components/ui/message-input.tsx | 429 ++++ src/components/ui/message-list.tsx | 45 + src/components/ui/prompt-suggestions.tsx | 38 + src/components/ui/typing-indicator.tsx | 15 + src/db/schema.ts | 31 + src/hooks/use-audio-recording.ts | 93 + src/hooks/use-auto-scroll.ts | 73 + src/hooks/use-autosize-textarea.ts | 39 + src/hooks/use-copy-to-clipboard.ts | 36 + src/hooks/use-register-page-actions.ts | 23 + src/lib/audio-utils.ts | 50 + src/lib/eliza/chat-adapter.ts | 340 +++ src/lib/eliza/json-render/catalog.ts | 393 +++ wrangler.jsonc | 2 +- 56 files changed, 12152 insertions(+), 356 deletions(-) create mode 100755 docs/spec.json create mode 100755 drizzle/0008_superb_lifeguard.sql create mode 100755 drizzle/meta/0008_snapshot.json create mode 100755 src/app/api/agent/route.ts create mode 100755 src/components/agent/agent-provider.tsx create mode 100755 src/components/agent/chat-panel.tsx create mode 100755 src/components/agent/dynamic-ui.tsx create mode 100755 src/components/ai/prompt-input.tsx create mode 100755 src/components/dashboard-chat.tsx create mode 100755 src/components/dashboard-context-menu.tsx create mode 100755 src/components/page-actions-provider.tsx create mode 100755 src/components/ui/audio-visualizer.tsx create mode 100755 src/components/ui/chat-message.tsx create mode 100755 src/components/ui/chat.tsx create mode 100755 src/components/ui/copy-button.tsx create mode 100755 src/components/ui/file-preview.tsx create mode 100755 src/components/ui/input-group.tsx create mode 100755 src/components/ui/interrupt-prompt.tsx create mode 100755 src/components/ui/markdown-renderer.tsx create mode 100755 src/components/ui/message-input.tsx create mode 100755 src/components/ui/message-list.tsx create mode 100755 src/components/ui/prompt-suggestions.tsx create mode 100755 src/components/ui/typing-indicator.tsx create mode 100755 src/hooks/use-audio-recording.ts create mode 100755 src/hooks/use-auto-scroll.ts create mode 100755 src/hooks/use-autosize-textarea.ts create mode 100755 src/hooks/use-copy-to-clipboard.ts create mode 100755 src/hooks/use-register-page-actions.ts create mode 100755 src/lib/audio-utils.ts create mode 100755 src/lib/eliza/chat-adapter.ts create mode 100755 src/lib/eliza/json-render/catalog.ts diff --git a/.gitignore b/.gitignore index 7c117c5..ef6a85c 100755 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ dist/ .playwright-mcp mobile-ui-references/ .fuse_* + +# directories tmp/ +references/ diff --git a/bun.lock b/bun.lock index bb8ae1a..5b1c2d6 100755 --- a/bun.lock +++ b/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=="], diff --git a/docs/spec.json b/docs/spec.json new file mode 100755 index 0000000..f6207ee --- /dev/null +++ b/docs/spec.json @@ -0,0 +1,2725 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "meta": { + "title": "COMPASS Phase One Implementation Specification", + "version": "1.0.0", + "created": "2026-02-05", + "lastUpdated": "2026-02-05", + "status": "approved", + "client": "High-Performance Structures (HPS)", + "projectGoal": "Replace Buildertrend with COMPASS - Phase One delivers critical features to enable migration", + "totalEstimatedWeeks": 10, + "techStack": { + "framework": "Next.js 15.5 with App Router", + "runtime": "Cloudflare Workers via @opennextjs/cloudflare", + "database": "Cloudflare D1 (SQLite) with Drizzle ORM", + "auth": "WorkOS AuthKit", + "email": "Resend", + "storage": "Google Drive API v3 (service account)", + "offline": "Workbox + Dexie.js (IndexedDB)", + "ui": "shadcn/ui + Tailwind CSS v4 + React 19", + "validation": "Zod 4.x", + "forms": "React Hook Form 7.x" + } + }, + "features": [ + { + "id": "F001", + "name": "Three-Tier User System", + "priority": "P0", + "status": "planned", + "estimatedDays": 8, + "sprint": 1, + "dependencies": [], + "blocksFeatures": ["F004", "F005", "F006"], + "description": "Extend the user management system to support three distinct user tiers: Internal Users, Subcontractors/Suppliers, and Clients. Each tier has different access levels, capabilities, and organizational structures.", + "businessValue": "Enables proper access control and role-based routing for notifications, bid packages, and schedule items. Critical for multi-stakeholder project management.", + "userStories": [ + { + "id": "US001-1", + "role": "Admin", + "action": "create and manage internal users with specific roles", + "benefit": "I can assign appropriate permissions to office staff, field workers, and other internal team members", + "acceptanceCriteria": [ + "Admin can create new internal users with roles: super_admin, office_admin, field_admin, field", + "Admin can edit user roles and deactivate users", + "Role changes take effect immediately", + "Audit log captures all user management actions" + ] + }, + { + "id": "US001-2", + "role": "Admin", + "action": "manage subcontractor companies with multiple contacts", + "benefit": "I can organize vendor contacts by their function and route communications appropriately", + "acceptanceCriteria": [ + "Admin can add multiple contacts per vendor company", + "Each contact has a functional role: estimator, scheduler, billing, or sales", + "Contacts can optionally be given login accounts", + "Contact list is searchable and filterable by role" + ] + }, + { + "id": "US001-3", + "role": "Admin", + "action": "add multiple client accounts to a single project", + "benefit": "all stakeholders on the client side can access project information with appropriate permissions", + "acceptanceCriteria": [ + "Multiple client users can be assigned to one project", + "Each client user has their own login credentials", + "Client users only see projects they are assigned to", + "Client permissions are read-only by default" + ] + }, + { + "id": "US001-4", + "role": "Subcontractor", + "action": "log in and view my assigned tasks and bid packages", + "benefit": "I can stay informed about upcoming work and respond to bid requests", + "acceptanceCriteria": [ + "Subcontractor users see only projects they are assigned to", + "Estimators see bid packages, schedulers see schedule items", + "Subcontractors can update task status for their assigned work", + "Subcontractors can submit daily logs" + ] + } + ], + "technicalApproach": { + "overview": "Extend existing users table with userType discriminator. Create new vendor_contacts table for multi-contact vendor management. Leverage existing projectMembers table for client-project assignments.", + "schemaChanges": [ + { + "table": "users", + "operation": "ALTER", + "changes": [ + { + "column": "user_type", + "type": "TEXT", + "default": "internal", + "values": ["internal", "subcontractor", "client"], + "description": "Discriminator for three-tier user system" + } + ] + }, + { + "table": "vendor_contacts", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "vendor_id", "type": "TEXT", "references": "vendors.id", "onDelete": "CASCADE" }, + { "name": "name", "type": "TEXT", "notNull": true }, + { "name": "email", "type": "TEXT" }, + { "name": "phone", "type": "TEXT" }, + { "name": "functional_role", "type": "TEXT", "notNull": true, "values": ["estimator", "scheduler", "billing", "sales"] }, + { "name": "is_primary", "type": "BOOLEAN", "default": false }, + { "name": "has_login_account", "type": "BOOLEAN", "default": false }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "nullable": true }, + { "name": "notes", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_vendor_contacts_vendor", "columns": ["vendor_id"] }, + { "name": "idx_vendor_contacts_role", "columns": ["functional_role"] }, + { "name": "idx_vendor_contacts_user", "columns": ["user_id"] } + ] + }, + { + "table": "user_invitations", + "operation": "ALTER", + "changes": [ + { + "column": "user_type", + "type": "TEXT", + "default": "internal", + "description": "Specifies what type of user is being invited" + }, + { + "column": "vendor_contact_id", + "type": "TEXT", + "references": "vendor_contacts.id", + "nullable": true, + "description": "Links invitation to vendor contact if inviting a subcontractor" + } + ] + } + ], + "permissionsChanges": { + "file": "src/lib/permissions.ts", + "changes": [ + "Add userType-aware permission checks", + "Subcontractor role with limited project access", + "Client role inherits from existing client permissions", + "Role-based resource filtering (estimators see bid packages, schedulers see schedule)" + ] + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/vendors/[id]/contacts", + "description": "List all contacts for a vendor", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/vendors/[id]/contacts", + "description": "Create a new vendor contact", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/vendor-contacts/[id]", + "description": "Update a vendor contact", + "auth": "office, admin" + }, + { + "method": "DELETE", + "path": "/api/vendor-contacts/[id]", + "description": "Delete a vendor contact", + "auth": "admin" + }, + { + "method": "POST", + "path": "/api/vendor-contacts/[id]/invite", + "description": "Send login invitation to vendor contact", + "auth": "admin" + } + ], + "components": [ + { + "name": "VendorContactsTable", + "path": "src/components/vendors/vendor-contacts-table.tsx", + "description": "Data table showing all contacts for a vendor with role badges" + }, + { + "name": "VendorContactDialog", + "path": "src/components/vendors/vendor-contact-dialog.tsx", + "description": "Modal for creating/editing vendor contacts" + }, + { + "name": "InviteSubcontractorDialog", + "path": "src/components/vendors/invite-subcontractor-dialog.tsx", + "description": "Modal for inviting vendor contact as a user" + }, + { + "name": "UserTypeFilter", + "path": "src/components/people/user-type-filter.tsx", + "description": "Filter component for people list by user type" + } + ] + }, + "testCases": [ + { + "id": "TC001-1", + "type": "unit", + "description": "Permission check correctly identifies user type", + "steps": ["Create users of each type", "Verify permission checks return correct values"] + }, + { + "id": "TC001-2", + "type": "integration", + "description": "Vendor contact CRUD operations work correctly", + "steps": ["Create vendor contact", "Update contact role", "Delete contact", "Verify cascade behavior"] + }, + { + "id": "TC001-3", + "type": "e2e", + "description": "Subcontractor invitation flow", + "steps": ["Admin creates vendor contact", "Admin sends invitation", "Contact receives email", "Contact creates account", "Contact logs in and sees limited view"] + } + ] + }, + { + "id": "F002", + "name": "PWA Infrastructure and Offline Foundation", + "priority": "P0", + "status": "planned", + "estimatedDays": 10, + "sprint": 1, + "dependencies": [], + "blocksFeatures": ["F003"], + "description": "Establish Progressive Web App infrastructure including service worker, IndexedDB schema, and offline detection. This foundation enables full offline CRUD capabilities for the application.", + "businessValue": "Field workers need to access and update project information from job sites with poor or no connectivity. Offline support is critical for daily operations and was cited as a major Buildertrend limitation.", + "userStories": [ + { + "id": "US002-1", + "role": "Field Worker", + "action": "install COMPASS as an app on my phone", + "benefit": "I can access it quickly without opening a browser", + "acceptanceCriteria": [ + "Web app manifest enables 'Add to Home Screen' prompt", + "App icon and splash screen display correctly", + "App launches in standalone mode without browser chrome", + "App works on iOS Safari and Android Chrome" + ] + }, + { + "id": "US002-2", + "role": "Field Worker", + "action": "see a clear indicator when I'm offline", + "benefit": "I know whether my changes are being saved locally or synced to the server", + "acceptanceCriteria": [ + "Offline indicator appears in header when connection lost", + "Indicator shows number of pending changes", + "Toast notification when connection restored", + "Sync progress indicator during background sync" + ] + }, + { + "id": "US002-3", + "role": "Field Worker", + "action": "continue working when I lose internet connection", + "benefit": "my work isn't interrupted by poor cell coverage on job sites", + "acceptanceCriteria": [ + "All previously loaded data available offline", + "Can create new daily logs offline", + "Can update task status offline", + "Changes sync automatically when online" + ] + }, + { + "id": "US002-4", + "role": "System", + "action": "resolve conflicts when the same record is edited online and offline", + "benefit": "data integrity is maintained without user intervention", + "acceptanceCriteria": [ + "Last-write-wins strategy for simple conflicts", + "User prompted for manual resolution on complex conflicts", + "Conflict history logged for audit", + "No data loss during conflict resolution" + ] + } + ], + "technicalApproach": { + "overview": "Implement PWA using Workbox for service worker management and Dexie.js for IndexedDB abstraction. Create a sync queue system that tracks pending changes and reconciles with server on reconnection.", + "serviceWorker": { + "tool": "Workbox 7.x via next-pwa or manual configuration", + "strategies": [ + { + "route": "/api/*", + "strategy": "NetworkFirst", + "fallback": "cached response or offline indicator" + }, + { + "route": "/_next/static/*", + "strategy": "CacheFirst", + "expiration": "30 days" + }, + { + "route": "/dashboard/*", + "strategy": "StaleWhileRevalidate", + "description": "Serve cached page immediately, update in background" + } + ], + "backgroundSync": { + "queueName": "compass-sync-queue", + "maxRetries": 3, + "retryDelay": "exponential backoff starting at 5 minutes" + } + }, + "indexedDBSchema": { + "library": "Dexie.js 4.x", + "databaseName": "compass-offline", + "version": 1, + "stores": [ + { + "name": "projects", + "keyPath": "id", + "indexes": ["status", "updatedAt", "syncStatus"], + "description": "Cached project records" + }, + { + "name": "scheduleTasks", + "keyPath": "id", + "indexes": ["projectId", "status", "syncStatus"], + "description": "Cached schedule tasks" + }, + { + "name": "dailyLogs", + "keyPath": "id", + "indexes": ["projectId", "date", "syncStatus"], + "description": "Daily logs including offline-created ones" + }, + { + "name": "syncQueue", + "keyPath": "id", + "indexes": ["entityType", "entityId", "createdAt", "status"], + "description": "Queue of pending sync operations" + }, + { + "name": "syncConflicts", + "keyPath": "id", + "indexes": ["entityType", "entityId", "resolvedAt"], + "description": "Conflicts requiring manual resolution" + }, + { + "name": "offlinePhotos", + "keyPath": "id", + "indexes": ["dailyLogId", "uploadStatus"], + "description": "Photos captured offline pending upload" + } + ] + }, + "syncQueue": { + "operations": ["CREATE", "UPDATE", "DELETE"], + "queueItem": { + "id": "uuid", + "entityType": "dailyLog | scheduleTask | ...", + "entityId": "string", + "operation": "CREATE | UPDATE | DELETE", + "payload": "JSON serialized entity", + "createdAt": "ISO timestamp", + "attemptCount": "number", + "lastAttemptAt": "ISO timestamp | null", + "status": "pending | syncing | synced | failed | conflict", + "errorMessage": "string | null" + }, + "syncProcess": [ + "1. Detect online status change", + "2. Fetch pending queue items ordered by createdAt", + "3. For each item, attempt server sync", + "4. On success: mark synced, update local record with server response", + "5. On 409 Conflict: move to conflicts table for resolution", + "6. On network error: increment attemptCount, retry with backoff", + "7. Emit sync completion event for UI update" + ] + }, + "conflictResolution": { + "strategy": "last-write-wins with manual override option", + "autoResolve": [ + "Server version newer and local version unchanged since fetch", + "Local version is CREATE and server has no record" + ], + "manualResolve": [ + "Both local and server modified since last sync", + "DELETE conflicts (local deleted, server modified)" + ], + "conflictUI": { + "component": "SyncConflictDialog", + "showDiff": true, + "options": ["Keep mine", "Keep server", "Merge (manual edit)"] + } + }, + "files": [ + { + "path": "src/lib/offline/db.ts", + "description": "Dexie database instance and schema definition" + }, + { + "path": "src/lib/offline/sync-queue.ts", + "description": "Sync queue management functions" + }, + { + "path": "src/lib/offline/sync-service.ts", + "description": "Background sync orchestration" + }, + { + "path": "src/lib/offline/conflict-resolver.ts", + "description": "Conflict detection and resolution logic" + }, + { + "path": "src/hooks/use-online-status.ts", + "description": "React hook for online/offline detection" + }, + { + "path": "src/hooks/use-sync-status.ts", + "description": "React hook for sync queue status" + }, + { + "path": "src/components/offline/offline-indicator.tsx", + "description": "Header component showing offline status" + }, + { + "path": "src/components/offline/sync-status-badge.tsx", + "description": "Badge showing sync status for individual records" + }, + { + "path": "src/components/offline/sync-conflict-dialog.tsx", + "description": "Modal for manual conflict resolution" + }, + { + "path": "public/manifest.json", + "description": "PWA manifest file" + }, + { + "path": "src/app/sw.ts", + "description": "Service worker entry point" + } + ], + "manifest": { + "name": "COMPASS", + "short_name": "COMPASS", + "description": "Construction Project Management System", + "start_url": "/dashboard", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] + } + }, + "testCases": [ + { + "id": "TC002-1", + "type": "unit", + "description": "Sync queue correctly orders operations", + "steps": ["Add multiple operations", "Verify FIFO ordering", "Verify operation deduplication"] + }, + { + "id": "TC002-2", + "type": "unit", + "description": "Conflict detection identifies all conflict types", + "steps": ["Create conflicting local/server states", "Verify conflict detection", "Verify conflict categorization"] + }, + { + "id": "TC002-3", + "type": "integration", + "description": "Background sync processes queue on reconnection", + "steps": ["Queue operations while offline", "Simulate reconnection", "Verify all operations synced"] + }, + { + "id": "TC002-4", + "type": "e2e", + "description": "Full offline workflow", + "steps": ["Load app online", "Go offline", "Create/edit records", "Go online", "Verify sync completes"] + } + ] + }, + { + "id": "F003", + "name": "Daily Logging System", + "priority": "P0", + "status": "planned", + "estimatedDays": 8, + "sprint": 2, + "dependencies": ["F002"], + "blocksFeatures": [], + "description": "Complete daily logging system allowing field workers, office staff, and subcontractors to create detailed daily logs from any device. Supports offline creation with photo attachments and automatic weather data.", + "businessValue": "Daily logs are essential for project documentation, liability protection, and client communication. Field workers need to log from job sites with unreliable connectivity. This directly replaces Buildertrend's daily log feature.", + "userStories": [ + { + "id": "US003-1", + "role": "Field Worker", + "action": "create a daily log from my phone while on site", + "benefit": "I can document work completed, issues, and conditions while the details are fresh", + "acceptanceCriteria": [ + "Mobile-optimized form with large touch targets", + "Can create log while offline", + "Log saved locally and synced when online", + "Confirmation shown when log saved" + ] + }, + { + "id": "US003-2", + "role": "Field Worker", + "action": "attach photos to my daily log", + "benefit": "I can provide visual documentation of work progress and issues", + "acceptanceCriteria": [ + "Can capture photos directly from camera", + "Can select existing photos from gallery", + "Multiple photos per log supported", + "Photos compressed for upload", + "Photos queue for upload when offline" + ] + }, + { + "id": "US003-3", + "role": "Field Worker", + "action": "have weather automatically filled in", + "benefit": "I save time and ensure accurate weather documentation", + "acceptanceCriteria": [ + "Weather fetched based on project location when online", + "Temperature, conditions, and precipitation displayed", + "Weather can be manually overridden", + "Weather data cached for offline use" + ] + }, + { + "id": "US003-4", + "role": "PM", + "action": "view all daily logs for a project in chronological order", + "benefit": "I can review project progress and identify issues", + "acceptanceCriteria": [ + "Timeline view showing all logs", + "Filter by date range, author, or tags", + "Photos displayed inline", + "Export to PDF option" + ] + }, + { + "id": "US003-5", + "role": "Subcontractor", + "action": "submit a daily log for work my crew performed", + "benefit": "I can document our work and hours for billing and coordination", + "acceptanceCriteria": [ + "Subcontractor logs tagged with company name", + "Can log crew members present and hours", + "Logs visible to office staff and admins", + "Option to share specific logs with client" + ] + } + ], + "technicalApproach": { + "overview": "Build on PWA infrastructure to provide full offline daily log creation. Use IndexedDB for local storage, queue photos for background upload, and fetch weather from OpenWeather API.", + "schemaChanges": [ + { + "table": "daily_logs", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "author_id", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "log_date", "type": "TEXT", "notNull": true, "description": "Date of the log (YYYY-MM-DD)" }, + { "name": "weather_temp_f", "type": "INTEGER", "description": "Temperature in Fahrenheit" }, + { "name": "weather_conditions", "type": "TEXT", "description": "e.g., Sunny, Cloudy, Rain" }, + { "name": "weather_precipitation", "type": "TEXT", "description": "e.g., None, Light Rain, Heavy Rain" }, + { "name": "weather_source", "type": "TEXT", "default": "auto", "values": ["auto", "manual"] }, + { "name": "work_completed", "type": "TEXT", "notNull": true, "description": "Description of work done" }, + { "name": "issues", "type": "TEXT", "description": "Any issues or problems encountered" }, + { "name": "materials_used", "type": "TEXT", "description": "JSON array of materials" }, + { "name": "crew_present", "type": "TEXT", "description": "JSON array of crew member names/counts" }, + { "name": "hours_worked", "type": "REAL", "description": "Total crew hours" }, + { "name": "safety_incidents", "type": "TEXT", "description": "Any safety incidents to report" }, + { "name": "visitor_log", "type": "TEXT", "description": "Visitors to job site" }, + { "name": "notes", "type": "TEXT", "description": "Additional notes" }, + { "name": "is_client_visible", "type": "BOOLEAN", "default": false }, + { "name": "tags", "type": "TEXT", "description": "JSON array of tags" }, + { "name": "sync_status", "type": "TEXT", "default": "synced", "values": ["synced", "pending", "conflict"] }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_daily_logs_project_date", "columns": ["project_id", "log_date"] }, + { "name": "idx_daily_logs_author", "columns": ["author_id"] }, + { "name": "idx_daily_logs_sync", "columns": ["sync_status"] } + ] + }, + { + "table": "daily_log_photos", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "daily_log_id", "type": "TEXT", "references": "daily_logs.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "file_name", "type": "TEXT", "notNull": true }, + { "name": "file_size", "type": "INTEGER" }, + { "name": "mime_type", "type": "TEXT" }, + { "name": "drive_file_id", "type": "TEXT", "description": "Google Drive file ID once uploaded" }, + { "name": "drive_url", "type": "TEXT", "description": "Google Drive view URL" }, + { "name": "thumbnail_url", "type": "TEXT" }, + { "name": "caption", "type": "TEXT" }, + { "name": "upload_status", "type": "TEXT", "default": "pending", "values": ["pending", "uploading", "uploaded", "failed"] }, + { "name": "sort_order", "type": "INTEGER", "default": 0 }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_daily_log_photos_log", "columns": ["daily_log_id"] }, + { "name": "idx_daily_log_photos_upload", "columns": ["upload_status"] } + ] + }, + { + "table": "daily_log_task_links", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "daily_log_id", "type": "TEXT", "references": "daily_logs.id", "onDelete": "CASCADE" }, + { "name": "schedule_task_id", "type": "TEXT", "references": "schedule_tasks.id", "onDelete": "CASCADE" }, + { "name": "notes", "type": "TEXT" } + ], + "description": "Links daily logs to specific schedule tasks worked on" + } + ], + "weatherIntegration": { + "provider": "OpenWeather API", + "endpoint": "https://api.openweathermap.org/data/2.5/weather", + "parameters": ["lat", "lon", "units=imperial", "appid"], + "caching": "Cache weather by project location for 1 hour", + "fallback": "Manual entry when offline or API unavailable" + }, + "photoHandling": { + "capture": "Use native file input with capture=camera on mobile", + "compression": "Compress to max 1920px width, 80% JPEG quality", + "localStorage": "Store blob in IndexedDB while offline", + "upload": "Background upload to Google Drive Daily Logs folder", + "thumbnail": "Generate 200px thumbnail for list views" + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/projects/[id]/daily-logs", + "description": "List daily logs for a project", + "params": ["startDate", "endDate", "authorId"], + "auth": "field, office, admin, client (if visible)" + }, + { + "method": "GET", + "path": "/api/daily-logs/[id]", + "description": "Get single daily log with photos", + "auth": "field, office, admin, client (if visible)" + }, + { + "method": "POST", + "path": "/api/projects/[id]/daily-logs", + "description": "Create a new daily log", + "auth": "field, office, admin, subcontractor" + }, + { + "method": "PUT", + "path": "/api/daily-logs/[id]", + "description": "Update a daily log", + "auth": "author, office, admin" + }, + { + "method": "DELETE", + "path": "/api/daily-logs/[id]", + "description": "Delete a daily log", + "auth": "admin" + }, + { + "method": "POST", + "path": "/api/daily-logs/[id]/photos", + "description": "Upload photo to daily log", + "auth": "field, office, admin, subcontractor" + }, + { + "method": "GET", + "path": "/api/weather", + "description": "Get current weather for location", + "params": ["lat", "lon"], + "auth": "any authenticated" + } + ], + "components": [ + { + "name": "DailyLogForm", + "path": "src/components/daily-logs/daily-log-form.tsx", + "description": "Mobile-optimized form for creating/editing daily logs" + }, + { + "name": "DailyLogTimeline", + "path": "src/components/daily-logs/daily-log-timeline.tsx", + "description": "Chronological list of daily logs for a project" + }, + { + "name": "DailyLogCard", + "path": "src/components/daily-logs/daily-log-card.tsx", + "description": "Card component displaying log summary" + }, + { + "name": "DailyLogDetail", + "path": "src/components/daily-logs/daily-log-detail.tsx", + "description": "Full detail view of a daily log" + }, + { + "name": "PhotoCapture", + "path": "src/components/daily-logs/photo-capture.tsx", + "description": "Camera/gallery photo input with preview" + }, + { + "name": "PhotoGallery", + "path": "src/components/daily-logs/photo-gallery.tsx", + "description": "Grid of photos with lightbox viewer" + }, + { + "name": "WeatherWidget", + "path": "src/components/daily-logs/weather-widget.tsx", + "description": "Displays current weather with auto-refresh" + }, + { + "name": "TaskLinker", + "path": "src/components/daily-logs/task-linker.tsx", + "description": "Select schedule tasks worked on" + } + ], + "routes": [ + { + "path": "/dashboard/projects/[id]/daily-logs", + "description": "Daily log timeline for a project" + }, + { + "path": "/dashboard/projects/[id]/daily-logs/new", + "description": "Create new daily log" + }, + { + "path": "/dashboard/projects/[id]/daily-logs/[logId]", + "description": "View/edit single daily log" + } + ] + }, + "testCases": [ + { + "id": "TC003-1", + "type": "unit", + "description": "Daily log validation accepts valid data", + "steps": ["Submit valid log data", "Verify validation passes", "Verify log created"] + }, + { + "id": "TC003-2", + "type": "integration", + "description": "Photo upload to Google Drive works", + "steps": ["Create log with photo", "Verify photo uploaded to Drive", "Verify URL stored in database"] + }, + { + "id": "TC003-3", + "type": "integration", + "description": "Weather auto-fill from location", + "steps": ["Create log with project location", "Verify weather fetched", "Verify weather saved to log"] + }, + { + "id": "TC003-4", + "type": "e2e", + "description": "Offline daily log creation and sync", + "steps": ["Go offline", "Create daily log with photos", "Go online", "Verify log synced", "Verify photos uploaded"] + }, + { + "id": "TC003-5", + "type": "e2e", + "description": "Subcontractor can create daily log", + "steps": ["Login as subcontractor", "Navigate to assigned project", "Create daily log", "Verify log visible to PM"] + } + ] + }, + { + "id": "F004", + "name": "Google Drive Integration", + "priority": "P0", + "status": "planned", + "estimatedDays": 10, + "sprint": 1, + "dependencies": [], + "blocksFeatures": ["F003", "F005"], + "description": "Replace planned S3 storage with Google Drive integration using a service account. All project files stored in company Google Drive with automatic CSI folder structure creation.", + "businessValue": "HPS uses Google Workspace for all office apps. Integrating with their existing Google Drive eliminates the need for separate file storage and keeps all documents in familiar tools.", + "userStories": [ + { + "id": "US004-1", + "role": "PM", + "action": "browse project files within COMPASS", + "benefit": "I don't have to switch to Google Drive to find project documents", + "acceptanceCriteria": [ + "File browser shows real Drive contents", + "Can navigate folder hierarchy", + "File previews available for common types", + "Click file to open in Drive" + ] + }, + { + "id": "US004-2", + "role": "PM", + "action": "upload documents to the correct project folder", + "benefit": "files are automatically organized in the right location", + "acceptanceCriteria": [ + "Drag-and-drop upload supported", + "Can select destination folder", + "Upload progress shown", + "File appears in browser after upload" + ] + }, + { + "id": "US004-3", + "role": "System", + "action": "create CSI folder structure when a project is created", + "benefit": "all projects have consistent folder organization", + "acceptanceCriteria": [ + "50+ CSI folders created automatically", + "Folder structure matches HPS standard", + "Project folder named with project code", + "Provisioning status tracked" + ] + }, + { + "id": "US004-4", + "role": "Admin", + "action": "configure the root folder for COMPASS projects", + "benefit": "I can control where project folders are created", + "acceptanceCriteria": [ + "Settings page for Drive configuration", + "Can select root folder from Drive", + "Validation that service account has access", + "Folder ID stored in system settings" + ] + } + ], + "technicalApproach": { + "overview": "Use Google Drive API v3 with service account authentication. Service account credentials stored securely in environment variables. Existing file browser UI connected to Drive backend.", + "authentication": { + "type": "Service Account", + "setup": [ + "1. Create Google Cloud project", + "2. Enable Google Drive API", + "3. Create service account", + "4. Download JSON credentials", + "5. Share root folder with service account email", + "6. Store credentials in GOOGLE_SERVICE_ACCOUNT_KEY env var" + ], + "library": "googleapis npm package", + "scopes": ["https://www.googleapis.com/auth/drive"] + }, + "schemaChanges": [ + { + "table": "projects", + "operation": "ALTER", + "changes": [ + { + "column": "drive_folder_id", + "type": "TEXT", + "nullable": true, + "description": "Google Drive folder ID for this project" + }, + { + "column": "drive_folder_url", + "type": "TEXT", + "nullable": true, + "description": "Direct URL to project folder in Drive" + } + ] + }, + { + "table": "system_settings", + "operation": "CREATE", + "columns": [ + { "name": "key", "type": "TEXT", "primaryKey": true }, + { "name": "value", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "initialData": [ + { "key": "drive_root_folder_id", "value": "" }, + { "key": "drive_configured", "value": "false" } + ] + }, + { + "table": "documents", + "operation": "ALTER", + "changes": [ + { + "column": "drive_file_id", + "type": "TEXT", + "nullable": true, + "description": "Google Drive file ID" + }, + { + "column": "drive_url", + "type": "TEXT", + "nullable": true, + "description": "Direct link to file in Drive" + }, + { + "column": "drive_thumbnail_url", + "type": "TEXT", + "nullable": true, + "description": "Thumbnail URL for preview" + } + ] + } + ], + "driveService": { + "file": "src/lib/drive/drive-service.ts", + "methods": [ + { + "name": "listFiles", + "params": ["folderId: string", "pageToken?: string"], + "returns": "{ files: DriveFile[], nextPageToken?: string }", + "description": "List files and folders in a Drive folder" + }, + { + "name": "getFile", + "params": ["fileId: string"], + "returns": "DriveFile", + "description": "Get file metadata" + }, + { + "name": "createFolder", + "params": ["name: string", "parentId: string"], + "returns": "DriveFile", + "description": "Create a new folder" + }, + { + "name": "uploadFile", + "params": ["file: Buffer", "name: string", "mimeType: string", "parentId: string"], + "returns": "DriveFile", + "description": "Upload a file to Drive" + }, + { + "name": "deleteFile", + "params": ["fileId: string"], + "returns": "void", + "description": "Move file to trash" + }, + { + "name": "moveFile", + "params": ["fileId: string", "newParentId: string"], + "returns": "DriveFile", + "description": "Move file to different folder" + }, + { + "name": "renameFile", + "params": ["fileId: string", "newName: string"], + "returns": "DriveFile", + "description": "Rename a file" + }, + { + "name": "getDownloadUrl", + "params": ["fileId: string"], + "returns": "string", + "description": "Get direct download URL" + }, + { + "name": "createCSIFolderStructure", + "params": ["projectFolderId: string"], + "returns": "void", + "description": "Create 50+ CSI division folders" + } + ] + }, + "folderStructure": { + "source": "hps-structures/directories sample project", + "template": [ + "Architectural", + "Architectural/3D Views", + "Architectural/Archive", + "Communications", + "Contracts", + "Estimates", + "Estimates/Preconstruction Estimates", + "Meeting Notes", + "Property Information", + "Spec and Finishes", + "Spec and Finishes/Cabinetry", + "Spec and Finishes/Electrical Needs", + "Spec and Finishes/Plumbing Fixtures", + "Testing", + "Utilities", + "___BID SET", + "___BID SET/BidPackages" + ], + "note": "Full CSI structure to be defined from HPS requirements" + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/drive/folders/[folderId]", + "description": "List contents of a folder", + "params": ["pageToken"], + "auth": "field, office, admin" + }, + { + "method": "POST", + "path": "/api/drive/folders", + "description": "Create a new folder", + "body": ["name", "parentId"], + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/drive/upload", + "description": "Upload a file", + "body": ["file (multipart)", "folderId"], + "auth": "field, office, admin" + }, + { + "method": "DELETE", + "path": "/api/drive/files/[fileId]", + "description": "Delete a file", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/drive/files/[fileId]/move", + "description": "Move a file", + "body": ["newParentId"], + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/drive/files/[fileId]/rename", + "description": "Rename a file", + "body": ["newName"], + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/settings/drive", + "description": "Get Drive configuration", + "auth": "admin" + }, + { + "method": "PUT", + "path": "/api/settings/drive", + "description": "Update Drive configuration", + "body": ["rootFolderId"], + "auth": "admin" + } + ], + "componentChanges": [ + { + "component": "FileBrowser", + "path": "src/components/files/file-browser.tsx", + "changes": [ + "Replace mock data with Drive API calls", + "Add loading states", + "Handle pagination with pageToken" + ] + }, + { + "component": "FileDropZone", + "path": "src/components/files/file-drop-zone.tsx", + "changes": [ + "Connect to Drive upload endpoint", + "Show upload progress", + "Handle upload errors" + ] + }, + { + "component": "DriveSettingsForm", + "path": "src/components/settings/drive-settings-form.tsx", + "description": "New component for configuring Drive root folder" + } + ], + "provisioningJob": { + "trigger": "Project creation", + "queue": "Cloudflare Queues", + "steps": [ + "1. Create project folder in root: {PROJECT_CODE} - {PROJECT_NAME}", + "2. Create CSI folder structure from template", + "3. Update project record with drive_folder_id", + "4. Update provisioning status to complete", + "5. Send notification email" + ], + "errorHandling": "Retry up to 3 times, then mark as failed and notify admin" + } + }, + "testCases": [ + { + "id": "TC004-1", + "type": "unit", + "description": "Drive service authenticates with service account", + "steps": ["Initialize drive service", "Verify authentication succeeds", "Verify can list root folder"] + }, + { + "id": "TC004-2", + "type": "integration", + "description": "File upload creates file in Drive", + "steps": ["Upload test file", "Verify file exists in Drive", "Verify metadata correct"] + }, + { + "id": "TC004-3", + "type": "integration", + "description": "CSI folder creation completes", + "steps": ["Create project", "Trigger provisioning", "Verify all folders created", "Verify project updated"] + }, + { + "id": "TC004-4", + "type": "e2e", + "description": "File browser shows Drive contents", + "steps": ["Navigate to project files", "Verify folders displayed", "Upload new file", "Verify file appears"] + } + ] + }, + { + "id": "F005", + "name": "Bid Package System", + "priority": "P0", + "status": "planned", + "estimatedDays": 13, + "sprint": 3, + "dependencies": ["F001", "F004"], + "blocksFeatures": [], + "description": "Complete bid package system for creating and sending requests for bid to subcontractors. Includes customizable templates, auto-fill from project data, and role-based delivery ensuring estimators receive bid packages while schedulers receive only schedule items.", + "businessValue": "Streamlines the bidding process which is currently manual and time-consuming. Role-based routing ensures the right people get the right information, reducing confusion and improving response rates.", + "userStories": [ + { + "id": "US005-1", + "role": "PM", + "action": "create a bid package for a project", + "benefit": "I can solicit bids from subcontractors in a standardized format", + "acceptanceCriteria": [ + "Can select from bid package templates", + "Project data auto-fills (name, address, dates)", + "Can add scope items from schedule or manual entry", + "Can attach drawings from Google Drive", + "Can set bid due date" + ] + }, + { + "id": "US005-2", + "role": "PM", + "action": "send a bid package to selected vendors", + "benefit": "I can reach multiple potential subcontractors with one action", + "acceptanceCriteria": [ + "Can select vendors by trade/category", + "Only estimator contacts shown for selection", + "Email sent to all selected contacts", + "Delivery tracked per recipient" + ] + }, + { + "id": "US005-3", + "role": "PM", + "action": "track bid package responses", + "benefit": "I know which subs have responded and can follow up with non-responders", + "acceptanceCriteria": [ + "Response status per vendor: sent, viewed, responded, declined", + "Can record bid amounts received", + "Can add notes per vendor", + "Dashboard shows pending bids" + ] + }, + { + "id": "US005-4", + "role": "Admin", + "action": "create and manage bid package templates", + "benefit": "I can standardize our bid request format across projects", + "acceptanceCriteria": [ + "WYSIWYG template editor", + "Variable placeholders: {{project.name}}, {{project.address}}, etc.", + "Can create trade-specific templates (electrical, plumbing, etc.)", + "Templates versioned" + ] + }, + { + "id": "US005-5", + "role": "Subcontractor (Estimator)", + "action": "view bid packages sent to me", + "benefit": "I can review the scope and prepare my bid", + "acceptanceCriteria": [ + "See only bid packages sent to me", + "Can download attached drawings", + "Can mark as 'viewed'", + "Can submit bid response through portal" + ] + } + ], + "technicalApproach": { + "overview": "Build complete bid package management including templates, auto-fill, email delivery, and response tracking. Integration with vendor contacts for role-based routing. Store bid package documents in Google Drive.", + "schemaChanges": [ + { + "table": "bid_package_templates", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "name", "type": "TEXT", "notNull": true }, + { "name": "description", "type": "TEXT" }, + { "name": "trade_category", "type": "TEXT", "description": "e.g., Electrical, Plumbing, Roofing" }, + { "name": "content", "type": "TEXT", "notNull": true, "description": "HTML template with variable placeholders" }, + { "name": "default_scope_items", "type": "TEXT", "description": "JSON array of default scope items" }, + { "name": "is_active", "type": "BOOLEAN", "default": true }, + { "name": "version", "type": "INTEGER", "default": 1 }, + { "name": "created_by", "type": "TEXT", "references": "users.id" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_templates_trade", "columns": ["trade_category"] } + ] + }, + { + "table": "bid_packages", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "template_id", "type": "TEXT", "references": "bid_package_templates.id" }, + { "name": "title", "type": "TEXT", "notNull": true }, + { "name": "description", "type": "TEXT" }, + { "name": "trade_category", "type": "TEXT" }, + { "name": "bid_due_date", "type": "TEXT", "notNull": true }, + { "name": "status", "type": "TEXT", "default": "draft", "values": ["draft", "sent", "closed", "awarded"] }, + { "name": "rendered_content", "type": "TEXT", "description": "HTML with variables replaced" }, + { "name": "drive_folder_id", "type": "TEXT", "description": "Folder for bid package attachments" }, + { "name": "notes", "type": "TEXT" }, + { "name": "created_by", "type": "TEXT", "references": "users.id" }, + { "name": "sent_at", "type": "TEXT" }, + { "name": "closed_at", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_packages_project", "columns": ["project_id"] }, + { "name": "idx_bid_packages_status", "columns": ["status"] }, + { "name": "idx_bid_packages_due", "columns": ["bid_due_date"] } + ] + }, + { + "table": "bid_package_scope_items", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "bid_package_id", "type": "TEXT", "references": "bid_packages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "description", "type": "TEXT", "notNull": true }, + { "name": "quantity", "type": "TEXT" }, + { "name": "unit", "type": "TEXT" }, + { "name": "csi_code", "type": "TEXT" }, + { "name": "notes", "type": "TEXT" }, + { "name": "sort_order", "type": "INTEGER", "default": 0 } + ], + "indexes": [ + { "name": "idx_bid_scope_items_package", "columns": ["bid_package_id"] } + ] + }, + { + "table": "bid_package_attachments", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "bid_package_id", "type": "TEXT", "references": "bid_packages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "file_name", "type": "TEXT", "notNull": true }, + { "name": "drive_file_id", "type": "TEXT", "notNull": true }, + { "name": "drive_url", "type": "TEXT" }, + { "name": "file_size", "type": "INTEGER" }, + { "name": "mime_type", "type": "TEXT" }, + { "name": "sort_order", "type": "INTEGER", "default": 0 }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_attachments_package", "columns": ["bid_package_id"] } + ] + }, + { + "table": "bid_package_recipients", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "bid_package_id", "type": "TEXT", "references": "bid_packages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "vendor_id", "type": "TEXT", "references": "vendors.id", "notNull": true }, + { "name": "vendor_contact_id", "type": "TEXT", "references": "vendor_contacts.id", "notNull": true }, + { "name": "email", "type": "TEXT", "notNull": true }, + { "name": "sent_at", "type": "TEXT" }, + { "name": "viewed_at", "type": "TEXT" }, + { "name": "response_status", "type": "TEXT", "default": "pending", "values": ["pending", "sent", "viewed", "responded", "declined", "no_response"] }, + { "name": "bid_amount", "type": "REAL" }, + { "name": "response_notes", "type": "TEXT" }, + { "name": "response_date", "type": "TEXT" }, + { "name": "is_awarded", "type": "BOOLEAN", "default": false }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_recipients_package", "columns": ["bid_package_id"] }, + { "name": "idx_bid_recipients_vendor", "columns": ["vendor_id"] }, + { "name": "idx_bid_recipients_status", "columns": ["response_status"] } + ] + } + ], + "templateVariables": { + "description": "Variables that can be used in bid package templates, replaced at render time", + "variables": [ + { "key": "{{project.name}}", "source": "projects.name" }, + { "key": "{{project.code}}", "source": "projects.id or generated code" }, + { "key": "{{project.address}}", "source": "projects.address" }, + { "key": "{{project.clientName}}", "source": "projects.clientName" }, + { "key": "{{project.manager}}", "source": "projects.projectManager" }, + { "key": "{{bidPackage.title}}", "source": "bid_packages.title" }, + { "key": "{{bidPackage.dueDate}}", "source": "bid_packages.bid_due_date, formatted" }, + { "key": "{{bidPackage.scopeItems}}", "source": "Rendered list of scope items" }, + { "key": "{{company.name}}", "source": "System setting" }, + { "key": "{{company.address}}", "source": "System setting" }, + { "key": "{{company.phone}}", "source": "System setting" }, + { "key": "{{company.email}}", "source": "System setting" }, + { "key": "{{today}}", "source": "Current date, formatted" } + ] + }, + "autoFillSources": { + "description": "Data sources for auto-populating bid packages", + "sources": [ + { + "source": "Project record", + "fields": ["name", "address", "clientName", "projectManager"] + }, + { + "source": "Schedule tasks", + "fields": ["Tasks can be selected and converted to scope items"] + }, + { + "source": "Budget line items", + "fields": ["CSI items can be selected as scope"] + }, + { + "source": "Google Drive", + "fields": ["Drawings and specs from ___BID SET folder"] + } + ] + }, + "roleBasedRouting": { + "description": "Logic for ensuring correct contacts receive appropriate communications", + "rules": [ + { + "content": "Bid Package", + "targetRole": "estimator", + "logic": "Only vendor_contacts with functional_role='estimator' shown in recipient selection" + }, + { + "content": "Schedule Items", + "targetRole": "scheduler", + "logic": "Only vendor_contacts with functional_role='scheduler' receive schedule notifications" + }, + { + "content": "Invoices/Bills", + "targetRole": "billing", + "logic": "Only vendor_contacts with functional_role='billing' receive payment communications" + } + ] + }, + "emailDelivery": { + "provider": "Resend", + "template": "bid-package-invitation", + "content": { + "subject": "Request for Bid: {{project.name}} - {{bidPackage.title}}", + "body": [ + "Greeting with vendor name", + "Project overview", + "Scope summary", + "Due date", + "Link to view full bid package", + "List of attachments", + "Contact information" + ] + }, + "tracking": { + "sentAt": "Recorded when email sent", + "viewedAt": "Tracked via unique link click", + "viewLink": "/bid-packages/[id]/view?token=[unique_token]" + } + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/projects/[id]/bid-packages", + "description": "List bid packages for a project", + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/bid-packages/[id]", + "description": "Get bid package details", + "auth": "office, admin, subcontractor (if recipient)" + }, + { + "method": "POST", + "path": "/api/projects/[id]/bid-packages", + "description": "Create new bid package", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/bid-packages/[id]", + "description": "Update bid package", + "auth": "office, admin" + }, + { + "method": "DELETE", + "path": "/api/bid-packages/[id]", + "description": "Delete bid package (draft only)", + "auth": "admin" + }, + { + "method": "POST", + "path": "/api/bid-packages/[id]/send", + "description": "Send bid package to recipients", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/bid-packages/[id]/recipients", + "description": "Add recipients to bid package", + "body": ["vendorContactIds"], + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/bid-package-recipients/[id]", + "description": "Update recipient response", + "body": ["bidAmount", "responseNotes", "responseStatus"], + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/bid-package-templates", + "description": "List bid package templates", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/bid-package-templates", + "description": "Create bid package template", + "auth": "admin" + }, + { + "method": "PUT", + "path": "/api/bid-package-templates/[id]", + "description": "Update bid package template", + "auth": "admin" + }, + { + "method": "GET", + "path": "/api/vendors/by-trade/[trade]", + "description": "Get vendors by trade with estimator contacts", + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/bid-packages/[id]/view", + "description": "Public view page for recipients (with token)", + "auth": "token-based" + } + ], + "components": [ + { + "name": "BidPackageList", + "path": "src/components/bid-packages/bid-package-list.tsx", + "description": "Table of bid packages for a project" + }, + { + "name": "BidPackageForm", + "path": "src/components/bid-packages/bid-package-form.tsx", + "description": "Form for creating/editing bid packages" + }, + { + "name": "BidPackageDetail", + "path": "src/components/bid-packages/bid-package-detail.tsx", + "description": "Full bid package view with recipients" + }, + { + "name": "ScopeItemsEditor", + "path": "src/components/bid-packages/scope-items-editor.tsx", + "description": "Editable list of scope items" + }, + { + "name": "RecipientSelector", + "path": "src/components/bid-packages/recipient-selector.tsx", + "description": "Multi-select for vendors/contacts filtered by estimator role" + }, + { + "name": "RecipientStatusTable", + "path": "src/components/bid-packages/recipient-status-table.tsx", + "description": "Table showing delivery and response status" + }, + { + "name": "BidResponseForm", + "path": "src/components/bid-packages/bid-response-form.tsx", + "description": "Form for recording bid responses" + }, + { + "name": "TemplateEditor", + "path": "src/components/bid-packages/template-editor.tsx", + "description": "WYSIWYG editor for bid package templates" + }, + { + "name": "AttachmentPicker", + "path": "src/components/bid-packages/attachment-picker.tsx", + "description": "File picker for Drive attachments" + }, + { + "name": "BidPackagePublicView", + "path": "src/components/bid-packages/bid-package-public-view.tsx", + "description": "Public-facing view for subcontractors" + } + ], + "routes": [ + { + "path": "/dashboard/projects/[id]/bid-packages", + "description": "Bid packages list for a project" + }, + { + "path": "/dashboard/projects/[id]/bid-packages/new", + "description": "Create new bid package" + }, + { + "path": "/dashboard/projects/[id]/bid-packages/[bidId]", + "description": "View/edit bid package" + }, + { + "path": "/dashboard/settings/bid-templates", + "description": "Manage bid package templates" + }, + { + "path": "/bid-packages/[id]/view", + "description": "Public view for recipients (token auth)" + } + ] + }, + "testCases": [ + { + "id": "TC005-1", + "type": "unit", + "description": "Template variable replacement works correctly", + "steps": ["Create template with variables", "Render with project data", "Verify all variables replaced"] + }, + { + "id": "TC005-2", + "type": "integration", + "description": "Bid package email delivery", + "steps": ["Create bid package", "Add recipients", "Send package", "Verify emails sent via Resend"] + }, + { + "id": "TC005-3", + "type": "integration", + "description": "Role-based filtering shows only estimators", + "steps": ["Create vendor with multiple contacts", "Open recipient selector", "Verify only estimators shown"] + }, + { + "id": "TC005-4", + "type": "e2e", + "description": "Full bid package workflow", + "steps": ["Create package from template", "Add scope items", "Attach drawings", "Select recipients", "Send", "Record responses"] + }, + { + "id": "TC005-5", + "type": "e2e", + "description": "Subcontractor views bid package", + "steps": ["Send bid package to sub", "Sub clicks email link", "Verify view tracked", "Sub downloads attachments"] + } + ] + }, + { + "id": "F006", + "name": "Notifications System", + "priority": "P0", + "status": "planned", + "estimatedDays": 10, + "sprint": 3, + "dependencies": ["F001"], + "blocksFeatures": [], + "description": "Complete notification system with in-app notifications, email delivery, and time-driven scheduled notifications. Supports schedule assignment notifications, payment events, and bid package reminders.", + "businessValue": "Proactive notifications keep team members informed without requiring them to constantly check the app. Time-driven notifications for upcoming tasks and overdue items prevent things from falling through the cracks.", + "userStories": [ + { + "id": "US006-1", + "role": "User", + "action": "see my notifications in the app", + "benefit": "I stay informed about important events without checking email", + "acceptanceCriteria": [ + "Notification bell icon in header shows unread count", + "Dropdown shows recent notifications", + "Click notification to navigate to related item", + "Can mark as read or mark all as read" + ] + }, + { + "id": "US006-2", + "role": "User", + "action": "receive email notifications for important events", + "benefit": "I'm alerted even when not in the app", + "acceptanceCriteria": [ + "Email sent for configured event types", + "Email includes direct link to item", + "Can configure which events trigger email", + "Unsubscribe link in email" + ] + }, + { + "id": "US006-3", + "role": "Field Worker", + "action": "get notified when I'm assigned to a schedule item", + "benefit": "I know immediately when I have new work assigned", + "acceptanceCriteria": [ + "Notification created when task assignedTo changes to me", + "Email sent if preference enabled", + "Notification includes task details and dates" + ] + }, + { + "id": "US006-4", + "role": "PM", + "action": "receive automatic reminders for tasks due tomorrow", + "benefit": "I can proactively address upcoming deadlines", + "acceptanceCriteria": [ + "Daily job runs at configured time (e.g., 7am)", + "Finds all tasks due tomorrow", + "Sends notification to assignees and PM", + "Summary email with all upcoming tasks" + ] + }, + { + "id": "US006-5", + "role": "Subcontractor (Scheduler)", + "action": "get notified when schedule items are assigned to my company", + "benefit": "I can plan crew availability for upcoming work", + "acceptanceCriteria": [ + "Only scheduler contacts receive schedule notifications", + "Notification includes start date, duration, scope", + "Email with link to schedule view" + ] + }, + { + "id": "US006-6", + "role": "Admin", + "action": "get notified when payment events occur", + "benefit": "I can track financial activity without constant checking", + "acceptanceCriteria": [ + "Notification for: invoice paid, payment received, bill due", + "Includes amount and related entities", + "Links to financial detail view" + ] + } + ], + "technicalApproach": { + "overview": "Build notification service that creates notifications, delivers via email (Resend), and provides in-app notification center. Scheduled jobs via Cloudflare Queues for time-based notifications.", + "schemaChanges": [ + { + "table": "notifications", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "type", "type": "TEXT", "notNull": true, "values": ["task_assigned", "task_due", "task_overdue", "bid_package_sent", "bid_response_received", "payment_received", "invoice_due", "daily_log_submitted", "project_status_changed", "mention", "system"] }, + { "name": "title", "type": "TEXT", "notNull": true }, + { "name": "message", "type": "TEXT", "notNull": true }, + { "name": "link", "type": "TEXT", "description": "URL to navigate to on click" }, + { "name": "entity_type", "type": "TEXT", "description": "Type of related entity" }, + { "name": "entity_id", "type": "TEXT", "description": "ID of related entity" }, + { "name": "is_read", "type": "BOOLEAN", "default": false }, + { "name": "read_at", "type": "TEXT" }, + { "name": "email_sent", "type": "BOOLEAN", "default": false }, + { "name": "email_sent_at", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_notifications_user", "columns": ["user_id"] }, + { "name": "idx_notifications_unread", "columns": ["user_id", "is_read"] }, + { "name": "idx_notifications_type", "columns": ["type"] }, + { "name": "idx_notifications_entity", "columns": ["entity_type", "entity_id"] } + ] + }, + { + "table": "notification_preferences", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "onDelete": "CASCADE", "notNull": true, "unique": true }, + { "name": "email_enabled", "type": "BOOLEAN", "default": true }, + { "name": "email_task_assigned", "type": "BOOLEAN", "default": true }, + { "name": "email_task_due", "type": "BOOLEAN", "default": true }, + { "name": "email_task_overdue", "type": "BOOLEAN", "default": true }, + { "name": "email_bid_package", "type": "BOOLEAN", "default": true }, + { "name": "email_payment", "type": "BOOLEAN", "default": true }, + { "name": "email_daily_digest", "type": "BOOLEAN", "default": false }, + { "name": "digest_time", "type": "TEXT", "default": "07:00" }, + { "name": "quiet_hours_start", "type": "TEXT" }, + { "name": "quiet_hours_end", "type": "TEXT" }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_notification_prefs_user", "columns": ["user_id"] } + ] + }, + { + "table": "scheduled_notifications", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "type", "type": "TEXT", "notNull": true, "values": ["task_reminder", "bid_due_reminder", "invoice_due_reminder", "daily_digest"] }, + { "name": "scheduled_for", "type": "TEXT", "notNull": true }, + { "name": "entity_type", "type": "TEXT" }, + { "name": "entity_id", "type": "TEXT" }, + { "name": "target_user_id", "type": "TEXT", "references": "users.id" }, + { "name": "status", "type": "TEXT", "default": "pending", "values": ["pending", "processing", "sent", "failed", "cancelled"] }, + { "name": "processed_at", "type": "TEXT" }, + { "name": "error_message", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_scheduled_notifications_time", "columns": ["scheduled_for", "status"] }, + { "name": "idx_scheduled_notifications_entity", "columns": ["entity_type", "entity_id"] } + ] + } + ], + "notificationService": { + "file": "src/lib/notifications/notification-service.ts", + "methods": [ + { + "name": "createNotification", + "params": ["userId", "type", "title", "message", "options?"], + "description": "Create notification and optionally send email based on preferences" + }, + { + "name": "createBulkNotifications", + "params": ["userIds[]", "type", "title", "message", "options?"], + "description": "Create notifications for multiple users" + }, + { + "name": "markAsRead", + "params": ["notificationId"], + "description": "Mark single notification as read" + }, + { + "name": "markAllAsRead", + "params": ["userId"], + "description": "Mark all notifications as read for user" + }, + { + "name": "getUnreadCount", + "params": ["userId"], + "returns": "number", + "description": "Get count of unread notifications" + }, + { + "name": "getUserNotifications", + "params": ["userId", "options?"], + "returns": "Notification[]", + "description": "Get paginated notifications for user" + }, + { + "name": "scheduleNotification", + "params": ["type", "scheduledFor", "entityType?", "entityId?", "targetUserId?"], + "description": "Schedule a future notification" + }, + { + "name": "cancelScheduledNotification", + "params": ["entityType", "entityId"], + "description": "Cancel scheduled notifications for an entity" + } + ] + }, + "emailTemplates": { + "provider": "Resend", + "templates": [ + { + "name": "task-assigned", + "subject": "You've been assigned to: {{taskTitle}}", + "variables": ["taskTitle", "projectName", "startDate", "link"] + }, + { + "name": "task-due-reminder", + "subject": "Reminder: {{taskTitle}} is due tomorrow", + "variables": ["taskTitle", "projectName", "dueDate", "link"] + }, + { + "name": "task-overdue", + "subject": "Overdue: {{taskTitle}} was due {{daysOverdue}} days ago", + "variables": ["taskTitle", "projectName", "dueDate", "daysOverdue", "link"] + }, + { + "name": "bid-package-invitation", + "subject": "Request for Bid: {{projectName}} - {{bidPackageTitle}}", + "variables": ["vendorName", "projectName", "bidPackageTitle", "dueDate", "link"] + }, + { + "name": "bid-response-received", + "subject": "Bid received from {{vendorName}} for {{projectName}}", + "variables": ["vendorName", "projectName", "bidAmount", "link"] + }, + { + "name": "payment-received", + "subject": "Payment received: ${{amount}} for {{projectName}}", + "variables": ["amount", "projectName", "payerName", "link"] + }, + { + "name": "daily-digest", + "subject": "COMPASS Daily Summary - {{date}}", + "variables": ["date", "tasksDueToday", "tasksOverdue", "pendingBids", "recentActivity"] + } + ] + }, + "scheduledJobs": { + "infrastructure": "Cloudflare Queues with cron triggers", + "jobs": [ + { + "name": "task-due-reminders", + "schedule": "0 7 * * *", + "description": "Daily at 7am: Find tasks due tomorrow, create notifications" + }, + { + "name": "task-overdue-alerts", + "schedule": "0 8 * * *", + "description": "Daily at 8am: Find overdue tasks, alert PMs" + }, + { + "name": "bid-due-reminders", + "schedule": "0 9 * * *", + "description": "Daily at 9am: Remind about bids due in 2 days" + }, + { + "name": "daily-digest", + "schedule": "0 6 * * *", + "description": "Daily at 6am: Send digest emails to opted-in users" + }, + { + "name": "process-scheduled-notifications", + "schedule": "*/15 * * * *", + "description": "Every 15 mins: Process scheduled_notifications table" + } + ] + }, + "eventTriggers": { + "description": "Application events that trigger notifications", + "triggers": [ + { + "event": "Task assigned", + "action": "scheduleTask UPDATE with new assignedTo", + "notification": "task_assigned to new assignee" + }, + { + "event": "Task status changed", + "action": "scheduleTask UPDATE status", + "notification": "task_status_changed to PM and assignee" + }, + { + "event": "Bid package sent", + "action": "bidPackage status → sent", + "notification": "bid_package_sent to creator" + }, + { + "event": "Bid response recorded", + "action": "bidPackageRecipient responseStatus updated", + "notification": "bid_response_received to PM" + }, + { + "event": "Daily log submitted", + "action": "dailyLog CREATE", + "notification": "daily_log_submitted to PM" + }, + { + "event": "Payment received", + "action": "payment CREATE", + "notification": "payment_received to admin" + } + ] + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/notifications", + "description": "Get current user's notifications", + "params": ["limit", "offset", "unreadOnly"], + "auth": "any authenticated" + }, + { + "method": "GET", + "path": "/api/notifications/unread-count", + "description": "Get unread notification count", + "auth": "any authenticated" + }, + { + "method": "PUT", + "path": "/api/notifications/[id]/read", + "description": "Mark notification as read", + "auth": "owner" + }, + { + "method": "PUT", + "path": "/api/notifications/mark-all-read", + "description": "Mark all notifications as read", + "auth": "any authenticated" + }, + { + "method": "GET", + "path": "/api/notification-preferences", + "description": "Get current user's notification preferences", + "auth": "any authenticated" + }, + { + "method": "PUT", + "path": "/api/notification-preferences", + "description": "Update notification preferences", + "auth": "any authenticated" + } + ], + "components": [ + { + "name": "NotificationBell", + "path": "src/components/notifications/notification-bell.tsx", + "description": "Header icon with unread count badge" + }, + { + "name": "NotificationDropdown", + "path": "src/components/notifications/notification-dropdown.tsx", + "description": "Dropdown list of recent notifications" + }, + { + "name": "NotificationItem", + "path": "src/components/notifications/notification-item.tsx", + "description": "Single notification display with icon and actions" + }, + { + "name": "NotificationCenter", + "path": "src/components/notifications/notification-center.tsx", + "description": "Full page view of all notifications" + }, + { + "name": "NotificationPreferencesForm", + "path": "src/components/notifications/notification-preferences-form.tsx", + "description": "Settings form for notification preferences" + } + ], + "routes": [ + { + "path": "/dashboard/notifications", + "description": "Full notification center" + }, + { + "path": "/dashboard/settings/notifications", + "description": "Notification preferences" + } + ] + }, + "testCases": [ + { + "id": "TC006-1", + "type": "unit", + "description": "Notification service creates notification correctly", + "steps": ["Call createNotification", "Verify notification in database", "Verify email sent if preference enabled"] + }, + { + "id": "TC006-2", + "type": "integration", + "description": "Task assignment triggers notification", + "steps": ["Assign task to user", "Verify notification created", "Verify email sent"] + }, + { + "id": "TC006-3", + "type": "integration", + "description": "Scheduled job processes due reminders", + "steps": ["Create task due tomorrow", "Run scheduled job", "Verify notification created"] + }, + { + "id": "TC006-4", + "type": "e2e", + "description": "User receives and interacts with notifications", + "steps": ["Trigger notification event", "See unread count update", "Open dropdown", "Click notification", "Verify navigation and marked read"] + }, + { + "id": "TC006-5", + "type": "e2e", + "description": "Preference changes affect email delivery", + "steps": ["Disable email for task_assigned", "Assign task", "Verify notification created", "Verify no email sent"] + } + ] + }, + { + "id": "F007", + "name": "Client Communications Suite", + "priority": "P1", + "status": "planned", + "estimatedDays": 13, + "sprint": 4, + "dependencies": ["F001", "F006"], + "blocksFeatures": [], + "description": "Complete client communication tools including in-app messaging, email logging, and client portal updates. Provides a single source of truth for all client interactions.", + "businessValue": "Centralizes all client communication history. Prevents the 'you never told me that' scenario by maintaining complete records. Improves client experience with proactive updates.", + "userStories": [ + { + "id": "US007-1", + "role": "PM", + "action": "send a message to a client through COMPASS", + "benefit": "all communication is logged and visible to the team", + "acceptanceCriteria": [ + "Can compose message to client users", + "Message stored in COMPASS", + "Email notification sent to client", + "Reply captured and logged" + ] + }, + { + "id": "US007-2", + "role": "Client", + "action": "view and respond to messages from the builder", + "benefit": "I can communicate without hunting through email", + "acceptanceCriteria": [ + "Client sees messages in their portal", + "Can reply to messages", + "Reply notifications sent to PM", + "Message history preserved" + ] + }, + { + "id": "US007-3", + "role": "PM", + "action": "log an email conversation with a client", + "benefit": "I can keep records of external email threads", + "acceptanceCriteria": [ + "Can manually create email log entry", + "Fields: date, subject, participants, summary", + "Can attach email as file", + "Appears in communication timeline" + ] + }, + { + "id": "US007-4", + "role": "PM", + "action": "post an update to the client portal", + "benefit": "clients are proactively informed of progress", + "acceptanceCriteria": [ + "Can create project update/announcement", + "Update visible to all project clients", + "Can include photos", + "Notification sent to clients" + ] + }, + { + "id": "US007-5", + "role": "Client", + "action": "see recent updates and activity on my project", + "benefit": "I stay informed without bothering the builder", + "acceptanceCriteria": [ + "Client portal shows recent updates", + "Can see schedule milestones", + "Can see client-visible daily logs", + "Activity feed of relevant events" + ] + } + ], + "technicalApproach": { + "overview": "Build messaging system with threads, email logging capability, and client-facing update posts. Integrate with notification system for delivery. Client portal surfaces relevant project information.", + "schemaChanges": [ + { + "table": "message_threads", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "subject", "type": "TEXT", "notNull": true }, + { "name": "started_by", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "last_message_at", "type": "TEXT" }, + { "name": "is_archived", "type": "BOOLEAN", "default": false }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_message_threads_project", "columns": ["project_id"] }, + { "name": "idx_message_threads_last", "columns": ["last_message_at"] } + ] + }, + { + "table": "message_thread_participants", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "thread_id", "type": "TEXT", "references": "message_threads.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "last_read_at", "type": "TEXT" }, + { "name": "is_muted", "type": "BOOLEAN", "default": false } + ], + "indexes": [ + { "name": "idx_thread_participants_thread", "columns": ["thread_id"] }, + { "name": "idx_thread_participants_user", "columns": ["user_id"] } + ], + "constraints": [ + { "type": "unique", "columns": ["thread_id", "user_id"] } + ] + }, + { + "table": "messages", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "thread_id", "type": "TEXT", "references": "message_threads.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "sender_id", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "content", "type": "TEXT", "notNull": true }, + { "name": "is_system_message", "type": "BOOLEAN", "default": false }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_messages_thread", "columns": ["thread_id"] }, + { "name": "idx_messages_created", "columns": ["created_at"] } + ] + }, + { + "table": "message_attachments", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "message_id", "type": "TEXT", "references": "messages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "file_name", "type": "TEXT", "notNull": true }, + { "name": "drive_file_id", "type": "TEXT" }, + { "name": "drive_url", "type": "TEXT" }, + { "name": "file_size", "type": "INTEGER" }, + { "name": "mime_type", "type": "TEXT" } + ] + }, + { + "table": "email_logs", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "logged_by", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "email_date", "type": "TEXT", "notNull": true }, + { "name": "subject", "type": "TEXT", "notNull": true }, + { "name": "from_address", "type": "TEXT" }, + { "name": "to_addresses", "type": "TEXT", "description": "JSON array" }, + { "name": "summary", "type": "TEXT" }, + { "name": "full_content", "type": "TEXT" }, + { "name": "attachment_drive_ids", "type": "TEXT", "description": "JSON array of Drive file IDs" }, + { "name": "is_client_visible", "type": "BOOLEAN", "default": true }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_email_logs_project", "columns": ["project_id"] }, + { "name": "idx_email_logs_date", "columns": ["email_date"] } + ] + }, + { + "table": "project_updates", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "author_id", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "title", "type": "TEXT", "notNull": true }, + { "name": "content", "type": "TEXT", "notNull": true }, + { "name": "is_pinned", "type": "BOOLEAN", "default": false }, + { "name": "photo_drive_ids", "type": "TEXT", "description": "JSON array of Drive file IDs" }, + { "name": "notify_clients", "type": "BOOLEAN", "default": true }, + { "name": "published_at", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_project_updates_project", "columns": ["project_id"] }, + { "name": "idx_project_updates_published", "columns": ["published_at"] } + ] + } + ], + "messagingFlow": { + "createThread": [ + "1. PM creates new thread with subject and initial message", + "2. Selects client participants", + "3. System creates thread, participants, first message", + "4. Email notification sent to client participants", + "5. Thread appears in both PM's and client's message list" + ], + "replyToThread": [ + "1. User (PM or client) opens thread", + "2. Types reply message", + "3. System creates message record", + "4. Updates thread.last_message_at", + "5. Email notification to other participants", + "6. In-app notification to other participants" + ] + }, + "clientPortal": { + "description": "Client-facing views showing relevant project information", + "views": [ + { + "name": "Dashboard", + "content": ["Recent updates", "Upcoming milestones", "Unread messages count"] + }, + { + "name": "Updates", + "content": ["Project updates/announcements", "Pinned updates at top"] + }, + { + "name": "Messages", + "content": ["Message threads client is participant in"] + }, + { + "name": "Schedule", + "content": ["Milestones only (not full Gantt)", "Client-visible tasks"] + }, + { + "name": "Documents", + "content": ["Documents marked client-visible"] + }, + { + "name": "Daily Logs", + "content": ["Logs marked is_client_visible"] + } + ] + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/projects/[id]/messages", + "description": "List message threads for project", + "auth": "office, admin, client (own threads)" + }, + { + "method": "POST", + "path": "/api/projects/[id]/messages", + "description": "Create new message thread", + "body": ["subject", "content", "participantIds"], + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/message-threads/[id]", + "description": "Get thread with messages", + "auth": "participant" + }, + { + "method": "POST", + "path": "/api/message-threads/[id]/messages", + "description": "Reply to thread", + "body": ["content", "attachments?"], + "auth": "participant" + }, + { + "method": "PUT", + "path": "/api/message-threads/[id]/read", + "description": "Mark thread as read", + "auth": "participant" + }, + { + "method": "GET", + "path": "/api/projects/[id]/email-logs", + "description": "List email logs for project", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/projects/[id]/email-logs", + "description": "Create email log entry", + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/projects/[id]/updates", + "description": "List project updates", + "auth": "office, admin, client" + }, + { + "method": "POST", + "path": "/api/projects/[id]/updates", + "description": "Create project update", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/project-updates/[id]", + "description": "Edit project update", + "auth": "author, admin" + }, + { + "method": "GET", + "path": "/api/client/dashboard", + "description": "Client portal dashboard data", + "auth": "client" + } + ], + "components": [ + { + "name": "MessageThreadList", + "path": "src/components/messages/message-thread-list.tsx", + "description": "List of message threads" + }, + { + "name": "MessageThread", + "path": "src/components/messages/message-thread.tsx", + "description": "Full thread view with all messages" + }, + { + "name": "MessageComposer", + "path": "src/components/messages/message-composer.tsx", + "description": "New message/reply input" + }, + { + "name": "NewThreadDialog", + "path": "src/components/messages/new-thread-dialog.tsx", + "description": "Modal for starting new thread" + }, + { + "name": "EmailLogForm", + "path": "src/components/communications/email-log-form.tsx", + "description": "Form for logging email conversations" + }, + { + "name": "EmailLogTimeline", + "path": "src/components/communications/email-log-timeline.tsx", + "description": "Timeline of logged emails" + }, + { + "name": "ProjectUpdateForm", + "path": "src/components/communications/project-update-form.tsx", + "description": "Form for creating project updates" + }, + { + "name": "ProjectUpdateCard", + "path": "src/components/communications/project-update-card.tsx", + "description": "Display card for project update" + }, + { + "name": "ProjectUpdateFeed", + "path": "src/components/communications/project-update-feed.tsx", + "description": "Feed of project updates" + }, + { + "name": "ClientPortalDashboard", + "path": "src/components/client-portal/client-portal-dashboard.tsx", + "description": "Client-facing dashboard" + }, + { + "name": "ClientPortalSidebar", + "path": "src/components/client-portal/client-portal-sidebar.tsx", + "description": "Navigation for client portal" + } + ], + "routes": [ + { + "path": "/dashboard/projects/[id]/messages", + "description": "Project message threads" + }, + { + "path": "/dashboard/projects/[id]/messages/[threadId]", + "description": "Single message thread" + }, + { + "path": "/dashboard/projects/[id]/communications", + "description": "Email logs and communication timeline" + }, + { + "path": "/dashboard/projects/[id]/updates", + "description": "Project updates/announcements" + }, + { + "path": "/portal", + "description": "Client portal root (redirects to first project)" + }, + { + "path": "/portal/projects/[id]", + "description": "Client portal project dashboard" + }, + { + "path": "/portal/projects/[id]/messages", + "description": "Client portal messages" + }, + { + "path": "/portal/projects/[id]/updates", + "description": "Client portal updates" + }, + { + "path": "/portal/projects/[id]/schedule", + "description": "Client portal schedule (milestones)" + }, + { + "path": "/portal/projects/[id]/documents", + "description": "Client portal documents" + } + ] + }, + "testCases": [ + { + "id": "TC007-1", + "type": "integration", + "description": "Message thread creation and notification", + "steps": ["Create thread with client", "Verify email sent", "Verify client sees thread"] + }, + { + "id": "TC007-2", + "type": "integration", + "description": "Client reply notification", + "steps": ["Client replies to thread", "Verify PM notified", "Verify message appears"] + }, + { + "id": "TC007-3", + "type": "e2e", + "description": "Email log workflow", + "steps": ["Create email log", "Attach file", "Verify appears in timeline"] + }, + { + "id": "TC007-4", + "type": "e2e", + "description": "Project update notification", + "steps": ["Create update with notify_clients", "Verify clients notified", "Verify update visible in portal"] + }, + { + "id": "TC007-5", + "type": "e2e", + "description": "Client portal access", + "steps": ["Login as client", "Navigate portal", "Verify only appropriate data visible"] + } + ] + }, + { + "id": "F008", + "name": "Schedule Enhancements for Notifications", + "priority": "P1", + "status": "planned", + "estimatedDays": 3, + "sprint": 3, + "dependencies": ["F001", "F006"], + "blocksFeatures": [], + "description": "Enhance existing schedule system to support notifications for subcontractors and integrate with the three-tier user system. Enables assignment of vendors to schedule items and role-based notification routing.", + "businessValue": "Ensures subcontractors are automatically notified when work is scheduled for them. Reduces manual coordination overhead and prevents missed handoffs.", + "userStories": [ + { + "id": "US008-1", + "role": "PM", + "action": "assign a vendor to a schedule task", + "benefit": "the subcontractor is automatically notified of upcoming work", + "acceptanceCriteria": [ + "Can select vendor from dropdown on task", + "Scheduler contact at vendor receives notification", + "Task shows vendor assignment", + "Vendor can view assigned tasks when logged in" + ] + }, + { + "id": "US008-2", + "role": "Subcontractor (Scheduler)", + "action": "see all tasks my company is assigned to", + "benefit": "I can plan crew schedules and resource allocation", + "acceptanceCriteria": [ + "Filtered view shows only vendor's tasks", + "Shows across all projects", + "Includes dates, status, project info", + "Can export to CSV" + ] + }, + { + "id": "US008-3", + "role": "PM", + "action": "have notifications automatically sent when schedule changes affect a vendor", + "benefit": "subcontractors stay informed without manual communication", + "acceptanceCriteria": [ + "Notification on task date change", + "Notification on task status change", + "Notification on task deletion", + "Email includes changed details" + ] + } + ], + "technicalApproach": { + "overview": "Add vendor assignment to schedule tasks. Integrate with notification system for automatic alerts. Create subcontractor-specific schedule views.", + "schemaChanges": [ + { + "table": "schedule_tasks", + "operation": "ALTER", + "changes": [ + { + "column": "assigned_vendor_id", + "type": "TEXT", + "references": "vendors.id", + "nullable": true, + "description": "Vendor company assigned to this task" + } + ] + }, + { + "table": "schedule_task_notifications", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "task_id", "type": "TEXT", "references": "schedule_tasks.id", "onDelete": "CASCADE" }, + { "name": "vendor_contact_id", "type": "TEXT", "references": "vendor_contacts.id" }, + { "name": "notification_type", "type": "TEXT", "values": ["assigned", "date_changed", "status_changed", "deleted"] }, + { "name": "sent_at", "type": "TEXT" }, + { "name": "email_sent", "type": "BOOLEAN", "default": false } + ], + "description": "Tracks notifications sent to vendors about schedule changes" + } + ], + "notificationTriggers": [ + { + "trigger": "Task vendor assigned", + "recipients": "Scheduler contacts at vendor", + "content": "You have been scheduled for {{taskTitle}} starting {{startDate}}" + }, + { + "trigger": "Task dates changed", + "recipients": "Scheduler contacts at assigned vendor", + "content": "Schedule change: {{taskTitle}} now {{startDate}} - {{endDate}}" + }, + { + "trigger": "Task status changed", + "recipients": "Scheduler contacts at assigned vendor", + "content": "Status update: {{taskTitle}} is now {{status}}" + } + ], + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/vendors/[id]/schedule", + "description": "Get all tasks assigned to a vendor", + "auth": "office, admin, subcontractor (own vendor)" + }, + { + "method": "GET", + "path": "/api/my-schedule", + "description": "Get tasks for current user's vendor", + "auth": "subcontractor" + } + ], + "componentChanges": [ + { + "component": "TaskFormDialog", + "path": "src/components/schedule/task-form-dialog.tsx", + "changes": ["Add vendor selector dropdown", "Show scheduler contacts for selected vendor"] + }, + { + "component": "SubcontractorScheduleView", + "path": "src/components/schedule/subcontractor-schedule-view.tsx", + "description": "New component for vendor's assigned tasks across projects" + } + ], + "routes": [ + { + "path": "/dashboard/my-schedule", + "description": "Subcontractor's view of their assigned tasks" + } + ] + }, + "testCases": [ + { + "id": "TC008-1", + "type": "integration", + "description": "Vendor assignment triggers notification", + "steps": ["Assign vendor to task", "Verify notification created", "Verify email to scheduler contact"] + }, + { + "id": "TC008-2", + "type": "integration", + "description": "Date change notification to vendor", + "steps": ["Change task dates", "Verify notification to vendor", "Verify includes old and new dates"] + }, + { + "id": "TC008-3", + "type": "e2e", + "description": "Subcontractor views assigned schedule", + "steps": ["Login as subcontractor", "View my-schedule", "Verify only own vendor's tasks shown"] + } + ] + } + ], + "phaseTwo": { + "description": "Features deferred to Phase Two based on client feedback", + "features": [ + { + "name": "Payment Processing", + "reason": "Client prefers to use NetSuite for payment workflows", + "originalEstimate": "2 weeks", + "dependsOn": "NetSuite Integration" + }, + { + "name": "Purchase Orders", + "reason": "Nice-to-have per client feedback", + "scope": "PO system coordinated with subs, tied to schedule" + }, + { + "name": "Project Reporting", + "reason": "Can use NetSuite for these reports initially", + "scope": "Sub usage reports, financial reports" + }, + { + "name": "Subcontractor Insurance Tracking", + "reason": "Part of enhanced subcontractor management", + "scope": "Single source of truth for insurance documentation" + }, + { + "name": "NetSuite Integration", + "reason": "Complex integration requiring Phase One stability first", + "scope": "Bidirectional sync, notifications/flags from NetSuite" + }, + { + "name": "Lead/Opportunity Management", + "reason": "Nice-to-have per client feedback", + "scope": "Lead tracking similar to Buildertrend, PlanSwift integration" + } + ] + }, + "milestones": [ + { + "name": "Sprint 1 Complete", + "targetDate": "Week 2", + "deliverables": [ + "Three-tier user schema deployed", + "PWA infrastructure functional", + "Google Drive service account connected", + "Basic file browser showing Drive contents" + ] + }, + { + "name": "Sprint 2 Complete", + "targetDate": "Week 4", + "deliverables": [ + "Vendor contact management UI complete", + "Offline sync layer functional", + "Daily log creation working (online)", + "Full Drive integration with CSI folders" + ] + }, + { + "name": "Sprint 3 Complete", + "targetDate": "Week 6", + "deliverables": [ + "Daily logs with offline support complete", + "Notification system operational", + "Bid package creation and sending functional", + "Schedule notifications to vendors working" + ] + }, + { + "name": "Sprint 4 Complete", + "targetDate": "Week 8", + "deliverables": [ + "Client messaging complete", + "Email logging functional", + "Project updates/portal working", + "Full offline CRUD operational" + ] + }, + { + "name": "Phase One Complete", + "targetDate": "Week 10", + "deliverables": [ + "All Phase One features integrated", + "End-to-end testing complete", + "Performance validated", + "Ready for production deployment" + ] + } + ], + "risks": [ + { + "risk": "Google Drive API rate limits", + "probability": "Medium", + "impact": "Medium", + "mitigation": "Implement request queuing and caching, batch operations where possible" + }, + { + "risk": "Offline sync conflicts more complex than expected", + "probability": "Medium", + "impact": "High", + "mitigation": "Start with simpler last-write-wins, add manual resolution UI early" + }, + { + "risk": "Service worker caching issues on Cloudflare Workers", + "probability": "Low", + "impact": "Medium", + "mitigation": "Test early on production environment, have fallback to online-only" + }, + { + "risk": "WorkOS service account auth complexity", + "probability": "Low", + "impact": "Medium", + "mitigation": "Verify auth flow works for subcontractor invitations in Sprint 1" + }, + { + "risk": "Scope creep from client communications feature", + "probability": "Medium", + "impact": "Medium", + "mitigation": "Define clear boundaries, defer email auto-capture to Phase Two" + } + ], + "openQuestions": [ + { + "question": "Exact CSI folder structure to use?", + "owner": "Client", + "status": "pending", + "notes": "Sample structure visible in hps-structures/directories, need confirmation" + }, + { + "question": "OpenWeather API key or alternative weather provider?", + "owner": "Dev Team", + "status": "pending", + "notes": "Need API key for weather auto-fill in daily logs" + }, + { + "question": "Email domain for Resend (transactional emails)?", + "owner": "Client", + "status": "pending", + "notes": "Need verified domain for bid packages, notifications" + }, + { + "question": "Conflict resolution preference - auto vs manual?", + "owner": "Client", + "status": "decided", + "decision": "Last-write-wins with manual override option" + }, + { + "question": "Client portal branding requirements?", + "owner": "Client", + "status": "pending", + "notes": "Need logo, colors, any custom branding for client-facing views" + } + ] +} diff --git a/drizzle/0008_superb_lifeguard.sql b/drizzle/0008_superb_lifeguard.sql new file mode 100755 index 0000000..72ede6c --- /dev/null +++ b/drizzle/0008_superb_lifeguard.sql @@ -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 +); \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100755 index 0000000..b6037ae --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,2324 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "353cccf6-b609-4381-bbb4-a8d5456cfae0", + "prevId": "79017edb-88ed-4147-b649-3e12d16a60f5", + "tables": { + "agent_conversations": { + "name": "agent_conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_conversations_user_id_users_id_fk": { + "name": "agent_conversations_user_id_users_id_fk", + "tableFrom": "agent_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_memories": { + "name": "agent_memories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "embedding": { + "name": "embedding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_memories_conversation_id_agent_conversations_id_fk": { + "name": "agent_memories_conversation_id_agent_conversations_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memories_user_id_users_id_fk": { + "name": "agent_memories_user_id_users_id_fk", + "tableFrom": "agent_memories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customers": { + "name": "customers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feedback": { + "name": "feedback", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewport_width": { + "name": "viewport_width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewport_height": { + "name": "viewport_height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_issue_url": { + "name": "github_issue_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group_members": { + "name": "group_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_members_group_id_groups_id_fk": { + "name": "group_members_group_id_groups_id_fk", + "tableFrom": "group_members", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_members_user_id_users_id_fk": { + "name": "group_members_user_id_users_id_fk", + "tableFrom": "group_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groups_organization_id_organizations_id_fk": { + "name": "groups_organization_id_organizations_id_fk", + "tableFrom": "groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_members": { + "name": "project_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'OPEN'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_manager": { + "name": "project_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_job_id": { + "name": "netsuite_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_baselines": { + "name": "schedule_baselines", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_data": { + "name": "snapshot_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_baselines_project_id_projects_id_fk": { + "name": "schedule_baselines_project_id_projects_id_fk", + "tableFrom": "schedule_baselines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_tasks": { + "name": "schedule_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workdays": { + "name": "workdays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date_calculated": { + "name": "end_date_calculated", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'PENDING'" + }, + "is_critical_path": { + "name": "is_critical_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_milestone": { + "name": "is_milestone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "percent_complete": { + "name": "percent_complete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_tasks_project_id_projects_id_fk": { + "name": "schedule_tasks_project_id_projects_id_fk", + "tableFrom": "schedule_tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_dependencies": { + "name": "task_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "predecessor_id": { + "name": "predecessor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "successor_id": { + "name": "successor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'FS'" + }, + "lag_days": { + "name": "lag_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "task_dependencies_predecessor_id_schedule_tasks_id_fk": { + "name": "task_dependencies_predecessor_id_schedule_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "schedule_tasks", + "columnsFrom": [ + "predecessor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_dependencies_successor_id_schedule_tasks_id_fk": { + "name": "task_dependencies_successor_id_schedule_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "schedule_tasks", + "columnsFrom": [ + "successor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_members": { + "name": "team_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'office'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendors": { + "name": "vendors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Subcontractor'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workday_exceptions": { + "name": "workday_exceptions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'non_working'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'company_holiday'" + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'one_time'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workday_exceptions_project_id_projects_id_fk": { + "name": "workday_exceptions_project_id_projects_id_fk", + "tableFrom": "workday_exceptions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credit_memos": { + "name": "credit_memos", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memo_number": { + "name": "memo_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_applied": { + "name": "amount_applied", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_remaining": { + "name": "amount_remaining", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "credit_memos_customer_id_customers_id_fk": { + "name": "credit_memos_customer_id_customers_id_fk", + "tableFrom": "credit_memos", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credit_memos_project_id_projects_id_fk": { + "name": "credit_memos_project_id_projects_id_fk", + "tableFrom": "credit_memos", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_number": { + "name": "invoice_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subtotal": { + "name": "subtotal", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax": { + "name": "tax", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_due": { + "name": "amount_due", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invoices_customer_id_customers_id_fk": { + "name": "invoices_customer_id_customers_id_fk", + "tableFrom": "invoices", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_project_id_projects_id_fk": { + "name": "invoices_project_id_projects_id_fk", + "tableFrom": "invoices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_auth": { + "name": "netsuite_auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_in": { + "name": "expires_in", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_sync_log": { + "name": "netsuite_sync_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sync_type": { + "name": "sync_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "records_processed": { + "name": "records_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "records_failed": { + "name": "records_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "error_summary": { + "name": "error_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_sync_metadata": { + "name": "netsuite_sync_metadata", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "local_table": { + "name": "local_table", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_record_id": { + "name": "local_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "netsuite_record_type": { + "name": "netsuite_record_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "netsuite_internal_id": { + "name": "netsuite_internal_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified_local": { + "name": "last_modified_local", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified_remote": { + "name": "last_modified_remote", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'synced'" + }, + "conflict_data": { + "name": "conflict_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "payments": { + "name": "payments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_type": { + "name": "payment_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payment_date": { + "name": "payment_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_number": { + "name": "reference_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "payments_customer_id_customers_id_fk": { + "name": "payments_customer_id_customers_id_fk", + "tableFrom": "payments", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_vendor_id_vendors_id_fk": { + "name": "payments_vendor_id_vendors_id_fk", + "tableFrom": "payments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_project_id_projects_id_fk": { + "name": "payments_project_id_projects_id_fk", + "tableFrom": "payments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_bills": { + "name": "vendor_bills", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bill_number": { + "name": "bill_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "bill_date": { + "name": "bill_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subtotal": { + "name": "subtotal", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax": { + "name": "tax", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_due": { + "name": "amount_due", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_bills_vendor_id_vendors_id_fk": { + "name": "vendor_bills_vendor_id_vendors_id_fk", + "tableFrom": "vendor_bills", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_bills_project_id_projects_id_fk": { + "name": "vendor_bills_project_id_projects_id_fk", + "tableFrom": "vendor_bills", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 27bb259..ecf47bf 100755 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/drizzle/seed-users.sql b/drizzle/seed-users.sql index 812bf1b..e87142f 100755 --- a/drizzle/seed-users.sql +++ b/drizzle/seed-users.sql @@ -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) diff --git a/drizzle/seed.sql b/drizzle/seed.sql index f47c17c..9050276 100755 --- a/drizzle/seed.sql +++ b/drizzle/seed.sql @@ -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 diff --git a/next.config.ts b/next.config.ts index 9352f4e..991ce9d 100755 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/package.json b/package.json index 8aee75e..47e2fab 100755 --- a/package.json +++ b/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", diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 13eb06b..9bc384a 100755 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -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 (
@@ -26,6 +27,7 @@ export default function AuthLayout({ High Performance Structures

+ - ); + ) } diff --git a/src/app/api/agent/route.ts b/src/app/api/agent/route.ts new file mode 100755 index 0000000..9137da6 --- /dev/null +++ b/src/app/api/agent/route.ts @@ -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 + sessionStatus?: Record +} + +async function getOrCreateSession( + userId: string, + conversationId?: string +): Promise { + 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 { + 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 } + | 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 { + 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 } + ) + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index d1d33c3..cec07d8 100755 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -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 } ) } } diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts index f9941ff..73bb8c3 100755 --- a/src/app/api/auth/sso/route.ts +++ b/src/app/api/auth/sso/route.ts @@ -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", }) diff --git a/src/app/dashboard/customers/page.tsx b/src/app/dashboard/customers/page.tsx index 65df670..4e5ca17 100755 --- a/src/app/dashboard/customers/page.tsx +++ b/src/app/dashboard/customers/page.tsx @@ -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([]) @@ -21,6 +23,24 @@ export default function CustomersPage() { const [dialogOpen, setDialogOpen] = React.useState(false) const [editing, setEditing] = React.useState(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

- diff --git a/src/app/dashboard/financials/page.tsx b/src/app/dashboard/financials/page.tsx index 212eac3..fc22080 100755 --- a/src/app/dashboard/financials/page.tsx +++ b/src/app/dashboard/financials/page.tsx @@ -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() { {tab === "invoices" && ( - )} {tab === "bills" && ( - )} {tab === "payments" && ( - )} {tab === "credit-memos" && ( - diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 185a491..1ed8854 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -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 ( + + -
-
- {children} +
+ +
+
+ {children} +
+
+
@@ -55,7 +66,9 @@ export default async function DashboardLayout({ + + ) } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 170ef8d..b82489e 100755 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -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 { try { - const { getCloudflareContext } = await import("@opennextjs/cloudflare") + const { getCloudflareContext } = await import( + "@opennextjs/cloudflare" + ) const { env } = await getCloudflareContext() - const token = (env as unknown as Record).GITHUB_TOKEN as string | undefined + const token = (env as unknown as Record) + .GITHUB_TOKEN as string | undefined const headers: Record = { 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 ( -
-
-
- -

- Compass -

-

- Development preview — features may be incomplete - or change without notice. -

-
- -
-
- -
-
-
-

- - Working -

-
    -
  • Projects — create and manage projects with D1 database
  • -
  • Schedule — Gantt chart with phases, tasks, dependencies, and critical path
  • -
  • File browser — drive-style UI with folder navigation
  • -
  • Settings — app preferences with theme and notifications
  • -
  • Sidebar navigation with contextual project/file views
  • -
  • Command palette search (Cmd+K)
  • -
-
- -
-

- - In Progress -

-
    -
  • Project auto-provisioning (code generation, CSI folder structure)
  • -
  • Budget tracking (CSI divisions, estimated vs actual, change orders)
  • -
  • Document management (S3/R2 storage, metadata, versioning)
  • -
  • Communication logging (manual entries, timeline view)
  • -
  • Dashboard — three-column layout (past due, due today, action items)
  • -
  • User authentication and roles (WorkOS)
  • -
  • Email notifications (Resend)
  • -
  • Basic reports (budget variance, overdue tasks, monthly actuals)
  • -
-
- -
-

- - Planned -

-
    -
  • Client portal with read-only views
  • -
  • BuilderTrend import wizard (CSV-based)
  • -
  • Daily logs
  • -
  • Time tracking
  • -
  • Report builder (custom fields and filters)
  • -
  • Bid package management
  • -
-
- -
-

- - Future -

-
    -
  • Netsuite/QuickBooks API sync
  • -
  • Payment integration
  • -
  • RFI/Submittal tracking
  • -
  • Native mobile apps (iOS/Android)
  • -
  • Advanced scheduling (resource leveling, baseline comparison)
  • -
-
- -
- - {data && ( -
- - -
-

View on GitHub

-

{REPO}

-
- -
-
- } - label="Stars" - value={data.repo.stargazers_count} - /> - } - label="Forks" - value={data.repo.forks_count} - /> - } - label="Issues" - value={data.repo.open_issues_count} - /> - } - label="Watchers" - value={data.repo.subscribers_count} - /> -
- - -
- )} -
-
-
- ) -} - -function StatCard({ - icon, - label, - value, -}: { - icon: React.ReactNode - label: string - value: number -}) { - return ( -
-
- {icon} - {label} -
-

- {value.toLocaleString()} -

-
- ) + return } diff --git a/src/app/dashboard/people/page.tsx b/src/app/dashboard/people/page.tsx index 2e3f35f..0deb0bf 100755 --- a/src/app/dashboard/people/page.tsx +++ b/src/app/dashboard/people/page.tsx @@ -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() }, []) diff --git a/src/app/dashboard/vendors/page.tsx b/src/app/dashboard/vendors/page.tsx index 7717c38..344ceb1 100755 --- a/src/app/dashboard/vendors/page.tsx +++ b/src/app/dashboard/vendors/page.tsx @@ -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([]) @@ -21,6 +23,24 @@ export default function VendorsPage() { const [dialogOpen, setDialogOpen] = React.useState(false) const [editing, setEditing] = React.useState(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

- diff --git a/src/components/agent/agent-provider.tsx b/src/components/agent/agent-provider.tsx new file mode 100755 index 0000000..eaeec5a --- /dev/null +++ b/src/components/agent/agent-provider.tsx @@ -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(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 ( + + {children} + + ) +} + +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) +} diff --git a/src/components/agent/chat-panel.tsx b/src/components/agent/chat-panel.tsx new file mode 100755 index 0000000..2fcf829 --- /dev/null +++ b/src/components/agent/chat-panel.tsx @@ -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 */} +
+ {/* Desktop resize handle */} +
+ +
+ {/* Chat */} +
+ +
+ + {/* Dynamic UI for agent-generated components */} + {messages.some((m) => m.actions) && ( +
+ {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 ( + + ) + })} +
+ )} +
+
+ + {/* Mobile backdrop */} + {isOpen && ( +
setIsOpen(false)} + aria-hidden="true" + /> + )} + + {/* Mobile FAB trigger (desktop uses header button) */} + {!isOpen && ( + + )} + + ) +} + +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 diff --git a/src/components/agent/dynamic-ui.tsx b/src/components/agent/dynamic-ui.tsx new file mode 100755 index 0000000..72ffaff --- /dev/null +++ b/src/components/agent/dynamic-ui.tsx @@ -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 }) => { + await executeAction(action) + }, + [] + ) + + return ( +
+ +
+ ) +} + +interface RendererProps { + spec: ComponentSpec + onAction: (action: { type: string; payload?: Record }) => void +} + +function ComponentRenderer({ spec, onAction }: RendererProps) { + switch (spec.type) { + case "DataTable": + return + + case "Card": + return + + case "Badge": + return {spec.props.label} + + case "StatCard": + return + + case "Button": + return ( + + ) + + case "ButtonGroup": + return ( +
+ {spec.props.buttons.map((btn, i) => ( + + ))} +
+ ) + + case "InvoiceTable": + return + + case "CustomerCard": + return + + case "VendorCard": + return + + case "SchedulePreview": + return + + case "ProjectSummary": + return + + case "Grid": + return ( +
+ {(spec.props.children as ComponentSpec[])?.map((child, i) => ( + + ))} +
+ ) + + case "Stack": + return ( +
+ {(spec.props.children as ComponentSpec[])?.map((child, i) => ( + + ))} +
+ ) + + default: + return ( +
+ Unknown component type: {(spec as { type: string }).type} +
+ ) + } +} + +// DataTable renderer +function DataTableRenderer({ + columns, + data, + onRowClick, + onAction, +}: { + columns: Array<{ key: string; header: string; format?: string }> + data: Array> + onRowClick?: { type: string; payload?: Record } + onAction: RendererProps["onAction"] +}) { + return ( +
+ + + + {columns.map((col) => ( + {col.header} + ))} + + + + {data.map((row, i) => ( + + onRowClick && + onAction({ + ...onRowClick, + payload: { ...onRowClick.payload, rowData: row }, + }) + } + > + {columns.map((col) => ( + + {formatValue(row[col.key], col.format)} + + ))} + + ))} + +
+
+ ) +} + +// Card renderer +function CardRenderer({ + title, + description, + children, + footer, + onAction, +}: { + title: string + description?: string + children?: unknown[] + footer?: string + onAction: RendererProps["onAction"] +}) { + return ( + + + {title} + {description && {description}} + + {children && children.length > 0 && ( + + {(children as ComponentSpec[]).map((child, i) => ( + + ))} + + )} + {footer && ( + +

{footer}

+
+ )} +
+ ) +} + +// StatCard renderer +function StatCardRenderer({ + title, + value, + change, + changeLabel, +}: { + title: string + value: string | number + change?: number + changeLabel?: string +}) { + return ( + + + {title} + {value} + + {change !== undefined && ( + +
+ = 0 ? "text-green-600" : "text-red-600"}> + {change >= 0 ? "+" : ""} + {change}% + + {changeLabel && ` ${changeLabel}`} +
+
+ )} +
+ ) +} + +// 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 } + onAction: RendererProps["onAction"] +}) { + const statusVariant = (status: string) => { + switch (status) { + case "paid": + return "default" + case "overdue": + return "destructive" + default: + return "secondary" + } + } + + return ( +
+ + + + Invoice # + Customer + Amount + Due Date + Status + + + + {invoices.map((invoice) => ( + + onRowClick && + onAction({ + ...onRowClick, + payload: { ...onRowClick.payload, invoiceId: invoice.id }, + }) + } + > + {invoice.number} + {invoice.customer} + + {formatCurrency(invoice.amount)} + + {formatDate(invoice.dueDate)} + + + {invoice.status} + + + + ))} + +
+
+ ) +} + +// 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 }> + onAction: RendererProps["onAction"] +}) { + return ( + + + {customer.name} + {customer.company && ( + {customer.company} + )} + + + {customer.email &&

Email: {customer.email}

} + {customer.phone &&

Phone: {customer.phone}

} +
+ {actions && actions.length > 0 && ( + + {actions.map((action, i) => ( + + ))} + + )} +
+ ) +} + +// 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 }> + onAction: RendererProps["onAction"] +}) { + return ( + + +
+ {vendor.name} + {vendor.category} +
+
+ + {vendor.email &&

Email: {vendor.email}

} + {vendor.phone &&

Phone: {vendor.phone}

} +
+ {actions && actions.length > 0 && ( + + {actions.map((action, i) => ( + + ))} + + )} +
+ ) +} + +// 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 } + onAction: RendererProps["onAction"] +}) { + return ( + + + {projectName} Schedule + {tasks.length} tasks + + + {tasks.slice(0, 5).map((task) => ( +
+ onTaskClick && + onAction({ + ...onTaskClick, + payload: { ...onTaskClick.payload, taskId: task.id }, + }) + } + > +
+ {task.title} + {task.phase} +
+
+ +
+ {task.percentComplete}% complete + + {formatDate(task.startDate)} - {formatDate(task.endDate)} + +
+
+
+ ))} + {tasks.length > 5 && ( +

+ +{tasks.length - 5} more tasks +

+ )} +
+
+ ) +} + +// 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 }> + onAction: RendererProps["onAction"] +}) { + const completion = stats + ? Math.round((stats.tasksComplete / stats.tasksTotal) * 100) + : 0 + + return ( + + +
+ {project.name} + {project.status} +
+ {project.address && ( + {project.address} + )} +
+ + {project.clientName && ( +

Client: {project.clientName}

+ )} + {project.projectManager && ( +

PM: {project.projectManager}

+ )} + {stats && ( +
+
+ Progress + + {stats.tasksComplete}/{stats.tasksTotal} tasks ({completion}%) + +
+ +
+ )} +
+ {actions && actions.length > 0 && ( + + {actions.map((action, i) => ( + + ))} + + )} +
+ ) +} + +// 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 {String(value)} + 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 diff --git a/src/components/ai/prompt-input.tsx b/src/components/ai/prompt-input.tsx new file mode 100755 index 0000000..5864edf --- /dev/null +++ b/src/components/ai/prompt-input.tsx @@ -0,0 +1,1297 @@ +"use client" + +import type { ChatStatus, FileUIPart } from "ai" +import { + CornerDownLeftIcon, + ImageIcon, + Loader2Icon, + MicIcon, + PaperclipIcon, + PlusIcon, + SquareIcon, + XIcon, +} from "lucide-react" +import { nanoid } from "nanoid" +import { + type ChangeEvent, + type ChangeEventHandler, + Children, + type ClipboardEventHandler, + type ComponentProps, + createContext, + type FormEvent, + type FormEventHandler, + Fragment, + type HTMLAttributes, + type KeyboardEventHandler, + type PropsWithChildren, + type ReactNode, + type RefObject, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupTextarea, +} from "@/components/ui/input-group" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" + +// ============================================================================ +// Provider Context & Types +// ============================================================================ + +export interface AttachmentsContext { + files: (FileUIPart & { id: string })[] + add: (files: File[] | FileList) => void + remove: (id: string) => void + clear: () => void + openFileDialog: () => void + fileInputRef: RefObject +} + +export interface TextInputContext { + value: string + setInput: (v: string) => void + clear: () => void +} + +export interface PromptInputControllerProps { + textInput: TextInputContext + attachments: AttachmentsContext + /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */ + __registerFileInput: (ref: RefObject, open: () => void) => void +} + +const PromptInputController = createContext(null) +const ProviderAttachmentsContext = createContext(null) + +export const usePromptInputController = () => { + const ctx = useContext(PromptInputController) + if (!ctx) { + throw new Error( + "Wrap your component inside to use usePromptInputController().", + ) + } + return ctx +} + +// Optional variants (do NOT throw). Useful for dual-mode components. +const useOptionalPromptInputController = () => useContext(PromptInputController) + +export const useProviderAttachments = () => { + const ctx = useContext(ProviderAttachmentsContext) + if (!ctx) { + throw new Error( + "Wrap your component inside to use useProviderAttachments().", + ) + } + return ctx +} + +const useOptionalProviderAttachments = () => useContext(ProviderAttachmentsContext) + +export type PromptInputProviderProps = PropsWithChildren<{ + initialInput?: string +}> + +/** + * Optional global provider that lifts PromptInput state outside of PromptInput. + * If you don't use it, PromptInput stays fully self-managed. + */ +export function PromptInputProvider({ + initialInput: initialTextInput = "", + children, +}: PromptInputProviderProps) { + // ----- textInput state + const [textInput, setTextInput] = useState(initialTextInput) + const clearInput = useCallback(() => setTextInput(""), []) + + // ----- attachments state (global when wrapped) + const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]) + const fileInputRef = useRef(null) + const openRef = useRef<() => void>(() => {}) + + const add = useCallback((files: File[] | FileList) => { + const incoming = Array.from(files) + if (incoming.length === 0) { + return + } + + setAttachmentFiles(prev => + prev.concat( + incoming.map(file => ({ + id: nanoid(), + type: "file" as const, + url: URL.createObjectURL(file), + mediaType: file.type, + filename: file.name, + })), + ), + ) + }, []) + + const remove = useCallback((id: string) => { + setAttachmentFiles(prev => { + const found = prev.find(f => f.id === id) + if (found?.url) { + URL.revokeObjectURL(found.url) + } + return prev.filter(f => f.id !== id) + }) + }, []) + + const clear = useCallback(() => { + setAttachmentFiles(prev => { + for (const f of prev) { + if (f.url) { + URL.revokeObjectURL(f.url) + } + } + return [] + }) + }, []) + + // Keep a ref to attachments for cleanup on unmount (avoids stale closure) + const attachmentsRef = useRef(attachmentFiles) + attachmentsRef.current = attachmentFiles + + // Cleanup blob URLs on unmount to prevent memory leaks + useEffect(() => { + return () => { + for (const f of attachmentsRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url) + } + } + } + }, []) + + const openFileDialog = useCallback(() => { + openRef.current?.() + }, []) + + const attachments = useMemo( + () => ({ + files: attachmentFiles, + add, + remove, + clear, + openFileDialog, + fileInputRef, + }), + [attachmentFiles, add, remove, clear, openFileDialog], + ) + + const __registerFileInput = useCallback( + (ref: RefObject, open: () => void) => { + fileInputRef.current = ref.current + openRef.current = open + }, + [], + ) + + const controller = useMemo( + () => ({ + textInput: { + value: textInput, + setInput: setTextInput, + clear: clearInput, + }, + attachments, + __registerFileInput, + }), + [textInput, clearInput, attachments, __registerFileInput], + ) + + return ( + + + {children} + + + ) +} + +// ============================================================================ +// Component Context & Hooks +// ============================================================================ + +const LocalAttachmentsContext = createContext(null) + +export const usePromptInputAttachments = () => { + // Dual-mode: prefer provider if present, otherwise use local + const provider = useOptionalProviderAttachments() + const local = useContext(LocalAttachmentsContext) + const context = provider ?? local + if (!context) { + throw new Error( + "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider", + ) + } + return context +} + +export type PromptInputAttachmentProps = HTMLAttributes & { + data: FileUIPart & { id: string } + className?: string +} + +export function PromptInputAttachment({ data, className, ...props }: PromptInputAttachmentProps) { + const attachments = usePromptInputAttachments() + + const filename = data.filename || "" + + const mediaType = data.mediaType?.startsWith("image/") && data.url ? "image" : "file" + const isImage = mediaType === "image" + + const attachmentLabel = filename || (isImage ? "Image" : "Attachment") + + return ( + + +
+
+
+ {isImage ? ( + {filename + ) : ( +
+ +
+ )} +
+ +
+ + {attachmentLabel} +
+
+ +
+ {isImage && ( +
+ {filename +
+ )} +
+
+

+ {filename || (isImage ? "Image" : "Attachment")} +

+ {data.mediaType && ( +

{data.mediaType}

+ )} +
+
+
+
+
+ ) +} + +export type PromptInputAttachmentsProps = Omit, "children"> & { + children: (attachment: FileUIPart & { id: string }) => ReactNode +} + +export function PromptInputAttachments({ + children, + className, + ...props +}: PromptInputAttachmentsProps) { + const attachments = usePromptInputAttachments() + + if (!attachments.files.length) { + return null + } + + return ( +
+ {attachments.files.map(file => ( + {children(file)} + ))} +
+ ) +} + +export type PromptInputActionAddAttachmentsProps = ComponentProps & { + label?: string +} + +export const PromptInputActionAddAttachments = ({ + label = "Add photos or files", + ...props +}: PromptInputActionAddAttachmentsProps) => { + const attachments = usePromptInputAttachments() + + return ( + { + e.preventDefault() + attachments.openFileDialog() + }} + > + {label} + + ) +} + +export interface PromptInputMessage { + text: string + files: FileUIPart[] +} + +export type PromptInputProps = Omit, "onSubmit" | "onError"> & { + accept?: string // e.g., "image/*" or leave undefined for any + multiple?: boolean + // When true, accepts drops anywhere on document. Default false (opt-in). + globalDrop?: boolean + // Render a hidden input with given name and keep it in sync for native form posts. Default false. + syncHiddenInput?: boolean + // Minimal constraints + maxFiles?: number + maxFileSize?: number // bytes + onError?: (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void + onSubmit: (message: PromptInputMessage, event: FormEvent) => void | Promise +} + +export const PromptInput = ({ + className, + accept, + multiple, + globalDrop, + syncHiddenInput, + maxFiles, + maxFileSize, + onError, + onSubmit, + children, + ...props +}: PromptInputProps) => { + // Try to use a provider controller if present + const controller = useOptionalPromptInputController() + const usingProvider = !!controller + + // Refs + const inputRef = useRef(null) + const formRef = useRef(null) + + // ----- Local attachments (only used when no provider) + const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]) + const files = usingProvider ? controller.attachments.files : items + + // Keep a ref to files for cleanup on unmount (avoids stale closure) + const filesRef = useRef(files) + filesRef.current = files + + const openFileDialogLocal = useCallback(() => { + inputRef.current?.click() + }, []) + + const matchesAccept = useCallback( + (f: File) => { + if (!accept || accept.trim() === "") { + return true + } + + const patterns = accept + .split(",") + .map(s => s.trim()) + .filter(Boolean) + + return patterns.some(pattern => { + if (pattern.endsWith("/*")) { + const prefix = pattern.slice(0, -1) // e.g: image/* -> image/ + return f.type.startsWith(prefix) + } + return f.type === pattern + }) + }, + [accept], + ) + + const addLocal = useCallback( + (fileList: File[] | FileList) => { + const incoming = Array.from(fileList) + const accepted = incoming.filter(f => matchesAccept(f)) + if (incoming.length && accepted.length === 0) { + onError?.({ + code: "accept", + message: "No files match the accepted types.", + }) + return + } + const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true) + const sized = accepted.filter(withinSize) + if (accepted.length > 0 && sized.length === 0) { + onError?.({ + code: "max_file_size", + message: "All files exceed the maximum size.", + }) + return + } + + setItems(prev => { + const capacity = + typeof maxFiles === "number" ? Math.max(0, maxFiles - prev.length) : undefined + const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized + if (typeof capacity === "number" && sized.length > capacity) { + onError?.({ + code: "max_files", + message: "Too many files. Some were not added.", + }) + } + const next: (FileUIPart & { id: string })[] = [] + for (const file of capped) { + next.push({ + id: nanoid(), + type: "file", + url: URL.createObjectURL(file), + mediaType: file.type, + filename: file.name, + }) + } + return prev.concat(next) + }) + }, + [matchesAccept, maxFiles, maxFileSize, onError], + ) + + const removeLocal = useCallback( + (id: string) => + setItems(prev => { + const found = prev.find(file => file.id === id) + if (found?.url) { + URL.revokeObjectURL(found.url) + } + return prev.filter(file => file.id !== id) + }), + [], + ) + + const clearLocal = useCallback( + () => + setItems(prev => { + for (const file of prev) { + if (file.url) { + URL.revokeObjectURL(file.url) + } + } + return [] + }), + [], + ) + + const add = usingProvider ? controller.attachments.add : addLocal + const remove = usingProvider ? controller.attachments.remove : removeLocal + const clear = usingProvider ? controller.attachments.clear : clearLocal + const openFileDialog = usingProvider ? controller.attachments.openFileDialog : openFileDialogLocal + + // Let provider know about our hidden file input so external menus can call openFileDialog() + useEffect(() => { + if (!usingProvider) { + return + } + controller.__registerFileInput(inputRef, () => inputRef.current?.click()) + }, [usingProvider, controller]) + + // Note: File input cannot be programmatically set for security reasons + // The syncHiddenInput prop is no longer functional + useEffect(() => { + if (syncHiddenInput && inputRef.current && files.length === 0) { + inputRef.current.value = "" + } + }, [files, syncHiddenInput]) + + // Attach drop handlers on nearest form and document (opt-in) + useEffect(() => { + const form = formRef.current + if (!form) { + return + } + if (globalDrop) { + return // when global drop is on, let the document-level handler own drops + } + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + } + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files) + } + } + form.addEventListener("dragover", onDragOver) + form.addEventListener("drop", onDrop) + return () => { + form.removeEventListener("dragover", onDragOver) + form.removeEventListener("drop", onDrop) + } + }, [add, globalDrop]) + + useEffect(() => { + if (!globalDrop) { + return + } + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + } + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files) + } + } + document.addEventListener("dragover", onDragOver) + document.addEventListener("drop", onDrop) + return () => { + document.removeEventListener("dragover", onDragOver) + document.removeEventListener("drop", onDrop) + } + }, [add, globalDrop]) + + useEffect( + () => () => { + if (!usingProvider) { + for (const f of filesRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url) + } + } + } + }, + [usingProvider], + ) + + const handleChange: ChangeEventHandler = event => { + if (event.currentTarget.files) { + add(event.currentTarget.files) + } + // Reset input value to allow selecting files that were previously removed + event.currentTarget.value = "" + } + + const convertBlobUrlToDataUrl = async (url: string): Promise => { + try { + const response = await fetch(url) + const blob = await response.blob() + return new Promise(resolve => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result as string) + reader.onerror = () => resolve(null) + reader.readAsDataURL(blob) + }) + } catch { + return null + } + } + + const ctx = useMemo( + () => ({ + files: files.map(item => ({ ...item, id: item.id })), + add, + remove, + clear, + openFileDialog, + fileInputRef: inputRef, + }), + [files, add, remove, clear, openFileDialog], + ) + + const handleSubmit: FormEventHandler = event => { + event.preventDefault() + + const form = event.currentTarget + const text = usingProvider + ? controller.textInput.value + : (() => { + const formData = new FormData(form) + return (formData.get("message") as string) || "" + })() + + // Reset form immediately after capturing text to avoid race condition + // where user input during async blob conversion would be lost + if (!usingProvider) { + form.reset() + } + + // Convert blob URLs to data URLs asynchronously + Promise.all( + files.map(async ({ id: _, ...item }) => { + if (item.url?.startsWith("blob:")) { + const dataUrl = await convertBlobUrlToDataUrl(item.url) + // If conversion failed, keep the original blob URL + return { + ...item, + url: dataUrl ?? item.url, + } + } + return item + }), + ) + .then((convertedFiles: FileUIPart[]) => { + try { + const result = onSubmit({ text, files: convertedFiles }, event) + + // Handle both sync and async onSubmit + if (result instanceof Promise) { + result + .then(() => { + clear() + if (usingProvider) { + controller.textInput.clear() + } + }) + .catch(() => { + // Don't clear on error - user may want to retry + }) + } else { + // Sync function completed without throwing, clear attachments + clear() + if (usingProvider) { + controller.textInput.clear() + } + } + } catch { + // Don't clear on error - user may want to retry + } + }) + .catch(() => { + // Don't clear on error - user may want to retry + }) + } + + // Render with or without local provider + const inner = ( + <> + +
+ {children} +
+ + ) + + return usingProvider ? ( + inner + ) : ( + {inner} + ) +} + +export type PromptInputBodyProps = HTMLAttributes + +export const PromptInputBody = ({ className, ...props }: PromptInputBodyProps) => ( +
+) + +export type PromptInputTextareaProps = ComponentProps + +export const PromptInputTextarea = ({ + onChange, + className, + placeholder = "What would you like to know?", + ...props +}: PromptInputTextareaProps) => { + const controller = useOptionalPromptInputController() + const attachments = usePromptInputAttachments() + const [isComposing, setIsComposing] = useState(false) + + const handleKeyDown: KeyboardEventHandler = e => { + if (e.key === "Enter") { + if (isComposing || e.nativeEvent.isComposing) { + return + } + if (e.shiftKey) { + return + } + e.preventDefault() + + // Check if the submit button is disabled before submitting + const form = e.currentTarget.form + const submitButton = form?.querySelector('button[type="submit"]') as HTMLButtonElement | null + if (submitButton?.disabled) { + return + } + + form?.requestSubmit() + } + + // Remove last attachment when Backspace is pressed and textarea is empty + if (e.key === "Backspace" && e.currentTarget.value === "" && attachments.files.length > 0) { + e.preventDefault() + const lastAttachment = attachments.files.at(-1) + if (lastAttachment) { + attachments.remove(lastAttachment.id) + } + } + } + + const handlePaste: ClipboardEventHandler = event => { + const items = event.clipboardData?.items + + if (!items) { + return + } + + const files: File[] = [] + + for (const item of items) { + if (item.kind === "file") { + const file = item.getAsFile() + if (file) { + files.push(file) + } + } + } + + if (files.length > 0) { + event.preventDefault() + attachments.add(files) + } + } + + const controlledProps = controller + ? { + value: controller.textInput.value, + onChange: (e: ChangeEvent) => { + controller.textInput.setInput(e.currentTarget.value) + onChange?.(e) + }, + } + : { + onChange, + } + + return ( + setIsComposing(false)} + onCompositionStart={() => setIsComposing(true)} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + placeholder={placeholder} + {...props} + {...controlledProps} + /> + ) +} + +export type PromptInputHeaderProps = Omit, "align"> + +export const PromptInputHeader = ({ className, ...props }: PromptInputHeaderProps) => ( + +) + +export type PromptInputFooterProps = Omit, "align"> + +export const PromptInputFooter = ({ className, ...props }: PromptInputFooterProps) => ( + +) + +export type PromptInputToolsProps = HTMLAttributes + +export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => ( +
+) + +export type PromptInputButtonProps = ComponentProps + +export const PromptInputButton = ({ + variant = "ghost", + className, + size, + ...props +}: PromptInputButtonProps) => { + const newSize = size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm") + + return ( + + ) +} + +export type PromptInputActionMenuProps = ComponentProps +export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => ( + +) + +export type PromptInputActionMenuTriggerProps = PromptInputButtonProps + +export const PromptInputActionMenuTrigger = ({ + className, + children, + ...props +}: PromptInputActionMenuTriggerProps) => ( + + + {children ?? } + + +) + +export type PromptInputActionMenuContentProps = ComponentProps +export const PromptInputActionMenuContent = ({ + className, + ...props +}: PromptInputActionMenuContentProps) => ( + +) + +export type PromptInputActionMenuItemProps = ComponentProps +export const PromptInputActionMenuItem = ({ + className, + ...props +}: PromptInputActionMenuItemProps) => + +// Note: Actions that perform side-effects (like opening a file dialog) +// are provided in opt-in modules (e.g., prompt-input-attachments). + +export type PromptInputSubmitProps = ComponentProps & { + status?: ChatStatus +} + +export const PromptInputSubmit = ({ + className, + variant = "default", + size = "icon-sm", + status, + children, + ...props +}: PromptInputSubmitProps) => { + let Icon = + + if (status === "submitted") { + Icon = + } else if (status === "streaming") { + Icon = + } else if (status === "error") { + Icon = + } + + return ( + + {children ?? Icon} + + ) +} + +interface SpeechRecognition extends EventTarget { + continuous: boolean + interimResults: boolean + lang: string + start(): void + stop(): void + onstart: ((this: SpeechRecognition, ev: Event) => void) | null + onend: ((this: SpeechRecognition, ev: Event) => void) | null + onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null + onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null +} + +interface SpeechRecognitionEvent extends Event { + results: SpeechRecognitionResultList + resultIndex: number +} + +interface SpeechRecognitionResultList { + readonly length: number + item(index: number): SpeechRecognitionResult + [index: number]: SpeechRecognitionResult +} + +interface SpeechRecognitionResult { + readonly length: number + item(index: number): SpeechRecognitionAlternative + [index: number]: SpeechRecognitionAlternative + isFinal: boolean +} + +interface SpeechRecognitionAlternative { + transcript: string + confidence: number +} + +interface SpeechRecognitionErrorEvent extends Event { + error: string +} + +declare global { + interface Window { + SpeechRecognition: { + new (): SpeechRecognition + } + webkitSpeechRecognition: { + new (): SpeechRecognition + } + } +} + +export type PromptInputSpeechButtonProps = ComponentProps & { + textareaRef?: RefObject + onTranscriptionChange?: (text: string) => void +} + +export const PromptInputSpeechButton = ({ + className, + textareaRef, + onTranscriptionChange, + ...props +}: PromptInputSpeechButtonProps) => { + const [isListening, setIsListening] = useState(false) + const [recognition, setRecognition] = useState(null) + const recognitionRef = useRef(null) + + useEffect(() => { + if ( + typeof window !== "undefined" && + ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) + ) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition + const speechRecognition = new SpeechRecognition() + + speechRecognition.continuous = true + speechRecognition.interimResults = true + speechRecognition.lang = "en-US" + + speechRecognition.onstart = () => { + setIsListening(true) + } + + speechRecognition.onend = () => { + setIsListening(false) + } + + speechRecognition.onresult = event => { + let finalTranscript = "" + + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i] + if (result.isFinal) { + finalTranscript += result[0]?.transcript ?? "" + } + } + + if (finalTranscript && textareaRef?.current) { + const textarea = textareaRef.current + const currentValue = textarea.value + const newValue = currentValue + (currentValue ? " " : "") + finalTranscript + + textarea.value = newValue + textarea.dispatchEvent(new Event("input", { bubbles: true })) + onTranscriptionChange?.(newValue) + } + } + + speechRecognition.onerror = event => { + console.error("Speech recognition error:", event.error) + setIsListening(false) + } + + recognitionRef.current = speechRecognition + setRecognition(speechRecognition) + } + + return () => { + if (recognitionRef.current) { + recognitionRef.current.stop() + } + } + }, [textareaRef, onTranscriptionChange]) + + const toggleListening = useCallback(() => { + if (!recognition) { + return + } + + if (isListening) { + recognition.stop() + } else { + recognition.start() + } + }, [recognition, isListening]) + + return ( + + + + ) +} + +export type PromptInputSelectProps = ComponentProps + +export const PromptInputSelect = (props: PromptInputSelectProps) => + 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" + )} + /> + + + + + {stats && ( +
+ + + View on GitHub + + + + | + + + {REPO} + +
+ + + {stats.stargazers_count} + + + + {stats.forks_count} + + + + {stats.open_issues_count} + + + + {stats.subscribers_count} + +
+
+ )} +
+
+ + {/* Active: messages or suggestions */} +
+ {messages.length > 0 ? ( +
+
+ {messages.map((msg) => { + if (msg.role === "user") { + return ( +
+
+ {msg.content} +
+
+ ) + } + return ( +
+ {msg.content ? ( + <> +
+ + {msg.content} + +
+
+ + + + +
+ + ) : ( + + )} +
+ ) + })} +
+
+ ) : ( +
+
+ +
+
+ )} +
+
+ + {/* Bottom input - active only */} +
+
{ + e.preventDefault() + const trimmed = chatInput.trim() + if (!trimmed || isGenerating) return + append({ role: "user", content: trimmed }) + setChatInput("") + }} + > +
+