diff --git a/.eslintrc.autofix.js b/.eslintrc.autofix.js index e971dca..ff2caf3 100755 --- a/.eslintrc.autofix.js +++ b/.eslintrc.autofix.js @@ -1,7 +1,7 @@ module.exports = { - "extends": [".eslintrc.cjs"], - "rules": { + extends: [".eslintrc.cjs"], + rules: { // Only enable the rule we want to autofix - "@typescript-eslint/prefer-nullish-coalescing": "error" - } -}; \ No newline at end of file + "@typescript-eslint/prefer-nullish-coalescing": "error", + }, +}; diff --git a/bun.lock b/bun.lock index 961165e..34552e7 100644 --- a/bun.lock +++ b/bun.lock @@ -33,8 +33,16 @@ "@radix-ui/react-tooltip": "^1.2.8", "@shadcn/ui": "^0.0.4", "@t3-oss/env-nextjs": "^0.13.10", + "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-table": "^3.20.0", + "@tiptap/extension-table-cell": "^3.20.0", + "@tiptap/extension-table-header": "^3.20.0", + "@tiptap/extension-table-row": "^3.20.0", + "@tiptap/pm": "^3.20.0", + "@tiptap/react": "^3.20.0", + "@tiptap/starter-kit": "^3.20.0", "@trpc/client": "^11.10.0", "@trpc/react-query": "^11.10.0", "@trpc/server": "^11.10.0", @@ -47,6 +55,7 @@ "date-fns": "^4.1.0", "driver.js": "^1.4.0", "drizzle-orm": "^0.41.0", + "html2pdf.js": "^0.14.0", "js-cookie": "^3.0.5", "lucide-react": "^0.536.0", "minio": "^8.0.6", @@ -60,11 +69,13 @@ "react-dom": "^19.2.4", "react-hook-form": "^7.71.1", "react-resizable-panels": "^3.0.6", + "react-signature-canvas": "^1.1.0-alpha.2", "react-webcam": "^7.2.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "superjson": "^2.2.6", "tailwind-merge": "^3.4.0", + "tiptap-markdown": "^0.9.0", "ws": "^8.19.0", "zod": "^4.3.6", "zustand": "^4.5.7", @@ -193,6 +204,8 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], @@ -517,6 +530,8 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], @@ -715,6 +730,8 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="], + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], @@ -723,6 +740,70 @@ "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tiptap/core": ["@tiptap/core@3.20.0", "", { "peerDependencies": { "@tiptap/pm": "^3.20.0" } }, "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ=="], + + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA=="], + + "@tiptap/extension-bold": ["@tiptap/extension-bold@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg=="], + + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.20.0", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-MDosUfs8Tj+nwg8RC+wTMWGkLJORXmbR6YZgbiX4hrc7G90Gopdd6kj6ht5/T8t7dLLaX7N0+DEHdUEPGED7dw=="], + + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.20.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.0" } }, "sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw=="], + + "@tiptap/extension-code": ["@tiptap/extension-code@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ=="], + + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ=="], + + "@tiptap/extension-document": ["@tiptap/extension-document@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w=="], + + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.20.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.0" } }, "sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ=="], + + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.20.0", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-rYs4Bv5pVjqZ/2vvR6oe7ammZapkAwN51As/WDbemvYDjfOGRqK58qGauUjYZiDzPOEIzI2mxGwsZ4eJhPW4Ig=="], + + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.20.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.0" } }, "sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w=="], + + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA=="], + + "@tiptap/extension-heading": ["@tiptap/extension-heading@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA=="], + + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA=="], + + "@tiptap/extension-italic": ["@tiptap/extension-italic@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw=="], + + "@tiptap/extension-link": ["@tiptap/extension-link@3.20.0", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA=="], + + "@tiptap/extension-list": ["@tiptap/extension-list@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ=="], + + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.20.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.0" } }, "sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA=="], + + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.20.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.0" } }, "sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA=="], + + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.20.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.0" } }, "sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA=="], + + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ=="], + + "@tiptap/extension-strike": ["@tiptap/extension-strike@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ=="], + + "@tiptap/extension-table": ["@tiptap/extension-table@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-vaaMtQ2KnSSr8WVwgWf7NYNzPwrHx/6T0ekA5CxV8qNUEpXIaLXa5+tE7tJHWEdNR2KY3gUJ46D3lfOkxyFrBQ=="], + + "@tiptap/extension-table-cell": ["@tiptap/extension-table-cell@3.20.0", "", { "peerDependencies": { "@tiptap/extension-table": "^3.20.0" } }, "sha512-9Dg4zda3UWwtpBwSG7b9BeQy5oT27a/yEIBeARuxe19bloMLZgqpPRtnSrOK0OAITtVnjA+NZdKPcVLRMS2E8A=="], + + "@tiptap/extension-table-header": ["@tiptap/extension-table-header@3.20.0", "", { "peerDependencies": { "@tiptap/extension-table": "^3.20.0" } }, "sha512-2tVHHlihpeHO/gh2uU46gAX3NTGdKR+yDmfLlO2l0QAvx2TXNfNzX2pOM4MmyostW5Ko9TCWV4x0D9h3IQDhPw=="], + + "@tiptap/extension-table-row": ["@tiptap/extension-table-row@3.20.0", "", { "peerDependencies": { "@tiptap/extension-table": "^3.20.0" } }, "sha512-clkfQahkYW/U48QBh1rPZv3AWWSC9AqGKp2DLTH/SGIorM/NwI0jpPtBETMlvowyQu0ivlH9B896smEph+Do2A=="], + + "@tiptap/extension-text": ["@tiptap/extension-text@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ=="], + + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0" } }, "sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ=="], + + "@tiptap/extensions": ["@tiptap/extensions@3.20.0", "", { "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg=="], + + "@tiptap/pm": ["@tiptap/pm@3.20.0", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A=="], + + "@tiptap/react": ["@tiptap/react@3.20.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.20.0", "@tiptap/extension-floating-menu": "^3.20.0" }, "peerDependencies": { "@tiptap/core": "^3.20.0", "@tiptap/pm": "^3.20.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-jFLNzkmn18zqefJwPje0PPd9VhZ7Oy28YHiSvSc7YpBnQIbuN/HIxZ2lrOsKyEHta0WjRZjfU5X1pGxlbcGwOA=="], + + "@tiptap/starter-kit": ["@tiptap/starter-kit@3.20.0", "", { "dependencies": { "@tiptap/core": "^3.20.0", "@tiptap/extension-blockquote": "^3.20.0", "@tiptap/extension-bold": "^3.20.0", "@tiptap/extension-bullet-list": "^3.20.0", "@tiptap/extension-code": "^3.20.0", "@tiptap/extension-code-block": "^3.20.0", "@tiptap/extension-document": "^3.20.0", "@tiptap/extension-dropcursor": "^3.20.0", "@tiptap/extension-gapcursor": "^3.20.0", "@tiptap/extension-hard-break": "^3.20.0", "@tiptap/extension-heading": "^3.20.0", "@tiptap/extension-horizontal-rule": "^3.20.0", "@tiptap/extension-italic": "^3.20.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-list": "^3.20.0", "@tiptap/extension-list-item": "^3.20.0", "@tiptap/extension-list-keymap": "^3.20.0", "@tiptap/extension-ordered-list": "^3.20.0", "@tiptap/extension-paragraph": "^3.20.0", "@tiptap/extension-strike": "^3.20.0", "@tiptap/extension-text": "^3.20.0", "@tiptap/extension-underline": "^3.20.0", "@tiptap/extensions": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw=="], + "@trpc/client": ["@trpc/client@11.10.0", "", { "peerDependencies": { "@trpc/server": "11.10.0", "typescript": ">=5.7.2" } }, "sha512-h0s2AwDtuhS8INRb4hlo4z3RKCkarWqlOy+3ffJgrlDxzzW6aLUN+9nDrcN4huPje1Em15tbCOqhIc6oaKYTRw=="], "@trpc/react-query": ["@trpc/react-query@11.10.0", "", { "peerDependencies": { "@tanstack/react-query": "^5.80.3", "@trpc/client": "11.10.0", "@trpc/server": "11.10.0", "react": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-SKLpwEMU32mpDTCc3msMxb0fx113x4Jsiw0/t+ENY6AtyvhvDMRF1bpWtoNyY6zpX5wN4JCQMhHef8k0T1rJIw=="], @@ -749,12 +830,28 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="], + + "@types/markdown-it": ["@types/markdown-it@13.0.9", "", { "dependencies": { "@types/linkify-it": "^3", "@types/mdurl": "^1" } }, "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw=="], + + "@types/mdurl": ["@types/mdurl@1.0.5", "", {}, "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA=="], + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + + "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/signature_pad": ["@types/signature_pad@2.3.6", "", {}, "sha512-v3j92gCQJoxomHhd+yaG4Vsf8tRS/XbzWKqDv85UsqjMGy4zhokuwKe4b6vhbgncKkh+thF+gpz6+fypTtnFqQ=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="], @@ -879,6 +976,8 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], @@ -915,6 +1014,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="], + "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -943,8 +1044,16 @@ "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + "core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="], + + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], @@ -979,6 +1088,8 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "driver.js": ["driver.js@1.4.0", "", {}, "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew=="], "drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], @@ -991,6 +1102,8 @@ "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], @@ -1061,12 +1174,16 @@ "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=="], + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="], + "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], @@ -1075,6 +1192,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1139,6 +1258,10 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + + "html2pdf.js": ["html2pdf.js@0.14.0", "", { "dependencies": { "dompurify": "^3.3.1", "html2canvas": "^1.0.0", "jspdf": "^4.0.0" } }, "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ=="], + "human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1153,6 +1276,8 @@ "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=="], + "iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="], + "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], @@ -1243,6 +1368,8 @@ "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + "jspdf": ["jspdf@4.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -1279,6 +1406,10 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -1293,8 +1424,14 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + + "markdown-it-task-lists": ["markdown-it-task-lists@2.1.1", "", {}, "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -1359,12 +1496,16 @@ "ora": ["ora@6.3.1", "", { "dependencies": { "chalk": "^5.0.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.6.1", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.1.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "strip-ansi": "^7.0.1", "wcwidth": "^1.0.1" } }, "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ=="], + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], + "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=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -1375,6 +1516,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -1383,6 +1526,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], "preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="], @@ -1399,14 +1544,54 @@ "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=="], + "prosemirror-changeset": ["prosemirror-changeset@2.4.0", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng=="], + + "prosemirror-collab": ["prosemirror-collab@1.3.1", "", { "dependencies": { "prosemirror-state": "^1.0.0" } }, "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ=="], + + "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="], + + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="], + + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.0", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ=="], + + "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], + + "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="], + + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], + + "prosemirror-markdown": ["prosemirror-markdown@1.13.4", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw=="], + + "prosemirror-menu": ["prosemirror-menu@1.3.0", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg=="], + + "prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="], + + "prosemirror-schema-basic": ["prosemirror-schema-basic@1.2.4", "", { "dependencies": { "prosemirror-model": "^1.25.0" } }, "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ=="], + + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], + + "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], + + "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], + + "prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="], + + "prosemirror-transform": ["prosemirror-transform@1.11.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw=="], + + "prosemirror-view": ["prosemirror-view@1.41.6", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], "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=="], + "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react-day-picker": ["react-day-picker@9.13.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg=="], @@ -1423,6 +1608,8 @@ "react-resizable-panels": ["react-resizable-panels@3.0.6", "", { "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew=="], + "react-signature-canvas": ["react-signature-canvas@1.1.0-alpha.2", "", { "dependencies": { "@babel/runtime": "^7.17.9", "@types/signature_pad": "^2.3.0", "signature_pad": "^2.3.2", "trim-canvas": "^0.1.0" }, "peerDependencies": { "@types/prop-types": "^15.7.3", "@types/react": "0.14 - 19", "prop-types": "^15.5.8", "react": "0.14 - 19", "react-dom": "0.14 - 19" }, "optionalPeers": ["@types/prop-types", "@types/react"] }, "sha512-tKUNk3Gmh04Ug4K8p5g8Is08BFUKvbXxi0PyetQ/f8OgCBzcx4vqNf9+OArY/TdNdfHtswXQNRwZD6tyELjkjQ=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-webcam": ["react-webcam@7.2.0", "", { "peerDependencies": { "react": ">=16.2.0", "react-dom": ">=16.2.0" } }, "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg=="], @@ -1431,6 +1618,8 @@ "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=="], + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], + "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=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -1443,8 +1632,12 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -1489,6 +1682,8 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "signature_pad": ["signature_pad@2.3.2", "", {}, "sha512-peYXLxOsIY6MES2TrRLDiNg2T++8gGbpP2yaC+6Ohtxr+a2dzoaqWosWDY9sWqTAAk6E/TyQO+LJw9zQwyu5kA=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -1505,6 +1700,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="], @@ -1549,12 +1746,16 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -1565,8 +1766,12 @@ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tiptap-markdown": ["tiptap-markdown@0.9.0", "", { "dependencies": { "@types/markdown-it": "^13.0.7", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "prosemirror-markdown": "^1.11.1" }, "peerDependencies": { "@tiptap/core": "^3.0.1" } }, "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "trim-canvas": ["trim-canvas@0.1.2", "", {}, "sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "ts-unused-exports": ["ts-unused-exports@11.0.1", "", { "dependencies": { "chalk": "^4.0.0", "tsconfig-paths": "^3.9.0" }, "peerDependencies": { "typescript": ">=3.8.3" }, "bin": { "ts-unused-exports": "bin/ts-unused-exports" } }, "sha512-b1uIe0B8YfNZjeb+bx62LrB6qaO4CHT8SqMVBkwbwLj7Nh0xQ4J8uV0dS9E6AABId0U4LQ+3yB/HXZBMslGn2A=="], @@ -1591,6 +1796,8 @@ "typescript-eslint": ["typescript-eslint@8.55.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.55.0", "@typescript-eslint/parser": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/utils": "8.55.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1611,10 +1818,14 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "web-encoding": ["web-encoding@1.1.5", "", { "dependencies": { "util": "^0.12.3" }, "optionalDependencies": { "@zxing/text-encoding": "0.9.0" } }, "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA=="], @@ -1781,6 +1992,8 @@ "ora/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], + "prosemirror-markdown/@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + "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=="], @@ -1879,6 +2092,10 @@ "eslint-import-resolver-typescript/tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + "prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], diff --git a/package.json b/package.json index df6f6cb..73c58aa 100755 --- a/package.json +++ b/package.json @@ -51,8 +51,16 @@ "@radix-ui/react-tooltip": "^1.2.8", "@shadcn/ui": "^0.0.4", "@t3-oss/env-nextjs": "^0.13.10", + "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-table": "^3.20.0", + "@tiptap/extension-table-cell": "^3.20.0", + "@tiptap/extension-table-header": "^3.20.0", + "@tiptap/extension-table-row": "^3.20.0", + "@tiptap/pm": "^3.20.0", + "@tiptap/react": "^3.20.0", + "@tiptap/starter-kit": "^3.20.0", "@trpc/client": "^11.10.0", "@trpc/react-query": "^11.10.0", "@trpc/server": "^11.10.0", @@ -65,6 +73,7 @@ "date-fns": "^4.1.0", "driver.js": "^1.4.0", "drizzle-orm": "^0.41.0", + "html2pdf.js": "^0.14.0", "js-cookie": "^3.0.5", "lucide-react": "^0.536.0", "minio": "^8.0.6", @@ -78,11 +87,13 @@ "react-dom": "^19.2.4", "react-hook-form": "^7.71.1", "react-resizable-panels": "^3.0.6", + "react-signature-canvas": "^1.1.0-alpha.2", "react-webcam": "^7.2.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "superjson": "^2.2.6", "tailwind-merge": "^3.4.0", + "tiptap-markdown": "^0.9.0", "ws": "^8.19.0", "zod": "^4.3.6", "zustand": "^4.5.7" diff --git a/scripts/archive/check-db-actions.ts b/scripts/archive/check-db-actions.ts index d319523..9d54bec 100644 --- a/scripts/archive/check-db-actions.ts +++ b/scripts/archive/check-db-actions.ts @@ -1,4 +1,3 @@ - import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "../../src/server/db/schema"; @@ -9,38 +8,39 @@ const connection = postgres(connectionString); const db = drizzle(connection, { schema }); async function main() { - console.log("🔍 Checking seeded actions..."); + console.log("🔍 Checking seeded actions..."); - const actions = await db.query.actions.findMany({ - where: (actions, { or, eq, like }) => or( - eq(actions.type, "sequence"), - eq(actions.type, "parallel"), - eq(actions.type, "loop"), - eq(actions.type, "branch"), - like(actions.type, "hristudio-core%") - ), - limit: 10 - }); + const actions = await db.query.actions.findMany({ + where: (actions, { or, eq, like }) => + or( + eq(actions.type, "sequence"), + eq(actions.type, "parallel"), + eq(actions.type, "loop"), + eq(actions.type, "branch"), + like(actions.type, "hristudio-core%"), + ), + limit: 10, + }); - console.log(`Found ${actions.length} control actions.`); + console.log(`Found ${actions.length} control actions.`); - for (const action of actions) { - console.log(`\nAction: ${action.name} (${action.type})`); - console.log(`ID: ${action.id}`); - // Explicitly log parameters to check structure - console.log("Parameters:", JSON.stringify(action.parameters, null, 2)); + for (const action of actions) { + console.log(`\nAction: ${action.name} (${action.type})`); + console.log(`ID: ${action.id}`); + // Explicitly log parameters to check structure + console.log("Parameters:", JSON.stringify(action.parameters, null, 2)); - const params = action.parameters as any; - if (params.children) { - console.log(`✅ Has ${params.children.length} children in parameters.`); - } else if (params.trueBranch || params.falseBranch) { - console.log(`✅ Has branches in parameters.`); - } else { - console.log(`❌ No children/branches found in parameters.`); - } + const params = action.parameters as any; + if (params.children) { + console.log(`✅ Has ${params.children.length} children in parameters.`); + } else if (params.trueBranch || params.falseBranch) { + console.log(`✅ Has branches in parameters.`); + } else { + console.log(`❌ No children/branches found in parameters.`); } + } - await connection.end(); + await connection.end(); } main(); diff --git a/scripts/archive/check-db.ts b/scripts/archive/check-db.ts index 26a5d56..d86dc9a 100644 --- a/scripts/archive/check-db.ts +++ b/scripts/archive/check-db.ts @@ -1,4 +1,3 @@ - import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "../../src/server/db/schema"; @@ -9,57 +8,59 @@ const connection = postgres(connectionString); const db = drizzle(connection, { schema }); async function main() { - console.log("🔍 Checking Database State..."); + console.log("🔍 Checking Database State..."); - // 1. Check Plugin - const plugins = await db.query.plugins.findMany(); - console.log(`\nFound ${plugins.length} plugins.`); + // 1. Check Plugin + const plugins = await db.query.plugins.findMany(); + console.log(`\nFound ${plugins.length} plugins.`); - const expectedKeys = new Set(); + const expectedKeys = new Set(); - for (const p of plugins) { - const meta = p.metadata as any; - const defs = p.actionDefinitions as any[]; + for (const p of plugins) { + const meta = p.metadata as any; + const defs = p.actionDefinitions as any[]; - console.log(`Plugin [${p.name}] (ID: ${p.id}):`); - console.log(` - Robot ID (Column): ${p.robotId}`); - console.log(` - Metadata.robotId: ${meta?.robotId}`); - console.log(` - Action Definitions: ${defs?.length ?? 0} found.`); + console.log(`Plugin [${p.name}] (ID: ${p.id}):`); + console.log(` - Robot ID (Column): ${p.robotId}`); + console.log(` - Metadata.robotId: ${meta?.robotId}`); + console.log(` - Action Definitions: ${defs?.length ?? 0} found.`); - if (defs && meta?.robotId) { - defs.forEach(d => { - const key = `${meta.robotId}.${d.id}`; - expectedKeys.add(key); - // console.log(` -> Registers: ${key}`); - }); - } + if (defs && meta?.robotId) { + defs.forEach((d) => { + const key = `${meta.robotId}.${d.id}`; + expectedKeys.add(key); + // console.log(` -> Registers: ${key}`); + }); } + } - // 2. Check Actions - const actions = await db.query.actions.findMany(); - console.log(`\nFound ${actions.length} actions.`); - let errorCount = 0; - for (const a of actions) { - // Only check plugin actions - if (a.sourceKind === 'plugin' || a.type.includes(".")) { - const isRegistered = expectedKeys.has(a.type); - const pluginIdMatch = a.pluginId === 'nao6-ros2'; + // 2. Check Actions + const actions = await db.query.actions.findMany(); + console.log(`\nFound ${actions.length} actions.`); + let errorCount = 0; + for (const a of actions) { + // Only check plugin actions + if (a.sourceKind === "plugin" || a.type.includes(".")) { + const isRegistered = expectedKeys.has(a.type); + const pluginIdMatch = a.pluginId === "nao6-ros2"; - console.log(`Action [${a.name}] (Type: ${a.type}):`); - console.log(` - PluginId: ${a.pluginId} ${pluginIdMatch ? '✅' : '❌'}`); - console.log(` - In Registry: ${isRegistered ? '✅' : '❌'}`); + console.log(`Action [${a.name}] (Type: ${a.type}):`); + console.log(` - PluginId: ${a.pluginId} ${pluginIdMatch ? "✅" : "❌"}`); + console.log(` - In Registry: ${isRegistered ? "✅" : "❌"}`); - if (!isRegistered || !pluginIdMatch) errorCount++; - } + if (!isRegistered || !pluginIdMatch) errorCount++; } + } - if (errorCount > 0) { - console.log(`\n❌ Found ${errorCount} actions with issues.`); - } else { - console.log("\n✅ All plugin actions validated successfully against registry definitions."); - } + if (errorCount > 0) { + console.log(`\n❌ Found ${errorCount} actions with issues.`); + } else { + console.log( + "\n✅ All plugin actions validated successfully against registry definitions.", + ); + } - await connection.end(); + await connection.end(); } main().catch(console.error); diff --git a/scripts/archive/debug-experiment-structure.ts b/scripts/archive/debug-experiment-structure.ts index f1e6dd9..bc9b15e 100644 --- a/scripts/archive/debug-experiment-structure.ts +++ b/scripts/archive/debug-experiment-structure.ts @@ -1,58 +1,60 @@ - import { db } from "~/server/db"; import { steps, experiments, actions } from "~/server/db/schema"; import { eq, asc } from "drizzle-orm"; async function debugExperimentStructure() { - console.log("Debugging Experiment Structure for Interactive Storyteller..."); + console.log("Debugging Experiment Structure for Interactive Storyteller..."); - // Find the experiment - const experiment = await db.query.experiments.findFirst({ - where: eq(experiments.name, "The Interactive Storyteller"), + // Find the experiment + const experiment = await db.query.experiments.findFirst({ + where: eq(experiments.name, "The Interactive Storyteller"), + with: { + steps: { + orderBy: [asc(steps.orderIndex)], with: { - steps: { - orderBy: [asc(steps.orderIndex)], - with: { - actions: { - orderBy: [asc(actions.orderIndex)], - } - } - } - } - }); + actions: { + orderBy: [asc(actions.orderIndex)], + }, + }, + }, + }, + }); - if (!experiment) { - console.error("Experiment not found!"); - return; + if (!experiment) { + console.error("Experiment not found!"); + return; + } + + console.log(`Experiment: ${experiment.name} (${experiment.id})`); + console.log(`Plugin Dependencies:`, experiment.pluginDependencies); + console.log("---------------------------------------------------"); + + experiment.steps.forEach((step, index) => { + console.log(`Step ${index + 1}: ${step.name}`); + console.log(` ID: ${step.id}`); + console.log(` Type: ${step.type}`); + console.log(` Order: ${step.orderIndex}`); + console.log(` Conditions:`, JSON.stringify(step.conditions, null, 2)); + + if (step.actions && step.actions.length > 0) { + console.log(` Actions (${step.actions.length}):`); + step.actions.forEach((action, actionIndex) => { + console.log(` ${actionIndex + 1}. [${action.type}] ${action.name}`); + if (action.type === "wizard_wait_for_response") { + console.log( + ` Options:`, + JSON.stringify((action.parameters as any)?.options, null, 2), + ); + } + }); } - - console.log(`Experiment: ${experiment.name} (${experiment.id})`); - console.log(`Plugin Dependencies:`, experiment.pluginDependencies); console.log("---------------------------------------------------"); - - experiment.steps.forEach((step, index) => { - console.log(`Step ${index + 1}: ${step.name}`); - console.log(` ID: ${step.id}`); - console.log(` Type: ${step.type}`); - console.log(` Order: ${step.orderIndex}`); - console.log(` Conditions:`, JSON.stringify(step.conditions, null, 2)); - - if (step.actions && step.actions.length > 0) { - console.log(` Actions (${step.actions.length}):`); - step.actions.forEach((action, actionIndex) => { - console.log(` ${actionIndex + 1}. [${action.type}] ${action.name}`); - if (action.type === 'wizard_wait_for_response') { - console.log(` Options:`, JSON.stringify((action.parameters as any)?.options, null, 2)); - } - }); - } - console.log("---------------------------------------------------"); - }); + }); } debugExperimentStructure() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/inspect-all-steps.ts b/scripts/archive/inspect-all-steps.ts index 34dc499..5cc6bd8 100644 --- a/scripts/archive/inspect-all-steps.ts +++ b/scripts/archive/inspect-all-steps.ts @@ -1,42 +1,41 @@ - import { db } from "../../src/server/db"; import { experiments, steps } from "../../src/server/db/schema"; import { eq } from "drizzle-orm"; async function inspectAllSteps() { - const result = await db.query.experiments.findMany({ - with: { - steps: { - orderBy: (steps, { asc }) => [asc(steps.orderIndex)], - columns: { - id: true, - name: true, - type: true, - orderIndex: true, - conditions: true, - } - } - } - }); + const result = await db.query.experiments.findMany({ + with: { + steps: { + orderBy: (steps, { asc }) => [asc(steps.orderIndex)], + columns: { + id: true, + name: true, + type: true, + orderIndex: true, + conditions: true, + }, + }, + }, + }); - console.log(`Found ${result.length} experiments.`); + console.log(`Found ${result.length} experiments.`); - for (const exp of result) { - console.log(`Experiment: ${exp.name} (${exp.id})`); - for (const step of exp.steps) { - // Only print conditional steps or the first step - if (step.type === 'conditional' || step.orderIndex === 0) { - console.log(` [${step.orderIndex}] ${step.name} (${step.type})`); - console.log(` Conditions: ${JSON.stringify(step.conditions)}`); - } - } - console.log('---'); + for (const exp of result) { + console.log(`Experiment: ${exp.name} (${exp.id})`); + for (const step of exp.steps) { + // Only print conditional steps or the first step + if (step.type === "conditional" || step.orderIndex === 0) { + console.log(` [${step.orderIndex}] ${step.name} (${step.type})`); + console.log(` Conditions: ${JSON.stringify(step.conditions)}`); + } } + console.log("---"); + } } inspectAllSteps() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/inspect-branch-action.ts b/scripts/archive/inspect-branch-action.ts index c7df93d..387ba9c 100644 --- a/scripts/archive/inspect-branch-action.ts +++ b/scripts/archive/inspect-branch-action.ts @@ -1,47 +1,46 @@ - import { db } from "~/server/db"; import { actions, steps } from "~/server/db/schema"; import { eq } from "drizzle-orm"; async function inspectAction() { - console.log("Inspecting Action 10851aef-e720-45fc-ba5e-05e1e3425dab..."); + console.log("Inspecting Action 10851aef-e720-45fc-ba5e-05e1e3425dab..."); - const actionId = "10851aef-e720-45fc-ba5e-05e1e3425dab"; + const actionId = "10851aef-e720-45fc-ba5e-05e1e3425dab"; - const action = await db.query.actions.findFirst({ - where: eq(actions.id, actionId), - with: { - step: { - columns: { - id: true, - name: true, - type: true, - conditions: true - } - } - } - }); + const action = await db.query.actions.findFirst({ + where: eq(actions.id, actionId), + with: { + step: { + columns: { + id: true, + name: true, + type: true, + conditions: true, + }, + }, + }, + }); - if (!action) { - console.error("Action not found!"); - return; - } + if (!action) { + console.error("Action not found!"); + return; + } - console.log("Action Found:"); - console.log(" Name:", action.name); - console.log(" Type:", action.type); - console.log(" Parameters:", JSON.stringify(action.parameters, null, 2)); + console.log("Action Found:"); + console.log(" Name:", action.name); + console.log(" Type:", action.type); + console.log(" Parameters:", JSON.stringify(action.parameters, null, 2)); - console.log("Parent Step:"); - console.log(" ID:", action.step.id); - console.log(" Name:", action.step.name); - console.log(" Type:", action.step.type); - console.log(" Conditions:", JSON.stringify(action.step.conditions, null, 2)); + console.log("Parent Step:"); + console.log(" ID:", action.step.id); + console.log(" Name:", action.step.name); + console.log(" Type:", action.step.type); + console.log(" Conditions:", JSON.stringify(action.step.conditions, null, 2)); } inspectAction() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/inspect-branch-steps.ts b/scripts/archive/inspect-branch-steps.ts index ab62351..c9fe639 100644 --- a/scripts/archive/inspect-branch-steps.ts +++ b/scripts/archive/inspect-branch-steps.ts @@ -1,30 +1,29 @@ - import { db } from "~/server/db"; import { steps } from "~/server/db/schema"; import { eq, inArray } from "drizzle-orm"; async function inspectBranchSteps() { - console.log("Inspecting Steps 4 (Branch A) and 5 (Branch B)..."); + console.log("Inspecting Steps 4 (Branch A) and 5 (Branch B)..."); - const step4Id = "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5"; - const step5Id = "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30"; + const step4Id = "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5"; + const step5Id = "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30"; - const branchSteps = await db.query.steps.findMany({ - where: inArray(steps.id, [step4Id, step5Id]) - }); + const branchSteps = await db.query.steps.findMany({ + where: inArray(steps.id, [step4Id, step5Id]), + }); - branchSteps.forEach(step => { - console.log(`Step: ${step.name} (${step.id})`); - console.log(` Type: ${step.type}`); - console.log(` Order: ${step.orderIndex}`); - console.log(` Conditions:`, JSON.stringify(step.conditions, null, 2)); - console.log("---------------------------------------------------"); - }); + branchSteps.forEach((step) => { + console.log(`Step: ${step.name} (${step.id})`); + console.log(` Type: ${step.type}`); + console.log(` Order: ${step.orderIndex}`); + console.log(` Conditions:`, JSON.stringify(step.conditions, null, 2)); + console.log("---------------------------------------------------"); + }); } inspectBranchSteps() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/inspect-db.ts b/scripts/archive/inspect-db.ts index c3c9e07..68f3fb9 100644 --- a/scripts/archive/inspect-db.ts +++ b/scripts/archive/inspect-db.ts @@ -1,24 +1,29 @@ - import { db } from "../../src/server/db"; import { steps } from "../../src/server/db/schema"; import { eq, like } from "drizzle-orm"; async function checkSteps() { - const allSteps = await db.select().from(steps).where(like(steps.name, "%Comprehension Check%")); + const allSteps = await db + .select() + .from(steps) + .where(like(steps.name, "%Comprehension Check%")); - console.log("Found steps:", allSteps.length); + console.log("Found steps:", allSteps.length); - for (const step of allSteps) { - console.log("Step Name:", step.name); - console.log("Type:", step.type); - console.log("Conditions (typeof):", typeof step.conditions); - console.log("Conditions (value):", JSON.stringify(step.conditions, null, 2)); - } + for (const step of allSteps) { + console.log("Step Name:", step.name); + console.log("Type:", step.type); + console.log("Conditions (typeof):", typeof step.conditions); + console.log( + "Conditions (value):", + JSON.stringify(step.conditions, null, 2), + ); + } } checkSteps() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/inspect-step.ts b/scripts/archive/inspect-step.ts index a3519fa..9a044ee 100644 --- a/scripts/archive/inspect-step.ts +++ b/scripts/archive/inspect-step.ts @@ -1,61 +1,62 @@ - - import { db } from "~/server/db"; import { steps, experiments } from "~/server/db/schema"; import { eq, asc } from "drizzle-orm"; async function inspectExperimentSteps() { - // Find experiment by ID - const experiment = await db.query.experiments.findFirst({ - where: eq(experiments.id, "961d0cb1-256d-4951-8387-6d855a0ae603") - }); + // Find experiment by ID + const experiment = await db.query.experiments.findFirst({ + where: eq(experiments.id, "961d0cb1-256d-4951-8387-6d855a0ae603"), + }); - if (!experiment) { - console.log("Experiment not found!"); - return; + if (!experiment) { + console.log("Experiment not found!"); + return; + } + + console.log(`Inspecting Experiment: ${experiment.name} (${experiment.id})`); + + const experimentSteps = await db.query.steps.findMany({ + where: eq(steps.experimentId, experiment.id), + orderBy: [asc(steps.orderIndex)], + with: { + actions: { + orderBy: (actions, { asc }) => [asc(actions.orderIndex)], + }, + }, + }); + + console.log(`Found ${experimentSteps.length} steps.`); + + for (const step of experimentSteps) { + console.log("--------------------------------------------------"); + console.log(`Step [${step.orderIndex}] ID: ${step.id}`); + console.log(`Name: ${step.name}`); + console.log(`Type: ${step.type}`); + + if (step.type === "conditional") { + console.log("Conditions:", JSON.stringify(step.conditions, null, 2)); } - console.log(`Inspecting Experiment: ${experiment.name} (${experiment.id})`); - - const experimentSteps = await db.query.steps.findMany({ - where: eq(steps.experimentId, experiment.id), - orderBy: [asc(steps.orderIndex)], - with: { - actions: { - orderBy: (actions, { asc }) => [asc(actions.orderIndex)] - } - } - }); - - console.log(`Found ${experimentSteps.length} steps.`); - - for (const step of experimentSteps) { - console.log("--------------------------------------------------"); - console.log(`Step [${step.orderIndex}] ID: ${step.id}`); - console.log(`Name: ${step.name}`); - console.log(`Type: ${step.type}`); - - - if (step.type === 'conditional') { - console.log("Conditions:", JSON.stringify(step.conditions, null, 2)); - } - - if (step.actions.length > 0) { - console.log("Actions:"); - for (const action of step.actions) { - console.log(` - [${action.orderIndex}] ${action.name} (${action.type})`); - if (action.type === 'wizard_wait_for_response') { - console.log(" Parameters:", JSON.stringify(action.parameters, null, 2)); - } - } + if (step.actions.length > 0) { + console.log("Actions:"); + for (const action of step.actions) { + console.log( + ` - [${action.orderIndex}] ${action.name} (${action.type})`, + ); + if (action.type === "wizard_wait_for_response") { + console.log( + " Parameters:", + JSON.stringify(action.parameters, null, 2), + ); } + } } + } } inspectExperimentSteps() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); - + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/inspect-visual-design.ts b/scripts/archive/inspect-visual-design.ts index c5ee946..a384612 100644 --- a/scripts/archive/inspect-visual-design.ts +++ b/scripts/archive/inspect-visual-design.ts @@ -1,33 +1,32 @@ - import { db } from "../../src/server/db"; import { experiments } from "../../src/server/db/schema"; import { eq } from "drizzle-orm"; async function inspectVisualDesign() { - const exps = await db.select().from(experiments); + const exps = await db.select().from(experiments); - for (const exp of exps) { - console.log(`Experiment: ${exp.name}`); - if (exp.visualDesign) { - const vd = exp.visualDesign as any; - console.log("Visual Design Steps:"); - if (vd.steps && Array.isArray(vd.steps)) { - vd.steps.forEach((s: any, i: number) => { - console.log(` [${i}] ${s.name} (${s.type})`); - console.log(` Trigger: ${JSON.stringify(s.trigger)}`); - }); - } else { - console.log(" No steps in visualDesign or invalid format."); - } - } else { - console.log(" No visualDesign blob."); - } + for (const exp of exps) { + console.log(`Experiment: ${exp.name}`); + if (exp.visualDesign) { + const vd = exp.visualDesign as any; + console.log("Visual Design Steps:"); + if (vd.steps && Array.isArray(vd.steps)) { + vd.steps.forEach((s: any, i: number) => { + console.log(` [${i}] ${s.name} (${s.type})`); + console.log(` Trigger: ${JSON.stringify(s.trigger)}`); + }); + } else { + console.log(" No steps in visualDesign or invalid format."); + } + } else { + console.log(" No visualDesign blob."); } + } } inspectVisualDesign() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/patch-branch-action-params.ts b/scripts/archive/patch-branch-action-params.ts index 50fa836..a2417e7 100644 --- a/scripts/archive/patch-branch-action-params.ts +++ b/scripts/archive/patch-branch-action-params.ts @@ -1,69 +1,74 @@ - import { db } from "~/server/db"; import { actions, steps } from "~/server/db/schema"; import { eq, sql } from "drizzle-orm"; async function patchActionParams() { - console.log("Patching Action Parameters for Interactive Storyteller..."); + console.log("Patching Action Parameters for Interactive Storyteller..."); - // Target Step IDs - const step3CondId = "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85"; // Step 3: Comprehension Check - const actionId = "10851aef-e720-45fc-ba5e-05e1e3425dab"; // Action: Wait for Choice + // Target Step IDs + const step3CondId = "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85"; // Step 3: Comprehension Check + const actionId = "10851aef-e720-45fc-ba5e-05e1e3425dab"; // Action: Wait for Choice - // 1. Get the authoritative conditions from the Step - const step = await db.query.steps.findFirst({ - where: eq(steps.id, step3CondId) - }); + // 1. Get the authoritative conditions from the Step + const step = await db.query.steps.findFirst({ + where: eq(steps.id, step3CondId), + }); - if (!step) { - console.error("Step 3 not found!"); - return; - } + if (!step) { + console.error("Step 3 not found!"); + return; + } - const conditions = step.conditions as any; - const richOptions = conditions?.options; + const conditions = step.conditions as any; + const richOptions = conditions?.options; - if (!richOptions || !Array.isArray(richOptions)) { - console.error("Step 3 conditions are missing valid options!"); - return; - } + if (!richOptions || !Array.isArray(richOptions)) { + console.error("Step 3 conditions are missing valid options!"); + return; + } - console.log("Found rich options in Step:", JSON.stringify(richOptions, null, 2)); + console.log( + "Found rich options in Step:", + JSON.stringify(richOptions, null, 2), + ); - // 2. Get the Action - const action = await db.query.actions.findFirst({ - where: eq(actions.id, actionId) - }); + // 2. Get the Action + const action = await db.query.actions.findFirst({ + where: eq(actions.id, actionId), + }); - if (!action) { - console.error("Action not found!"); - return; - } + if (!action) { + console.error("Action not found!"); + return; + } - console.log("Current Action Parameters:", JSON.stringify(action.parameters, null, 2)); + console.log( + "Current Action Parameters:", + JSON.stringify(action.parameters, null, 2), + ); - // 3. Patch the Action Parameters - // We replace the simple string options with the rich object options - const currentParams = action.parameters as any; - const newParams = { - ...currentParams, - options: richOptions // Overwrite with rich options from step - }; + // 3. Patch the Action Parameters + // We replace the simple string options with the rich object options + const currentParams = action.parameters as any; + const newParams = { + ...currentParams, + options: richOptions, // Overwrite with rich options from step + }; - console.log("New Action Parameters:", JSON.stringify(newParams, null, 2)); + console.log("New Action Parameters:", JSON.stringify(newParams, null, 2)); - await db.execute(sql` + await db.execute(sql` UPDATE hs_action SET parameters = ${JSON.stringify(newParams)}::jsonb WHERE id = ${actionId} `); - console.log("Action parameters successfully patched."); + console.log("Action parameters successfully patched."); } patchActionParams() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/patch-branch-steps.ts b/scripts/archive/patch-branch-steps.ts index e4e676c..442b2cd 100644 --- a/scripts/archive/patch-branch-steps.ts +++ b/scripts/archive/patch-branch-steps.ts @@ -1,92 +1,100 @@ - import { db } from "~/server/db"; import { steps } from "~/server/db/schema"; import { eq, sql } from "drizzle-orm"; async function patchBranchSteps() { - console.log("Patching branch steps for Interactive Storyteller..."); + console.log("Patching branch steps for Interactive Storyteller..."); - // Target Step IDs (From debug output) - const step3CondId = "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85"; // Step 3: Comprehension Check - const stepBranchAId = "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5"; // Step 4: Branch A (Correct) - const stepBranchBId = "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30"; // Step 5: Branch B (Incorrect) - const stepConclusionId = "cc3fbc7f-29e5-45e0-8d46-e80813c54292"; // Step 6: Conclusion + // Target Step IDs (From debug output) + const step3CondId = "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85"; // Step 3: Comprehension Check + const stepBranchAId = "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5"; // Step 4: Branch A (Correct) + const stepBranchBId = "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30"; // Step 5: Branch B (Incorrect) + const stepConclusionId = "cc3fbc7f-29e5-45e0-8d46-e80813c54292"; // Step 6: Conclusion - // Update Step 3 (The Conditional Step) - console.log("Updating Step 3 (Conditional Step)..."); - const step3Conditional = await db.query.steps.findFirst({ - where: eq(steps.id, step3CondId) + // Update Step 3 (The Conditional Step) + console.log("Updating Step 3 (Conditional Step)..."); + const step3Conditional = await db.query.steps.findFirst({ + where: eq(steps.id, step3CondId), + }); + + if (step3Conditional) { + const currentConditions = (step3Conditional.conditions as any) || {}; + const options = currentConditions.options || []; + + // Patch options to point to real step IDs + const newOptions = options.map((opt: any) => { + if (opt.value === "Correct") return { ...opt, nextStepId: stepBranchAId }; + if (opt.value === "Incorrect") + return { ...opt, nextStepId: stepBranchBId }; + return opt; }); - if (step3Conditional) { - const currentConditions = (step3Conditional.conditions as any) || {}; - const options = currentConditions.options || []; + const newConditions = { ...currentConditions, options: newOptions }; - // Patch options to point to real step IDs - const newOptions = options.map((opt: any) => { - if (opt.value === "Correct") return { ...opt, nextStepId: stepBranchAId }; - if (opt.value === "Incorrect") return { ...opt, nextStepId: stepBranchBId }; - return opt; - }); - - const newConditions = { ...currentConditions, options: newOptions }; - - await db.execute(sql` + await db.execute(sql` UPDATE hs_step SET conditions = ${JSON.stringify(newConditions)}::jsonb WHERE id = ${step3CondId} `); - console.log("Step 3 (Conditional) updated links."); - } else { - console.log("Step 3 (Conditional) not found."); - } + console.log("Step 3 (Conditional) updated links."); + } else { + console.log("Step 3 (Conditional) not found."); + } - // Update Step 4 (Branch A) - console.log("Updating Step 4 (Branch A)..."); - /* + // Update Step 4 (Branch A) + console.log("Updating Step 4 (Branch A)..."); + /* Note: We already patched Step 4 in previous run but under wrong assumption? Let's re-patch to be safe. Debug output showed ID: 3a2dc0b7-a43e-4236-9b9e-f957abafc1e5 It should jump to Conclusion (cc3fbc7f...) */ - const stepBranchA = await db.query.steps.findFirst({ - where: eq(steps.id, stepBranchAId) - }); + const stepBranchA = await db.query.steps.findFirst({ + where: eq(steps.id, stepBranchAId), + }); - if (stepBranchA) { - const currentConditions = (stepBranchA.conditions as Record) || {}; - const newConditions = { ...currentConditions, nextStepId: stepConclusionId }; + if (stepBranchA) { + const currentConditions = + (stepBranchA.conditions as Record) || {}; + const newConditions = { + ...currentConditions, + nextStepId: stepConclusionId, + }; - await db.execute(sql` + await db.execute(sql` UPDATE hs_step SET conditions = ${JSON.stringify(newConditions)}::jsonb WHERE id = ${stepBranchAId} `); - console.log("Step 4 (Branch A) updated jump target."); - } + console.log("Step 4 (Branch A) updated jump target."); + } - // Update Step 5 (Branch B) - console.log("Updating Step 5 (Branch B)..."); - const stepBranchB = await db.query.steps.findFirst({ - where: eq(steps.id, stepBranchBId) - }); + // Update Step 5 (Branch B) + console.log("Updating Step 5 (Branch B)..."); + const stepBranchB = await db.query.steps.findFirst({ + where: eq(steps.id, stepBranchBId), + }); - if (stepBranchB) { - const currentConditions = (stepBranchB.conditions as Record) || {}; - const newConditions = { ...currentConditions, nextStepId: stepConclusionId }; + if (stepBranchB) { + const currentConditions = + (stepBranchB.conditions as Record) || {}; + const newConditions = { + ...currentConditions, + nextStepId: stepConclusionId, + }; - await db.execute(sql` + await db.execute(sql` UPDATE hs_step SET conditions = ${JSON.stringify(newConditions)}::jsonb WHERE id = ${stepBranchBId} `); - console.log("Step 5 (Branch B) updated jump target."); - } + console.log("Step 5 (Branch B) updated jump target."); + } } patchBranchSteps() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/reproduce-hydration.ts b/scripts/archive/reproduce-hydration.ts index 6c07e1c..003b80b 100644 --- a/scripts/archive/reproduce-hydration.ts +++ b/scripts/archive/reproduce-hydration.ts @@ -1,43 +1,52 @@ - import { convertDatabaseToSteps } from "../../src/lib/experiment-designer/block-converter"; import { type ExperimentStep } from "../../src/lib/experiment-designer/types"; // Mock DB Steps (simulating what experimentsRouter returns before conversion) const mockDbSteps = [ - { - id: "step-1", - name: "Step 1", - type: "wizard", - orderIndex: 0, - actions: [ + { + id: "step-1", + name: "Step 1", + type: "wizard", + orderIndex: 0, + actions: [ + { + id: "seq-1", + name: "Test Sequence", + type: "sequence", + parameters: { + children: [ { - id: "seq-1", - name: "Test Sequence", - type: "sequence", - parameters: { - children: [ - { id: "child-1", name: "Child 1", type: "wait", parameters: { duration: 1 } }, - { id: "child-2", name: "Child 2", type: "wait", parameters: { duration: 2 } } - ] - } - } - ] - } + id: "child-1", + name: "Child 1", + type: "wait", + parameters: { duration: 1 }, + }, + { + id: "child-2", + name: "Child 2", + type: "wait", + parameters: { duration: 2 }, + }, + ], + }, + }, + ], + }, ]; // Mock Store Logic (simulating store.ts) function cloneActions(actions: any[]): any[] { - return actions.map((a) => ({ - ...a, - children: a.children ? cloneActions(a.children) : undefined, - })); + return actions.map((a) => ({ + ...a, + children: a.children ? cloneActions(a.children) : undefined, + })); } function cloneSteps(steps: any[]): any[] { - return steps.map((s) => ({ - ...s, - actions: cloneActions(s.actions), - })); + return steps.map((s) => ({ + ...s, + actions: cloneActions(s.actions), + })); } console.log("🔹 Testing Hydration & Cloning..."); @@ -47,15 +56,15 @@ const runtimeSteps = convertDatabaseToSteps(mockDbSteps); const seq = runtimeSteps[0]?.actions[0]; if (!seq) { - console.error("❌ Conversion Failed: Sequence action not found."); - process.exit(1); + console.error("❌ Conversion Failed: Sequence action not found."); + process.exit(1); } console.log(`Runtime Children Count: ${seq.children?.length ?? "undefined"}`); if (!seq.children || seq.children.length === 0) { - console.error("❌ Conversion Failed: Children not hydrated from parameters."); - process.exit(1); + console.error("❌ Conversion Failed: Children not hydrated from parameters."); + process.exit(1); } // 2. Store Cloning @@ -63,14 +72,16 @@ const clonedSteps = cloneSteps(runtimeSteps); const clonedSeq = clonedSteps[0]?.actions[0]; if (!clonedSeq) { - console.error("❌ Cloning Failed: Sequence action lost."); - process.exit(1); + console.error("❌ Cloning Failed: Sequence action lost."); + process.exit(1); } -console.log(`Cloned Children Count: ${clonedSeq.children?.length ?? "undefined"}`); +console.log( + `Cloned Children Count: ${clonedSeq.children?.length ?? "undefined"}`, +); if (clonedSeq.children?.length === 2) { - console.log("✅ SUCCESS: Data hydrated and cloned correctly."); + console.log("✅ SUCCESS: Data hydrated and cloned correctly."); } else { - console.error("❌ CLONING FAILED: Children lost during clone."); + console.error("❌ CLONING FAILED: Children lost during clone."); } diff --git a/scripts/archive/seed-control-demo-draft.ts b/scripts/archive/seed-control-demo-draft.ts index c0c3d4f..5754980 100644 --- a/scripts/archive/seed-control-demo-draft.ts +++ b/scripts/archive/seed-control-demo-draft.ts @@ -9,113 +9,128 @@ const connection = postgres(connectionString); const db = drizzle(connection, { schema }); async function main() { - console.log("🌱 Seeding 'Control Flow Demo' experiment..."); + console.log("🌱 Seeding 'Control Flow Demo' experiment..."); - try { - // 1. Find Admin User & Study - const user = await db.query.users.findFirst({ - where: (users, { eq }) => eq(users.email, "sean@soconnor.dev") - }); - if (!user) throw new Error("Admin user 'sean@soconnor.dev' not found. Run seed-dev.ts first."); + try { + // 1. Find Admin User & Study + const user = await db.query.users.findFirst({ + where: (users, { eq }) => eq(users.email, "sean@soconnor.dev"), + }); + if (!user) + throw new Error( + "Admin user 'sean@soconnor.dev' not found. Run seed-dev.ts first.", + ); - const study = await db.query.studies.findFirst({ - where: (studies, { eq }) => eq(studies.name, "Comparative WoZ Study") - }); - if (!study) throw new Error("Study 'Comparative WoZ Study' not found. Run seed-dev.ts first."); + const study = await db.query.studies.findFirst({ + where: (studies, { eq }) => eq(studies.name, "Comparative WoZ Study"), + }); + if (!study) + throw new Error( + "Study 'Comparative WoZ Study' not found. Run seed-dev.ts first.", + ); - // Find Robot - const robot = await db.query.robots.findFirst({ - where: (robots, { eq }) => eq(robots.name, "NAO6") - }); - if (!robot) throw new Error("Robot 'NAO6' not found. Run seed-dev.ts first."); + // Find Robot + const robot = await db.query.robots.findFirst({ + where: (robots, { eq }) => eq(robots.name, "NAO6"), + }); + if (!robot) + throw new Error("Robot 'NAO6' not found. Run seed-dev.ts first."); + // 2. Create Experiment + const [experiment] = await db + .insert(schema.experiments) + .values({ + studyId: study.id, + name: "Control Flow Demo", + description: + "Demonstration of enhanced control flow actions: Sequence, Parallel, Wait, Loop, Branch.", + version: 1, + status: "draft", + robotId: robot.id, + createdBy: user.id, + }) + .returning(); - // 2. Create Experiment - const [experiment] = await db.insert(schema.experiments).values({ - studyId: study.id, - name: "Control Flow Demo", - description: "Demonstration of enhanced control flow actions: Sequence, Parallel, Wait, Loop, Branch.", - version: 1, - status: "draft", - robotId: robot.id, - createdBy: user.id, - }).returning(); + if (!experiment) throw new Error("Failed to create experiment"); + console.log(`✅ Created Experiment: ${experiment.id}`); - if (!experiment) throw new Error("Failed to create experiment"); - console.log(`✅ Created Experiment: ${experiment.id}`); + // 3. Create Steps - // 3. Create Steps + // Step 1: Sequence & Parallel + const [step1] = await db + .insert(schema.steps) + .values({ + experimentId: experiment.id, + name: "Complex Action Structures", + description: "Demonstrating Sequence and Parallel groups", + type: "robot", + orderIndex: 0, + required: true, + durationEstimate: 30, + }) + .returning(); - // Step 1: Sequence & Parallel - const [step1] = await db.insert(schema.steps).values({ - experimentId: experiment.id, - name: "Complex Action Structures", - description: "Demonstrating Sequence and Parallel groups", - type: "robot", - orderIndex: 0, - required: true, - durationEstimate: 30 - }).returning(); + // Step 2: Loops & Waits + const [step2] = await db + .insert(schema.steps) + .values({ + experimentId: experiment.id, + name: "Repetition & Delays", + description: "Demonstrating Loop and Wait actions", + type: "robot", + orderIndex: 1, + required: true, + durationEstimate: 45, + }) + .returning(); - // Step 2: Loops & Waits - const [step2] = await db.insert(schema.steps).values({ - experimentId: experiment.id, - name: "Repetition & Delays", - description: "Demonstrating Loop and Wait actions", - type: "robot", - orderIndex: 1, - required: true, - durationEstimate: 45 - }).returning(); + // 4. Create Actions - // 4. Create Actions + // --- Step 1 Actions --- - // --- Step 1 Actions --- + // Top-level Sequence + const seqId = `seq-${Date.now()}`; + await db.insert(schema.actions).values({ + stepId: step1!.id, + name: "Introduction Sequence", + type: "sequence", // New type + orderIndex: 0, + parameters: {}, + pluginId: "hristudio-core", + category: "control", + // No explicit children column in schema? + // Wait, schema.actions has "children" as jsonb or it's a recursive relationship? + // Let's check schema/types. + // Looking at ActionChip, it expects `action.children`. + // In DB, it's likely stored in `children` jsonb column if it exists, OR we need to perform recursive inserts if schema supports parentId. + // Checking `types.ts` or schema... + // Assuming flat list references for now or JSONB. + // Wait, `ExperimentAction` in types has `children?: ExperimentAction[]`. + // If the DB schema `actions` table handles nesting via `parameters` or specific column, I need to know. + // Defaulting to "children" property in JSON parameter if DB doesn't have parentId. + // Checking `schema.ts`: "children" is likely NOT a column if I haven't seen it in seed-dev. + // However, `ActionChip` uses `action.children`. Steps map to `actions`. + // If `actions` table has `parentId` or `children` JSONB. + // I will assume `children` is part of the `parameters` or a simplified representation for now, + // BUT `FlowWorkspace` treats `action.children` as real actions. + // Let's check `schema.ts` quickly. + }); - // Top-level Sequence - const seqId = `seq-${Date.now()}`; - await db.insert(schema.actions).values({ - stepId: step1!.id, - name: "Introduction Sequence", - type: "sequence", // New type - orderIndex: 0, - parameters: {}, - pluginId: "hristudio-core", - category: "control", - // No explicit children column in schema? - // Wait, schema.actions has "children" as jsonb or it's a recursive relationship? - // Let's check schema/types. - // Looking at ActionChip, it expects `action.children`. - // In DB, it's likely stored in `children` jsonb column if it exists, OR we need to perform recursive inserts if schema supports parentId. - // Checking `types.ts` or schema... - // Assuming flat list references for now or JSONB. - // Wait, `ExperimentAction` in types has `children?: ExperimentAction[]`. - // If the DB schema `actions` table handles nesting via `parameters` or specific column, I need to know. - // Defaulting to "children" property in JSON parameter if DB doesn't have parentId. - // Checking `schema.ts`: "children" is likely NOT a column if I haven't seen it in seed-dev. - // However, `ActionChip` uses `action.children`. Steps map to `actions`. - // If `actions` table has `parentId` or `children` JSONB. - // I will assume `children` is part of the `parameters` or a simplified representation for now, - // BUT `FlowWorkspace` treats `action.children` as real actions. - // Let's check `schema.ts` quickly. - }); + // I need to check schema.actions definition effectively. + // For this pass, I will insert them as flat actions since I can't confirm nesting storage without checking schema. + // But the user WANTS to see the nesting (Sequence, Parallel). + // The `SortableActionChip` renders `action.children`. + // The `TrialExecutionEngine` executes `action.children`. + // So the data MUST include children. + // Most likely `actions` table has a `children` JSONB column. - // I need to check schema.actions definition effectively. - // For this pass, I will insert them as flat actions since I can't confirm nesting storage without checking schema. - // But the user WANTS to see the nesting (Sequence, Parallel). - // The `SortableActionChip` renders `action.children`. - // The `TrialExecutionEngine` executes `action.children`. - // So the data MUST include children. - // Most likely `actions` table has a `children` JSONB column. - - // I will insert a Parallel action with embedded children in the `children` column (if it exists) or `parameters`. - // Re-reading `scripts/seed-dev.ts`: It doesn't show any nested actions. - // I will read `src/server/db/schema.ts` to be sure. - - } catch (err) { - console.error(err); - process.exit(1); - } + // I will insert a Parallel action with embedded children in the `children` column (if it exists) or `parameters`. + // Re-reading `scripts/seed-dev.ts`: It doesn't show any nested actions. + // I will read `src/server/db/schema.ts` to be sure. + } catch (err) { + console.error(err); + process.exit(1); + } } // I'll write the file AFTER checking schema to ensure I structure the nested actions correctly. diff --git a/scripts/archive/seed-control-demo.ts b/scripts/archive/seed-control-demo.ts index 144d568..24da5de 100644 --- a/scripts/archive/seed-control-demo.ts +++ b/scripts/archive/seed-control-demo.ts @@ -1,4 +1,3 @@ - import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "../../src/server/db/schema"; @@ -11,231 +10,245 @@ const connection = postgres(connectionString); const db = drizzle(connection, { schema }); async function main() { - console.log("🌱 Seeding 'Control Flow Demo' experiment..."); + console.log("🌱 Seeding 'Control Flow Demo' experiment..."); - try { - // 1. Find Admin User & Study - const user = await db.query.users.findFirst({ - where: (users, { eq }) => eq(users.email, "sean@soconnor.dev") - }); - if (!user) throw new Error("Admin user 'sean@soconnor.dev' not found. Run seed-dev.ts first."); + try { + // 1. Find Admin User & Study + const user = await db.query.users.findFirst({ + where: (users, { eq }) => eq(users.email, "sean@soconnor.dev"), + }); + if (!user) + throw new Error( + "Admin user 'sean@soconnor.dev' not found. Run seed-dev.ts first.", + ); - const study = await db.query.studies.findFirst({ - where: (studies, { eq }) => eq(studies.name, "Comparative WoZ Study") - }); - if (!study) throw new Error("Study 'Comparative WoZ Study' not found. Run seed-dev.ts first."); + const study = await db.query.studies.findFirst({ + where: (studies, { eq }) => eq(studies.name, "Comparative WoZ Study"), + }); + if (!study) + throw new Error( + "Study 'Comparative WoZ Study' not found. Run seed-dev.ts first.", + ); - // Find Robot - const robot = await db.query.robots.findFirst({ - where: (robots, { eq }) => eq(robots.name, "NAO6") - }); - if (!robot) throw new Error("Robot 'NAO6' not found. Run seed-dev.ts first."); + // Find Robot + const robot = await db.query.robots.findFirst({ + where: (robots, { eq }) => eq(robots.name, "NAO6"), + }); + if (!robot) + throw new Error("Robot 'NAO6' not found. Run seed-dev.ts first."); + // 2. Create Experiment + const [experiment] = await db + .insert(schema.experiments) + .values({ + studyId: study.id, + name: "Control Flow Demo", + description: + "Demonstration of enhanced control flow actions: Sequence, Parallel, Wait, Loop, Branch.", + version: 1, + status: "draft", + robotId: robot.id, + createdBy: user.id, + }) + .returning(); - // 2. Create Experiment - const [experiment] = await db.insert(schema.experiments).values({ - studyId: study.id, - name: "Control Flow Demo", - description: "Demonstration of enhanced control flow actions: Sequence, Parallel, Wait, Loop, Branch.", - version: 1, - status: "draft", - robotId: robot.id, - createdBy: user.id, - }).returning(); + if (!experiment) throw new Error("Failed to create experiment"); + console.log(`✅ Created Experiment: ${experiment.id}`); - if (!experiment) throw new Error("Failed to create experiment"); - console.log(`✅ Created Experiment: ${experiment.id}`); + // 3. Create Steps - // 3. Create Steps + // Step 1: Sequence & Parallel + const [step1] = await db + .insert(schema.steps) + .values({ + experimentId: experiment.id, + name: "Complex Action Structures", + description: "Demonstrating Sequence and Parallel groups", + type: "robot", + orderIndex: 0, + required: true, + durationEstimate: 30, + }) + .returning(); + if (!step1) throw new Error("Failed to create step1"); - // Step 1: Sequence & Parallel - const [step1] = await db.insert(schema.steps).values({ - experimentId: experiment.id, - name: "Complex Action Structures", - description: "Demonstrating Sequence and Parallel groups", - type: "robot", - orderIndex: 0, - required: true, - durationEstimate: 30 - }).returning(); - if (!step1) throw new Error("Failed to create step1"); + // Step 2: Loops & Waits + const [step2] = await db + .insert(schema.steps) + .values({ + experimentId: experiment.id, + name: "Repetition & Delays", + description: "Demonstrating Loop and Wait actions", + type: "robot", + orderIndex: 1, + required: true, + durationEstimate: 45, + }) + .returning(); + if (!step2) throw new Error("Failed to create step2"); - // Step 2: Loops & Waits - const [step2] = await db.insert(schema.steps).values({ - experimentId: experiment.id, - name: "Repetition & Delays", - description: "Demonstrating Loop and Wait actions", - type: "robot", - orderIndex: 1, - required: true, - durationEstimate: 45 - }).returning(); - if (!step2) throw new Error("Failed to create step2"); + // 4. Create Actions - // 4. Create Actions + // --- Step 1 Actions --- - // --- Step 1 Actions --- + // Action 1: Sequence + // Note: Nested children are stored in 'children' property of the action object in frontend, + // but in DB 'parameters' is the JSONB field. + // However, looking at ActionChip, it expects `action.children`. + // The `ExperimentAction` type usually has `children` at top level. + // If the DB doesn't have it, the API must be hydrating it. + // BUT, for the purpose of this seed which writes to DB directly, I will put it in `parameters.children` + // and assume the frontend/API handles it or I'm missing a column. + // Actually, looking at schema again, `actions` table DOES NOT have children. + // So it MUST be in `parameters` or it's not persisted in this table structure yet (which would be a bug, but I'm seeding what exists). + // Wait, if I put it in parameters, does the UI read it? + // `ActionChip` reads `action.children`. + // I will try to put it in `parameters` and distinct `children` property in the JSON passed to `parameters`? + // No, `parameters` is jsonb. + // I will assume for now that the system expects it in parameters if it's not a column, OR it's not fully supported in DB yet. + // I will stick to what the UI likely consumes. `parameters: { children: [...] }` - // Action 1: Sequence - // Note: Nested children are stored in 'children' property of the action object in frontend, - // but in DB 'parameters' is the JSONB field. - // However, looking at ActionChip, it expects `action.children`. - // The `ExperimentAction` type usually has `children` at top level. - // If the DB doesn't have it, the API must be hydrating it. - // BUT, for the purpose of this seed which writes to DB directly, I will put it in `parameters.children` - // and assume the frontend/API handles it or I'm missing a column. - // Actually, looking at schema again, `actions` table DOES NOT have children. - // So it MUST be in `parameters` or it's not persisted in this table structure yet (which would be a bug, but I'm seeding what exists). - // Wait, if I put it in parameters, does the UI read it? - // `ActionChip` reads `action.children`. - // I will try to put it in `parameters` and distinct `children` property in the JSON passed to `parameters`? - // No, `parameters` is jsonb. - // I will assume for now that the system expects it in parameters if it's not a column, OR it's not fully supported in DB yet. - // I will stick to what the UI likely consumes. `parameters: { children: [...] }` + // Sequence + await db.insert(schema.actions).values({ + stepId: step1.id, + name: "Introduction Sequence", + type: "sequence", + orderIndex: 0, + // Embedding children here to demonstrate. + // Real implementation might vary if keys are strictly checked. + parameters: { + children: [ + { + id: uuidv4(), + name: "Say Hello", + type: "nao6-ros2.say_text", + parameters: { text: "Hello there!" }, + category: "interaction", + }, + { + id: uuidv4(), + name: "Wave Hand", + type: "nao6-ros2.move_arm", + parameters: { arm: "right", action: "wave" }, + category: "movement", + }, + ], + }, + pluginId: "hristudio-core", + category: "control", + sourceKind: "core", + }); - // Sequence - await db.insert(schema.actions).values({ - stepId: step1.id, - name: "Introduction Sequence", - type: "sequence", - orderIndex: 0, - // Embedding children here to demonstrate. - // Real implementation might vary if keys are strictly checked. - parameters: { - children: [ - { - id: uuidv4(), - name: "Say Hello", - type: "nao6-ros2.say_text", - parameters: { text: "Hello there!" }, - category: "interaction" - }, - { - id: uuidv4(), - name: "Wave Hand", - type: "nao6-ros2.move_arm", - parameters: { arm: "right", action: "wave" }, - category: "movement" - } - ] + // Parallel + await db.insert(schema.actions).values({ + stepId: step1.id, + name: "Parallel Actions", + type: "parallel", + orderIndex: 1, + parameters: { + children: [ + { + id: uuidv4(), + name: "Say 'Moving'", + type: "nao6-ros2.say_text", + parameters: { text: "I am moving and talking." }, + category: "interaction", + }, + { + id: uuidv4(), + name: "Walk Forward", + type: "nao6-ros2.move_to", + parameters: { x: 0.5, y: 0 }, + category: "movement", + }, + ], + }, + pluginId: "hristudio-core", + category: "control", + sourceKind: "core", + }); + + // --- Step 2 Actions --- + + // Loop + await db.insert(schema.actions).values({ + stepId: step2.id, + name: "Repeat Message", + type: "loop", + orderIndex: 0, + parameters: { + iterations: 3, + children: [ + { + id: uuidv4(), + name: "Say 'Echo'", + type: "nao6-ros2.say_text", + parameters: { text: "Echo" }, + category: "interaction", + }, + ], + }, + pluginId: "hristudio-core", + category: "control", + sourceKind: "core", + }); + + // Wait + await db.insert(schema.actions).values({ + stepId: step2.id, + name: "Wait 5 Seconds", + type: "wait", + orderIndex: 1, + parameters: { duration: 5 }, + pluginId: "hristudio-core", + category: "control", + sourceKind: "core", + }); + + // Branch (Controls step routing, not nested actions) + // Note: Branch configuration is stored in step.trigger.conditions, not action.parameters + // The branch action itself is just a marker that this step has conditional routing + await db.insert(schema.actions).values({ + stepId: step2.id, + name: "Conditional Routing", + type: "branch", + orderIndex: 2, + parameters: { + // Branch actions don't have nested children + // Routing is configured at the step level via trigger.conditions + }, + pluginId: "hristudio-core", + category: "control", + sourceKind: "core", + }); + + // Update step2 to have conditional routing + await db + .update(schema.steps) + .set({ + type: "conditional", + conditions: { + options: [ + { + label: "High Score Path", + nextStepIndex: 2, // Would go to a hypothetical step 3 + variant: "default", }, - pluginId: "hristudio-core", - category: "control", - sourceKind: "core" - }); - - // Parallel - await db.insert(schema.actions).values({ - stepId: step1.id, - name: "Parallel Actions", - type: "parallel", - orderIndex: 1, - parameters: { - children: [ - { - id: uuidv4(), - name: "Say 'Moving'", - type: "nao6-ros2.say_text", - parameters: { text: "I am moving and talking." }, - category: "interaction" - }, - { - id: uuidv4(), - name: "Walk Forward", - type: "nao6-ros2.move_to", - parameters: { x: 0.5, y: 0 }, - category: "movement" - } - ] + { + label: "Low Score Path", + nextStepIndex: 0, // Loop back to step 1 + variant: "outline", }, - pluginId: "hristudio-core", - category: "control", - sourceKind: "core" - }); - - - // --- Step 2 Actions --- - - // Loop - await db.insert(schema.actions).values({ - stepId: step2.id, - name: "Repeat Message", - type: "loop", - orderIndex: 0, - parameters: { - iterations: 3, - children: [ - { - id: uuidv4(), - name: "Say 'Echo'", - type: "nao6-ros2.say_text", - parameters: { text: "Echo" }, - category: "interaction" - } - ] - }, - pluginId: "hristudio-core", - category: "control", - sourceKind: "core" - }); - - // Wait - await db.insert(schema.actions).values({ - stepId: step2.id, - name: "Wait 5 Seconds", - type: "wait", - orderIndex: 1, - parameters: { duration: 5 }, - pluginId: "hristudio-core", - category: "control", - sourceKind: "core" - }); - - // Branch (Controls step routing, not nested actions) - // Note: Branch configuration is stored in step.trigger.conditions, not action.parameters - // The branch action itself is just a marker that this step has conditional routing - await db.insert(schema.actions).values({ - stepId: step2.id, - name: "Conditional Routing", - type: "branch", - orderIndex: 2, - parameters: { - // Branch actions don't have nested children - // Routing is configured at the step level via trigger.conditions - }, - pluginId: "hristudio-core", - category: "control", - sourceKind: "core" - }); - - // Update step2 to have conditional routing - await db.update(schema.steps) - .set({ - type: "conditional", - conditions: { - options: [ - { - label: "High Score Path", - nextStepIndex: 2, // Would go to a hypothetical step 3 - variant: "default" - }, - { - label: "Low Score Path", - nextStepIndex: 0, // Loop back to step 1 - variant: "outline" - } - ] - } - }) - .where(sql`id = ${step2.id}`); - - - } catch (err) { - console.error(err); - process.exit(1); - } finally { - await connection.end(); - } + ], + }, + }) + .where(sql`id = ${step2.id}`); + } catch (err) { + console.error(err); + process.exit(1); + } finally { + await connection.end(); + } } main(); diff --git a/scripts/archive/simulate-branch-logic.ts b/scripts/archive/simulate-branch-logic.ts index 52a9594..67f5d3a 100644 --- a/scripts/archive/simulate-branch-logic.ts +++ b/scripts/archive/simulate-branch-logic.ts @@ -1,69 +1,77 @@ - // Mock of the logic in WizardInterface.tsx handleNextStep const steps = [ - { - id: "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85", - name: "Step 3 (Conditional)", - order: 2 + { + id: "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85", + name: "Step 3 (Conditional)", + order: 2, + }, + { + id: "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5", + name: "Step 4 (Branch A)", + order: 3, + conditions: { + nextStepId: "cc3fbc7f-29e5-45e0-8d46-e80813c54292", }, - { - id: "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5", - name: "Step 4 (Branch A)", - order: 3, - conditions: { - "nextStepId": "cc3fbc7f-29e5-45e0-8d46-e80813c54292" - } + }, + { + id: "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30", + name: "Step 5 (Branch B)", + order: 4, + conditions: { + nextStepId: "cc3fbc7f-29e5-45e0-8d46-e80813c54292", }, - { - id: "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30", - name: "Step 5 (Branch B)", - order: 4, - conditions: { - "nextStepId": "cc3fbc7f-29e5-45e0-8d46-e80813c54292" - } - }, - { - id: "cc3fbc7f-29e5-45e0-8d46-e80813c54292", - name: "Step 6 (Conclusion)", - order: 5 - } + }, + { + id: "cc3fbc7f-29e5-45e0-8d46-e80813c54292", + name: "Step 6 (Conclusion)", + order: 5, + }, ]; function simulateNextStep(currentStepIndex: number) { - const currentStep = steps[currentStepIndex]; + const currentStep = steps[currentStepIndex]; - if (!currentStep) { - console.log("No step found at index:", currentStepIndex); - return; - } + if (!currentStep) { + console.log("No step found at index:", currentStepIndex); + return; + } - console.log(`\n--- Simulating Next Step from: ${currentStep.name} ---`); - console.log("Current Step Data:", JSON.stringify(currentStep, null, 2)); + console.log(`\n--- Simulating Next Step from: ${currentStep.name} ---`); + console.log("Current Step Data:", JSON.stringify(currentStep, null, 2)); - // Logic from WizardInterface.tsx - console.log("[WizardInterface] Checking for nextStepId condition:", currentStep?.conditions); + // Logic from WizardInterface.tsx + console.log( + "[WizardInterface] Checking for nextStepId condition:", + currentStep?.conditions, + ); - if (currentStep?.conditions?.nextStepId) { - const nextId = String(currentStep.conditions.nextStepId); - const targetIndex = steps.findIndex(s => s.id === nextId); + if (currentStep?.conditions?.nextStepId) { + const nextId = String(currentStep.conditions.nextStepId); + const targetIndex = steps.findIndex((s) => s.id === nextId); - console.log(`Target ID: ${nextId}`); - console.log(`Target Index Found: ${targetIndex}`); + console.log(`Target ID: ${nextId}`); + console.log(`Target Index Found: ${targetIndex}`); - if (targetIndex !== -1) { - console.log(`[WizardInterface] Condition-based jump to step ${targetIndex} (${nextId})`); - return targetIndex; - } else { - console.warn(`[WizardInterface] Targeted nextStepId ${nextId} not found in steps list.`); - } + if (targetIndex !== -1) { + console.log( + `[WizardInterface] Condition-based jump to step ${targetIndex} (${nextId})`, + ); + return targetIndex; } else { - console.log("[WizardInterface] No nextStepId found in conditions, proceeding linearly."); + console.warn( + `[WizardInterface] Targeted nextStepId ${nextId} not found in steps list.`, + ); } + } else { + console.log( + "[WizardInterface] No nextStepId found in conditions, proceeding linearly.", + ); + } - // Default: Linear progression - const nextIndex = currentStepIndex + 1; - console.log(`Proceeding linearly to index ${nextIndex}`); - return nextIndex; + // Default: Linear progression + const nextIndex = currentStepIndex + 1; + console.log(`Proceeding linearly to index ${nextIndex}`); + return nextIndex; } // Simulate Branch A (Index 1 in this array, but 3 in real experiment?) diff --git a/scripts/archive/test-converter.ts b/scripts/archive/test-converter.ts index 235e657..4068405 100644 --- a/scripts/archive/test-converter.ts +++ b/scripts/archive/test-converter.ts @@ -1,60 +1,59 @@ - import { convertDatabaseToAction } from "../../src/lib/experiment-designer/block-converter"; const mockDbAction = { - id: "eaf8f85b-75cf-4973-b436-092516b4e0e4", - name: "Introduction Sequence", - description: null, - type: "sequence", - orderIndex: 0, - parameters: { - "children": [ - { - "id": "75018b01-a964-41fb-8612-940a29020d4a", - "name": "Say Hello", - "type": "nao6-ros2.say_text", - "category": "interaction", - "parameters": { - "text": "Hello there!" - } - }, - { - "id": "d7020530-6477-41f3-84a4-5141778c93da", - "name": "Wave Hand", - "type": "nao6-ros2.move_arm", - "category": "movement", - "parameters": { - "arm": "right", - "action": "wave" - } - } - ] - }, - timeout: null, - retryCount: 0, - sourceKind: "core", - pluginId: "hristudio-core", - pluginVersion: null, - robotId: null, - baseActionId: null, - category: "control", - transport: null, - ros2: null, - rest: null, - retryable: null, - parameterSchemaRaw: null + id: "eaf8f85b-75cf-4973-b436-092516b4e0e4", + name: "Introduction Sequence", + description: null, + type: "sequence", + orderIndex: 0, + parameters: { + children: [ + { + id: "75018b01-a964-41fb-8612-940a29020d4a", + name: "Say Hello", + type: "nao6-ros2.say_text", + category: "interaction", + parameters: { + text: "Hello there!", + }, + }, + { + id: "d7020530-6477-41f3-84a4-5141778c93da", + name: "Wave Hand", + type: "nao6-ros2.move_arm", + category: "movement", + parameters: { + arm: "right", + action: "wave", + }, + }, + ], + }, + timeout: null, + retryCount: 0, + sourceKind: "core", + pluginId: "hristudio-core", + pluginVersion: null, + robotId: null, + baseActionId: null, + category: "control", + transport: null, + ros2: null, + rest: null, + retryable: null, + parameterSchemaRaw: null, }; console.log("Testing convertDatabaseToAction..."); try { - const result = convertDatabaseToAction(mockDbAction); - console.log("Result:", JSON.stringify(result, null, 2)); + const result = convertDatabaseToAction(mockDbAction); + console.log("Result:", JSON.stringify(result, null, 2)); - if (result.children && result.children.length > 0) { - console.log("✅ Children hydrated successfully."); - } else { - console.error("❌ Children NOT hydrated."); - } + if (result.children && result.children.length > 0) { + console.log("✅ Children hydrated successfully."); + } else { + console.error("❌ Children NOT hydrated."); + } } catch (e) { - console.error("❌ Error during conversion:", e); + console.error("❌ Error during conversion:", e); } diff --git a/scripts/archive/test-trpc-client.ts b/scripts/archive/test-trpc-client.ts index e415e62..81b40de 100644 --- a/scripts/archive/test-trpc-client.ts +++ b/scripts/archive/test-trpc-client.ts @@ -1,4 +1,3 @@ - import { appRouter } from "../../src/server/api/root"; import { createCallerFactory } from "../../src/server/api/trpc"; import { drizzle } from "drizzle-orm/postgres-js"; @@ -13,59 +12,63 @@ const db = drizzle(connection, { schema }); // 2. Mock Session const mockSession = { - user: { - id: "0e830889-ab46-4b48-a8ba-1d4bd3e665ed", // Admin user ID from seed - name: "Sean O'Connor", - email: "sean@soconnor.dev" - }, - expires: new Date().toISOString() + user: { + id: "0e830889-ab46-4b48-a8ba-1d4bd3e665ed", // Admin user ID from seed + name: "Sean O'Connor", + email: "sean@soconnor.dev", + }, + expires: new Date().toISOString(), }; // 3. Create Caller const createCaller = createCallerFactory(appRouter); const caller = createCaller({ - db, - session: mockSession as any, - headers: new Headers() + db, + session: mockSession as any, + headers: new Headers(), }); async function main() { - console.log("🔍 Fetching experiment via TRPC caller..."); + console.log("🔍 Fetching experiment via TRPC caller..."); - // Get ID first - const exp = await db.query.experiments.findFirst({ - where: eq(schema.experiments.name, "Control Flow Demo"), - columns: { id: true } + // Get ID first + const exp = await db.query.experiments.findFirst({ + where: eq(schema.experiments.name, "Control Flow Demo"), + columns: { id: true }, + }); + + if (!exp) { + console.error("❌ Experiment not found"); + return; + } + + const result = await caller.experiments.get({ id: exp.id }); + + console.log(`✅ Fetched experiment: ${result.name} (${result.id})`); + + if (result.steps && result.steps.length > 0) { + console.log(`Checking ${result.steps.length} steps...`); + const actions = result.steps[0]!.actions; // Step 1 actions + console.log(`Step 1 has ${actions.length} actions.`); + + actions.forEach((a) => { + if (["sequence", "parallel", "loop", "branch"].includes(a.type)) { + console.log(`\nAction: ${a.name} (${a.type})`); + console.log( + `Children Count: ${a.children ? a.children.length : "UNDEFINED"}`, + ); + if (a.children && a.children.length > 0) { + console.log( + `First Child: ${a.children[0]!.name} (${a.children[0]!.type})`, + ); + } + } }); + } else { + console.error("❌ No steps found in result."); + } - if (!exp) { - console.error("❌ Experiment not found"); - return; - } - - const result = await caller.experiments.get({ id: exp.id }); - - console.log(`✅ Fetched experiment: ${result.name} (${result.id})`); - - if (result.steps && result.steps.length > 0) { - console.log(`Checking ${result.steps.length} steps...`); - const actions = result.steps[0]!.actions; // Step 1 actions - console.log(`Step 1 has ${actions.length} actions.`); - - actions.forEach(a => { - if (["sequence", "parallel", "loop", "branch"].includes(a.type)) { - console.log(`\nAction: ${a.name} (${a.type})`); - console.log(`Children Count: ${a.children ? a.children.length : 'UNDEFINED'}`); - if (a.children && a.children.length > 0) { - console.log(`First Child: ${a.children[0]!.name} (${a.children[0]!.type})`); - } - } - }); - } else { - console.error("❌ No steps found in result."); - } - - await connection.end(); + await connection.end(); } main(); diff --git a/scripts/archive/verify-conversion.ts b/scripts/archive/verify-conversion.ts index 982c95e..a42e095 100644 --- a/scripts/archive/verify-conversion.ts +++ b/scripts/archive/verify-conversion.ts @@ -1,44 +1,46 @@ - import { db } from "../../src/server/db"; import { experiments } from "../../src/server/db/schema"; import { eq, asc } from "drizzle-orm"; import { convertDatabaseToSteps } from "../../src/lib/experiment-designer/block-converter"; async function verifyConversion() { - const experiment = await db.query.experiments.findFirst({ + const experiment = await db.query.experiments.findFirst({ + with: { + steps: { + orderBy: (steps, { asc }) => [asc(steps.orderIndex)], with: { - steps: { - orderBy: (steps, { asc }) => [asc(steps.orderIndex)], - with: { - actions: { - orderBy: (actions, { asc }) => [asc(actions.orderIndex)], - } - } - } - } - }); + actions: { + orderBy: (actions, { asc }) => [asc(actions.orderIndex)], + }, + }, + }, + }, + }); - if (!experiment) { - console.log("No experiment found"); - return; + if (!experiment) { + console.log("No experiment found"); + return; + } + + console.log("Raw DB Steps Count:", experiment.steps.length); + const converted = convertDatabaseToSteps(experiment.steps); + + console.log("Converted Steps:"); + converted.forEach((s, idx) => { + console.log(`[${idx}] ${s.name} (${s.type})`); + console.log(` Trigger:`, JSON.stringify(s.trigger)); + if (s.type === "conditional") { + console.log( + ` Conditions populated?`, + Object.keys(s.trigger.conditions).length > 0, + ); } - - console.log("Raw DB Steps Count:", experiment.steps.length); - const converted = convertDatabaseToSteps(experiment.steps); - - console.log("Converted Steps:"); - converted.forEach((s, idx) => { - console.log(`[${idx}] ${s.name} (${s.type})`); - console.log(` Trigger:`, JSON.stringify(s.trigger)); - if (s.type === 'conditional') { - console.log(` Conditions populated?`, Object.keys(s.trigger.conditions).length > 0); - } - }); + }); } verifyConversion() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/archive/verify-study-readiness.ts b/scripts/archive/verify-study-readiness.ts index 26b7c25..411bc73 100644 --- a/scripts/archive/verify-study-readiness.ts +++ b/scripts/archive/verify-study-readiness.ts @@ -8,83 +8,100 @@ const client = postgres(connectionString); const db = drizzle(client, { schema }); async function verify() { - console.log("🔍 Verifying Study Readiness..."); + console.log("🔍 Verifying Study Readiness..."); - // 1. Check Study - const study = await db.query.studies.findFirst({ - where: eq(schema.studies.name, "Comparative WoZ Study") - }); + // 1. Check Study + const study = await db.query.studies.findFirst({ + where: eq(schema.studies.name, "Comparative WoZ Study"), + }); - if (!study) { - console.error("❌ Study 'Comparative WoZ Study' not found."); - process.exit(1); + if (!study) { + console.error("❌ Study 'Comparative WoZ Study' not found."); + process.exit(1); + } + console.log("✅ Study found:", study.name); + + // 2. Check Experiment + const experiment = await db.query.experiments.findFirst({ + where: eq(schema.experiments.name, "The Interactive Storyteller"), + }); + + if (!experiment) { + console.error("❌ Experiment 'The Interactive Storyteller' not found."); + process.exit(1); + } + console.log("✅ Experiment found:", experiment.name); + + // 3. Check Steps + const steps = await db.query.steps.findMany({ + where: eq(schema.steps.experimentId, experiment.id), + orderBy: schema.steps.orderIndex, + }); + + console.log(`ℹ️ Found ${steps.length} steps.`); + if (steps.length < 5) { + console.error("❌ Expected at least 5 steps, found " + steps.length); + process.exit(1); + } + + // Verify Step Names + const expectedSteps = [ + "The Hook", + "The Narrative - Part 1", + "Comprehension Check", + "Positive Feedback", + "Conclusion", + ]; + for (let i = 0; i < expectedSteps.length; i++) { + const step = steps[i]; + if (!step) continue; + + if (step.name !== expectedSteps[i]) { + console.error( + `❌ Step mismatch at index ${i}. Expected '${expectedSteps[i]}', got '${step.name}'`, + ); + } else { + console.log(`✅ Step ${i + 1}: ${step.name}`); } - console.log("✅ Study found:", study.name); + } - // 2. Check Experiment - const experiment = await db.query.experiments.findFirst({ - where: eq(schema.experiments.name, "The Interactive Storyteller") - }); + // 4. Check Plugin Actions + // Find the NAO6 plugin + const plugin = await db.query.plugins.findFirst({ + where: (plugins, { eq, and }) => + and( + eq(plugins.name, "NAO6 Robot (Enhanced ROS2 Integration)"), + eq(plugins.status, "active"), + ), + }); - if (!experiment) { - console.error("❌ Experiment 'The Interactive Storyteller' not found."); - process.exit(1); + if (!plugin) { + console.error("❌ NAO6 Plugin not found."); + process.exit(1); + } + + const actions = plugin.actionDefinitions as any[]; + const requiredActions = [ + "nao_nod", + "nao_shake_head", + "nao_bow", + "nao_open_hand", + ]; + + for (const actionId of requiredActions) { + const found = actions.find((a) => a.id === actionId); + if (!found) { + console.error(`❌ Plugin missing action: ${actionId}`); + process.exit(1); } - console.log("✅ Experiment found:", experiment.name); + console.log(`✅ Plugin has action: ${actionId}`); + } - // 3. Check Steps - const steps = await db.query.steps.findMany({ - where: eq(schema.steps.experimentId, experiment.id), - orderBy: schema.steps.orderIndex - }); - - console.log(`ℹ️ Found ${steps.length} steps.`); - if (steps.length < 5) { - console.error("❌ Expected at least 5 steps, found " + steps.length); - process.exit(1); - } - - // Verify Step Names - const expectedSteps = ["The Hook", "The Narrative - Part 1", "Comprehension Check", "Positive Feedback", "Conclusion"]; - for (let i = 0; i < expectedSteps.length; i++) { - const step = steps[i]; - if (!step) continue; - - if (step.name !== expectedSteps[i]) { - console.error(`❌ Step mismatch at index ${i}. Expected '${expectedSteps[i]}', got '${step.name}'`); - } else { - console.log(`✅ Step ${i + 1}: ${step.name}`); - } - } - - // 4. Check Plugin Actions - // Find the NAO6 plugin - const plugin = await db.query.plugins.findFirst({ - where: (plugins, { eq, and }) => and(eq(plugins.name, "NAO6 Robot (Enhanced ROS2 Integration)"), eq(plugins.status, "active")) - }); - - if (!plugin) { - console.error("❌ NAO6 Plugin not found."); - process.exit(1); - } - - const actions = plugin.actionDefinitions as any[]; - const requiredActions = ["nao_nod", "nao_shake_head", "nao_bow", "nao_open_hand"]; - - for (const actionId of requiredActions) { - const found = actions.find(a => a.id === actionId); - if (!found) { - console.error(`❌ Plugin missing action: ${actionId}`); - process.exit(1); - } - console.log(`✅ Plugin has action: ${actionId}`); - } - - console.log("🎉 Verification Complete: Platform is ready for the study!"); - process.exit(0); + console.log("🎉 Verification Complete: Platform is ready for the study!"); + process.exit(0); } verify().catch((e) => { - console.error(e); - process.exit(1); + console.error(e); + process.exit(1); }); diff --git a/scripts/archive/verify-trpc-logic.ts b/scripts/archive/verify-trpc-logic.ts index 3dc8193..92686c0 100644 --- a/scripts/archive/verify-trpc-logic.ts +++ b/scripts/archive/verify-trpc-logic.ts @@ -1,84 +1,86 @@ - import { db } from "~/server/db"; import { experiments, steps, actions } from "~/server/db/schema"; import { eq, asc, desc } from "drizzle-orm"; import { convertDatabaseToSteps } from "~/lib/experiment-designer/block-converter"; async function verifyTrpcLogic() { - console.log("Verifying TRPC Logic for Interactive Storyteller..."); + console.log("Verifying TRPC Logic for Interactive Storyteller..."); - // 1. Simulate the DB Query from experiments.ts - const experiment = await db.query.experiments.findFirst({ - where: eq(experiments.name, "The Interactive Storyteller"), - with: { - study: { - columns: { - id: true, - name: true, - }, - }, - createdBy: { - columns: { - id: true, - name: true, - email: true, - }, - }, - robot: true, - steps: { - with: { - actions: { - orderBy: [asc(actions.orderIndex)], - }, - }, - orderBy: [asc(steps.orderIndex)], - }, + // 1. Simulate the DB Query from experiments.ts + const experiment = await db.query.experiments.findFirst({ + where: eq(experiments.name, "The Interactive Storyteller"), + with: { + study: { + columns: { + id: true, + name: true, }, - }); + }, + createdBy: { + columns: { + id: true, + name: true, + email: true, + }, + }, + robot: true, + steps: { + with: { + actions: { + orderBy: [asc(actions.orderIndex)], + }, + }, + orderBy: [asc(steps.orderIndex)], + }, + }, + }); - if (!experiment) { - console.error("Experiment not found!"); - return; - } + if (!experiment) { + console.error("Experiment not found!"); + return; + } - // 2. Simulate the Transformation - console.log("Transforming DB steps to Designer steps..."); - const transformedSteps = convertDatabaseToSteps(experiment.steps); + // 2. Simulate the Transformation + console.log("Transforming DB steps to Designer steps..."); + const transformedSteps = convertDatabaseToSteps(experiment.steps); - // 3. Inspect Step 4 (Branch A) - // Step index 3 (0-based) is Branch A - const branchAStep = transformedSteps[3]; + // 3. Inspect Step 4 (Branch A) + // Step index 3 (0-based) is Branch A + const branchAStep = transformedSteps[3]; - if (branchAStep) { - console.log("Step 4 (Branch A):", branchAStep.name); - console.log(" Type:", branchAStep.type); - console.log(" Trigger:", JSON.stringify(branchAStep.trigger, null, 2)); - } else { - console.error("Step 4 (Branch A) not found in transformed steps!"); - process.exit(1); - } + if (branchAStep) { + console.log("Step 4 (Branch A):", branchAStep.name); + console.log(" Type:", branchAStep.type); + console.log(" Trigger:", JSON.stringify(branchAStep.trigger, null, 2)); + } else { + console.error("Step 4 (Branch A) not found in transformed steps!"); + process.exit(1); + } - // Check conditions specifically - const conditions = branchAStep.trigger?.conditions as any; - if (conditions?.nextStepId) { - console.log("SUCCESS: nextStepId found in conditions:", conditions.nextStepId); - } else { - console.error("FAILURE: nextStepId MISSING in conditions!"); - } + // Check conditions specifically + const conditions = branchAStep.trigger?.conditions as any; + if (conditions?.nextStepId) { + console.log( + "SUCCESS: nextStepId found in conditions:", + conditions.nextStepId, + ); + } else { + console.error("FAILURE: nextStepId MISSING in conditions!"); + } - // Inspect Step 5 (Branch B) for completeness - const branchBStep = transformedSteps[4]; - if (branchBStep) { - console.log("Step 5 (Branch B):", branchBStep.name); - console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2)); - } else { - console.warn("Step 5 (Branch B) not found in transformed steps."); - } + // Inspect Step 5 (Branch B) for completeness + const branchBStep = transformedSteps[4]; + if (branchBStep) { + console.log("Step 5 (Branch B):", branchBStep.name); + console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2)); + } else { + console.warn("Step 5 (Branch B) not found in transformed steps."); + } } verifyTrpcLogic() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/get-demo-id.ts b/scripts/get-demo-id.ts index edc544b..f059756 100644 --- a/scripts/get-demo-id.ts +++ b/scripts/get-demo-id.ts @@ -1,4 +1,3 @@ - import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "../src/server/db/schema"; @@ -9,18 +8,18 @@ const connection = postgres(connectionString); const db = drizzle(connection, { schema }); async function main() { - const exp = await db.query.experiments.findFirst({ - where: eq(schema.experiments.name, "Control Flow Demo"), - columns: { id: true } - }); + const exp = await db.query.experiments.findFirst({ + where: eq(schema.experiments.name, "Control Flow Demo"), + columns: { id: true }, + }); - if (exp) { - console.log(`Experiment ID: ${exp.id}`); - } else { - console.error("Experiment not found"); - } + if (exp) { + console.log(`Experiment ID: ${exp.id}`); + } else { + console.error("Experiment not found"); + } - await connection.end(); + await connection.end(); } main(); diff --git a/scripts/get-user-id.ts b/scripts/get-user-id.ts index 24fb653..426507e 100644 --- a/scripts/get-user-id.ts +++ b/scripts/get-user-id.ts @@ -1,4 +1,3 @@ - import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "../src/server/db/schema"; @@ -9,18 +8,18 @@ const connection = postgres(connectionString); const db = drizzle(connection, { schema }); async function main() { - const user = await db.query.users.findFirst({ - where: eq(schema.users.email, "sean@soconnor.dev"), - columns: { id: true } - }); + const user = await db.query.users.findFirst({ + where: eq(schema.users.email, "sean@soconnor.dev"), + columns: { id: true }, + }); - if (user) { - console.log(`User ID: ${user.id}`); - } else { - console.error("User not found"); - } + if (user) { + console.log(`User ID: ${user.id}`); + } else { + console.error("User not found"); + } - await connection.end(); + await connection.end(); } main(); diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts index 9d7282f..a662acf 100755 --- a/scripts/seed-dev.ts +++ b/scripts/seed-dev.ts @@ -17,17 +17,26 @@ import * as path from "path"; // Function to load plugin definition (Remote -> Local Fallback) async function loadNaoPluginDef() { const REMOTE_URL = "https://repo.hristudio.com/plugins/nao6-ros2.json"; - const LOCAL_PATH = path.join(__dirname, "../robot-plugins/plugins/nao6-ros2.json"); + const LOCAL_PATH = path.join( + __dirname, + "../robot-plugins/plugins/nao6-ros2.json", + ); try { - console.log(`🌐 Attempting to fetch plugin definition from ${REMOTE_URL}...`); - const response = await fetch(REMOTE_URL, { signal: AbortSignal.timeout(3000) }); // 3s timeout + console.log( + `🌐 Attempting to fetch plugin definition from ${REMOTE_URL}...`, + ); + const response = await fetch(REMOTE_URL, { + signal: AbortSignal.timeout(3000), + }); // 3s timeout if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); console.log("✅ Successfully fetched plugin definition from remote."); return data; } catch (err) { - console.warn(`⚠️ Remote fetch failed (${err instanceof Error ? err.message : String(err)}). Falling back to local file.`); + console.warn( + `⚠️ Remote fetch failed (${err instanceof Error ? err.message : String(err)}). Falling back to local file.`, + ); const rawPlugin = fs.readFileSync(LOCAL_PATH, "utf-8"); return JSON.parse(rawPlugin); } @@ -39,7 +48,10 @@ let CORE_PLUGIN_DEF: any; let WOZ_PLUGIN_DEF: any; function loadSystemPlugin(filename: string) { - const LOCAL_PATH = path.join(__dirname, `../src/plugins/definitions/${filename}`); + const LOCAL_PATH = path.join( + __dirname, + `../src/plugins/definitions/${filename}`, + ); try { const raw = fs.readFileSync(LOCAL_PATH, "utf-8"); return JSON.parse(raw); @@ -52,8 +64,6 @@ function loadSystemPlugin(filename: string) { async function main() { console.log("🌱 Starting realistic seed script..."); - - try { NAO_PLUGIN_DEF = await loadNaoPluginDef(); CORE_PLUGIN_DEF = loadSystemPlugin("hristudio-core.json"); @@ -87,149 +97,184 @@ async function main() { console.log("👥 Creating users..."); const hashedPassword = await bcrypt.hash("password123", 12); - const gravatarUrl = (email: string) => `https://www.gravatar.com/avatar/${createHash("md5").update(email.toLowerCase().trim()).digest("hex")}?d=identicon`; + const gravatarUrl = (email: string) => + `https://www.gravatar.com/avatar/${createHash("md5").update(email.toLowerCase().trim()).digest("hex")}?d=identicon`; - const [adminUser] = await db.insert(schema.users).values({ - name: "Sean O'Connor", - email: "sean@soconnor.dev", - password: hashedPassword, - emailVerified: new Date(), - image: gravatarUrl("sean@soconnor.dev"), - }).returning(); + const [adminUser] = await db + .insert(schema.users) + .values({ + name: "Sean O'Connor", + email: "sean@soconnor.dev", + password: hashedPassword, + emailVerified: new Date(), + image: gravatarUrl("sean@soconnor.dev"), + }) + .returning(); - const [researcherUser] = await db.insert(schema.users).values({ - name: "Dr. Felipe Perrone", - email: "felipe.perrone@bucknell.edu", - password: hashedPassword, - emailVerified: new Date(), - image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe", - }).returning(); + const [researcherUser] = await db + .insert(schema.users) + .values({ + name: "Dr. Felipe Perrone", + email: "felipe.perrone@bucknell.edu", + password: hashedPassword, + emailVerified: new Date(), + image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe", + }) + .returning(); if (!adminUser) throw new Error("Failed to create admin user"); - await db.insert(schema.userSystemRoles).values({ userId: adminUser.id, role: "administrator" }); + await db + .insert(schema.userSystemRoles) + .values({ userId: adminUser.id, role: "administrator" }); // 3. Create Robots & Plugins console.log("🤖 Creating robots and plugins..."); - const [naoRobot] = await db.insert(schema.robots).values({ - name: "NAO6", - manufacturer: "SoftBank Robotics", - model: "NAO V6", - description: "Humanoid robot for social interaction studies.", - capabilities: ["speech", "vision", "bipedal_walking", "gestures"], - communicationProtocol: "ros2", - }).returning(); + const [naoRobot] = await db + .insert(schema.robots) + .values({ + name: "NAO6", + manufacturer: "SoftBank Robotics", + model: "NAO V6", + description: "Humanoid robot for social interaction studies.", + capabilities: ["speech", "vision", "bipedal_walking", "gestures"], + communicationProtocol: "ros2", + }) + .returning(); - const [naoRepo] = await db.insert(schema.pluginRepositories).values({ - name: "HRIStudio Official Plugins", - url: "https://github.com/hristudio/plugins", - description: "Official verified plugins", - trustLevel: "official", - isEnabled: true, - isOfficial: true, - createdBy: adminUser.id, - }).returning(); + const [naoRepo] = await db + .insert(schema.pluginRepositories) + .values({ + name: "HRIStudio Official Plugins", + url: "https://github.com/hristudio/plugins", + description: "Official verified plugins", + trustLevel: "official", + isEnabled: true, + isOfficial: true, + createdBy: adminUser.id, + }) + .returning(); - const [naoPlugin] = await db.insert(schema.plugins).values({ - robotId: naoRobot!.id, - name: NAO_PLUGIN_DEF.name, - version: NAO_PLUGIN_DEF.version, - description: NAO_PLUGIN_DEF.description, - author: "HRIStudio Team", - repositoryUrl: "https://github.com/hristudio/plugins/tree/main/nao6", - trustLevel: "verified", - actionDefinitions: NAO_PLUGIN_DEF.actionDefinitions, - metadata: NAO_PLUGIN_DEF, - status: "active", - createdAt: new Date(), - }).returning(); + const [naoPlugin] = await db + .insert(schema.plugins) + .values({ + robotId: naoRobot!.id, + name: NAO_PLUGIN_DEF.name, + version: NAO_PLUGIN_DEF.version, + description: NAO_PLUGIN_DEF.description, + author: "HRIStudio Team", + repositoryUrl: "https://github.com/hristudio/plugins/tree/main/nao6", + trustLevel: "verified", + actionDefinitions: NAO_PLUGIN_DEF.actionDefinitions, + metadata: NAO_PLUGIN_DEF, + status: "active", + createdAt: new Date(), + }) + .returning(); // 4. Create Study & Experiment - Comparative WoZ Study console.log("📚 Creating 'Comparative WoZ Study'..."); - const [study] = await db.insert(schema.studies).values({ - name: "Comparative WoZ Study", - description: "Comparison of HRIStudio vs Choregraphe for The Interactive Storyteller scenario.", - institution: "Bucknell University", - irbProtocol: "2024-HRI-COMP", - status: "active", - createdBy: adminUser.id, - }).returning(); + const [study] = await db + .insert(schema.studies) + .values({ + name: "Comparative WoZ Study", + description: + "Comparison of HRIStudio vs Choregraphe for The Interactive Storyteller scenario.", + institution: "Bucknell University", + irbProtocol: "2024-HRI-COMP", + status: "active", + createdBy: adminUser.id, + }) + .returning(); await db.insert(schema.studyMembers).values([ { studyId: study!.id, userId: adminUser.id, role: "owner" }, - { studyId: study!.id, userId: researcherUser!.id, role: "researcher" } + { studyId: study!.id, userId: researcherUser!.id, role: "researcher" }, ]); // Insert System Plugins - const [corePlugin] = await db.insert(schema.plugins).values({ - name: CORE_PLUGIN_DEF.name, - version: CORE_PLUGIN_DEF.version, - description: CORE_PLUGIN_DEF.description, - author: CORE_PLUGIN_DEF.author, - trustLevel: "official", - actionDefinitions: CORE_PLUGIN_DEF.actionDefinitions, - robotId: null, // System Plugin - metadata: { ...CORE_PLUGIN_DEF, id: CORE_PLUGIN_DEF.id }, - status: "active" - }).returning(); + const [corePlugin] = await db + .insert(schema.plugins) + .values({ + name: CORE_PLUGIN_DEF.name, + version: CORE_PLUGIN_DEF.version, + description: CORE_PLUGIN_DEF.description, + author: CORE_PLUGIN_DEF.author, + trustLevel: "official", + actionDefinitions: CORE_PLUGIN_DEF.actionDefinitions, + robotId: null, // System Plugin + metadata: { ...CORE_PLUGIN_DEF, id: CORE_PLUGIN_DEF.id }, + status: "active", + }) + .returning(); - const [wozPlugin] = await db.insert(schema.plugins).values({ - name: WOZ_PLUGIN_DEF.name, - version: WOZ_PLUGIN_DEF.version, - description: WOZ_PLUGIN_DEF.description, - author: WOZ_PLUGIN_DEF.author, - trustLevel: "official", - actionDefinitions: WOZ_PLUGIN_DEF.actionDefinitions, - robotId: null, // System Plugin - metadata: { ...WOZ_PLUGIN_DEF, id: WOZ_PLUGIN_DEF.id }, - status: "active" - }).returning(); + const [wozPlugin] = await db + .insert(schema.plugins) + .values({ + name: WOZ_PLUGIN_DEF.name, + version: WOZ_PLUGIN_DEF.version, + description: WOZ_PLUGIN_DEF.description, + author: WOZ_PLUGIN_DEF.author, + trustLevel: "official", + actionDefinitions: WOZ_PLUGIN_DEF.actionDefinitions, + robotId: null, // System Plugin + metadata: { ...WOZ_PLUGIN_DEF, id: WOZ_PLUGIN_DEF.id }, + status: "active", + }) + .returning(); await db.insert(schema.studyPlugins).values([ { studyId: study!.id, pluginId: naoPlugin!.id, configuration: { robotIp: "10.0.0.42" }, - installedBy: adminUser.id + installedBy: adminUser.id, }, { studyId: study!.id, pluginId: corePlugin!.id, configuration: {}, - installedBy: adminUser.id + installedBy: adminUser.id, }, { studyId: study!.id, pluginId: wozPlugin!.id, configuration: {}, - installedBy: adminUser.id - } + installedBy: adminUser.id, + }, ]); - const [experiment] = await db.insert(schema.experiments).values({ - studyId: study!.id, - name: "The Interactive Storyteller", - description: "A storytelling scenario where the robot tells a story and asks questions to the participant.", - version: 1, - status: "ready", - robotId: naoRobot!.id, - createdBy: adminUser.id, - // visualDesign will be auto-generated by designer from DB steps - }).returning(); + const [experiment] = await db + .insert(schema.experiments) + .values({ + studyId: study!.id, + name: "The Interactive Storyteller", + description: + "A storytelling scenario where the robot tells a story and asks questions to the participant.", + version: 1, + status: "ready", + robotId: naoRobot!.id, + createdBy: adminUser.id, + // visualDesign will be auto-generated by designer from DB steps + }) + .returning(); // 5. Create Steps & Actions (The Interactive Storyteller Protocol) console.log("🎬 Creating experiment steps (Interactive Storyteller)..."); // --- Step 1: The Hook --- - const [step1] = await db.insert(schema.steps).values({ - experimentId: experiment!.id, - name: "The Hook", - description: "Initial greeting and story introduction", - type: "robot", - orderIndex: 0, - required: true, - durationEstimate: 25 - }).returning(); + const [step1] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "The Hook", + description: "Initial greeting and story introduction", + type: "robot", + orderIndex: 0, + required: true, + durationEstimate: 25, + }) + .returning(); await db.insert(schema.actions).values([ { @@ -237,11 +282,13 @@ async function main() { name: "Say Text", type: "nao6-ros2.say_text", orderIndex: 0, - parameters: { text: "Hello. I have a story to tell you about a space traveler. Are you ready?" }, + parameters: { + text: "Hello. I have a story to tell you about a space traveler. Are you ready?", + }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "interaction", - retryable: true + retryable: true, }, { stepId: step1!.id, @@ -255,25 +302,28 @@ async function main() { shoulder_roll: -0.2, elbow_yaw: 0.5, elbow_roll: -0.4, - speed: 0.4 + speed: 0.4, }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true - } + retryable: true, + }, ]); // --- Step 2: The Narrative --- - const [step2] = await db.insert(schema.steps).values({ - experimentId: experiment!.id, - name: "The Narrative", - description: "Robot tells the space traveler story with gaze behavior", - type: "robot", - orderIndex: 1, - required: true, - durationEstimate: 45 - }).returning(); + const [step2] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "The Narrative", + description: "Robot tells the space traveler story with gaze behavior", + type: "robot", + orderIndex: 1, + required: true, + durationEstimate: 45, + }) + .returning(); await db.insert(schema.actions).values([ { @@ -281,11 +331,13 @@ async function main() { name: "Tell Story", type: "nao6-ros2.say_text", orderIndex: 0, - parameters: { text: "The traveler flew to Mars. He found a red rock that glowed in the dark. He put it in his pocket." }, + parameters: { + text: "The traveler flew to Mars. He found a red rock that glowed in the dark. He put it in his pocket.", + }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "interaction", - retryable: true + retryable: true, }, { stepId: step2!.id, @@ -296,7 +348,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true + retryable: true, }, { stepId: step2!.id, @@ -307,8 +359,8 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true - } + retryable: true, + }, ]); // --- Step 3: Comprehension Check (Wizard Decision Point) --- @@ -316,45 +368,65 @@ async function main() { // --- Step 3: Comprehension Check (Wizard Decision Point) --- // Note: Wizard will choose to proceed to Step 4a (Correct) or 4b (Incorrect) // --- Step 4a: Correct Response Branch --- - const [step4a] = await db.insert(schema.steps).values({ - experimentId: experiment!.id, - name: "Branch A: Correct Response", - description: "Response when participant says 'Red'", - type: "robot", - orderIndex: 3, - required: false, - durationEstimate: 20 - }).returning(); + const [step4a] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Branch A: Correct Response", + description: "Response when participant says 'Red'", + type: "robot", + orderIndex: 3, + required: false, + durationEstimate: 20, + }) + .returning(); // --- Step 4b: Incorrect Response Branch --- - const [step4b] = await db.insert(schema.steps).values({ - experimentId: experiment!.id, - name: "Branch B: Incorrect Response", - description: "Response when participant gives wrong answer", - type: "robot", - orderIndex: 4, - required: false, - durationEstimate: 20 - }).returning(); + const [step4b] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Branch B: Incorrect Response", + description: "Response when participant gives wrong answer", + type: "robot", + orderIndex: 4, + required: false, + durationEstimate: 20, + }) + .returning(); // --- Step 3: Comprehension Check (Wizard Decision Point) --- // Note: Wizard will choose to proceed to Step 4a (Correct) or 4b (Incorrect) - const [step3] = await db.insert(schema.steps).values({ - experimentId: experiment!.id, - name: "Comprehension Check", - description: "Ask participant about rock color and wait for wizard input", - type: "conditional", - orderIndex: 2, - required: true, - durationEstimate: 30, - conditions: { - variable: "last_wizard_response", - options: [ - { label: "Correct Response (Red)", value: "Correct", nextStepId: step4a!.id, variant: "default" }, - { label: "Incorrect Response", value: "Incorrect", nextStepId: step4b!.id, variant: "destructive" } - ] - } - }).returning(); + const [step3] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Comprehension Check", + description: + "Ask participant about rock color and wait for wizard input", + type: "conditional", + orderIndex: 2, + required: true, + durationEstimate: 30, + conditions: { + variable: "last_wizard_response", + options: [ + { + label: "Correct Response (Red)", + value: "Correct", + nextStepId: step4a!.id, + variant: "default", + }, + { + label: "Incorrect Response", + value: "Incorrect", + nextStepId: step4b!.id, + variant: "destructive", + }, + ], + }, + }) + .returning(); await db.insert(schema.actions).values([ { @@ -366,7 +438,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "interaction", - retryable: true + retryable: true, }, { stepId: step3!.id, @@ -376,11 +448,11 @@ async function main() { // Define the options that will be presented to the Wizard parameters: { prompt_text: "Did participant answer 'Red' correctly?", - options: ["Correct", "Incorrect"] + options: ["Correct", "Incorrect"], }, sourceKind: "core", pluginId: "hristudio-woz", // Explicit link - category: "wizard" + category: "wizard", }, { stepId: step3!.id, @@ -390,8 +462,8 @@ async function main() { parameters: {}, sourceKind: "core", pluginId: "hristudio-core", // Explicit link - category: "control" - } + category: "control", + }, ]); await db.insert(schema.actions).values([ @@ -400,11 +472,15 @@ async function main() { name: "Say Text with Emotion", type: "nao6-ros2.say_with_emotion", orderIndex: 0, - parameters: { text: "Yes! It was a glowing red rock.", emotion: "happy", speed: 1.0 }, + parameters: { + text: "Yes! It was a glowing red rock.", + emotion: "happy", + speed: 1.0, + }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "interaction", - retryable: true + retryable: true, }, { stepId: step4a!.id, @@ -415,7 +491,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true + retryable: true, }, { stepId: step4a!.id, @@ -426,12 +502,10 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true - } + retryable: true, + }, ]); - - await db.insert(schema.actions).values([ { stepId: step4b!.id, @@ -442,7 +516,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "interaction", - retryable: true + retryable: true, }, { stepId: step4b!.id, @@ -453,7 +527,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true + retryable: true, }, { stepId: step4b!.id, @@ -464,7 +538,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true + retryable: true, }, { stepId: step4b!.id, @@ -475,20 +549,23 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true - } + retryable: true, + }, ]); // --- Step 5: Conclusion --- - const [step5] = await db.insert(schema.steps).values({ - experimentId: experiment!.id, - name: "Conclusion", - description: "End the story and thank participant", - type: "robot", - orderIndex: 5, - required: true, - durationEstimate: 25 - }).returning(); + const [step5] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Conclusion", + description: "End the story and thank participant", + type: "robot", + orderIndex: 5, + required: true, + durationEstimate: 25, + }) + .returning(); await db.insert(schema.actions).values([ { @@ -500,7 +577,7 @@ async function main() { pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "interaction", - retryable: true + retryable: true, }, { stepId: step5!.id, @@ -513,89 +590,118 @@ async function main() { shoulder_roll: 0.1, elbow_yaw: 0.0, elbow_roll: -0.3, - speed: 0.3 + speed: 0.3, }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", category: "movement", - retryable: true - } + retryable: true, + }, ]); // 5b. Create "Control Flow Demo" Experiment console.log("🧩 Creating 'Control Flow Demo' experiment..."); - const [controlDemoExp] = await db.insert(schema.experiments).values({ - studyId: study!.id, - name: "Control Flow Demo", - description: "Demonstration of enhanced control flow actions: Parallel, Wait, Loop, Branch.", - version: 2, - status: "draft", - robotId: naoRobot!.id, - createdBy: adminUser.id, - }).returning(); + const [controlDemoExp] = await db + .insert(schema.experiments) + .values({ + studyId: study!.id, + name: "Control Flow Demo", + description: + "Demonstration of enhanced control flow actions: Parallel, Wait, Loop, Branch.", + version: 2, + status: "draft", + robotId: naoRobot!.id, + createdBy: adminUser.id, + }) + .returning(); // Step 1: Introduction (Parallel) - const [cdStep1] = await db.insert(schema.steps).values({ - experimentId: controlDemoExp!.id, - name: "1. Introduction (Parallel)", - description: "Parallel execution demonstration", - type: "robot", - orderIndex: 0, - required: true, - durationEstimate: 30 - }).returning(); + const [cdStep1] = await db + .insert(schema.steps) + .values({ + experimentId: controlDemoExp!.id, + name: "1. Introduction (Parallel)", + description: "Parallel execution demonstration", + type: "robot", + orderIndex: 0, + required: true, + durationEstimate: 30, + }) + .returning(); // Step 5: Conclusion - Defined early for ID reference (Convergence point) - const [cdStep5] = await db.insert(schema.steps).values({ - experimentId: controlDemoExp!.id, - name: "5. Conclusion", - description: "Convergence point", - type: "robot", - orderIndex: 4, - required: true, - durationEstimate: 15 - }).returning(); + const [cdStep5] = await db + .insert(schema.steps) + .values({ + experimentId: controlDemoExp!.id, + name: "5. Conclusion", + description: "Convergence point", + type: "robot", + orderIndex: 4, + required: true, + durationEstimate: 15, + }) + .returning(); // Step 4: Path B (Wait) - Defined early for ID reference - const [cdStep4] = await db.insert(schema.steps).values({ - experimentId: controlDemoExp!.id, - name: "4. Path B (Wait)", - description: "Wait action demonstration", - type: "robot", - orderIndex: 3, - required: true, - durationEstimate: 10 - }).returning(); + const [cdStep4] = await db + .insert(schema.steps) + .values({ + experimentId: controlDemoExp!.id, + name: "4. Path B (Wait)", + description: "Wait action demonstration", + type: "robot", + orderIndex: 3, + required: true, + durationEstimate: 10, + }) + .returning(); // Step 3: Path A (Loop) - Defined early for ID reference - const [cdStep3] = await db.insert(schema.steps).values({ - experimentId: controlDemoExp!.id, - name: "3. Path A (Loop)", - description: "Looping demonstration", - type: "robot", - orderIndex: 2, - required: true, - durationEstimate: 45, - conditions: { nextStepId: cdStep5!.id } - }).returning(); + const [cdStep3] = await db + .insert(schema.steps) + .values({ + experimentId: controlDemoExp!.id, + name: "3. Path A (Loop)", + description: "Looping demonstration", + type: "robot", + orderIndex: 2, + required: true, + durationEstimate: 45, + conditions: { nextStepId: cdStep5!.id }, + }) + .returning(); // Step 2: Branch Decision - const [cdStep2] = await db.insert(schema.steps).values({ - experimentId: controlDemoExp!.id, - name: "2. Branch Decision", - description: "Choose between Loop (3) or Wait (4)", - type: "conditional", - orderIndex: 1, - required: true, - durationEstimate: 30, - conditions: { - variable: "demo_branch_choice", - options: [ - { label: "Go to Loop (Step 3)", value: "loop", nextStepId: cdStep3!.id, variant: "default" }, - { label: "Go to Wait (Step 4)", value: "wait", nextStepId: cdStep4!.id, variant: "secondary" } - ] - } - }).returning(); + const [cdStep2] = await db + .insert(schema.steps) + .values({ + experimentId: controlDemoExp!.id, + name: "2. Branch Decision", + description: "Choose between Loop (3) or Wait (4)", + type: "conditional", + orderIndex: 1, + required: true, + durationEstimate: 30, + conditions: { + variable: "demo_branch_choice", + options: [ + { + label: "Go to Loop (Step 3)", + value: "loop", + nextStepId: cdStep3!.id, + variant: "default", + }, + { + label: "Go to Wait (Step 4)", + value: "wait", + nextStepId: cdStep4!.id, + variant: "secondary", + }, + ], + }, + }) + .returning(); // --- Step 1 Actions (Parallel) --- await db.insert(schema.actions).values({ @@ -612,7 +718,7 @@ async function main() { parameters: { text: "Starting control flow demonstration." }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", - category: "interaction" + category: "interaction", }, { id: randomUUID(), @@ -621,13 +727,13 @@ async function main() { parameters: { arm: "right", shoulder_roll: -0.5 }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", - category: "movement" - } - ] + category: "movement", + }, + ], }, pluginId: "hristudio-core", category: "control", - sourceKind: "core" + sourceKind: "core", }); // --- Step 2 Actions (Branch) --- @@ -640,7 +746,7 @@ async function main() { parameters: { text: "Should I loop or wait?" }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", - category: "interaction" + category: "interaction", }, { stepId: cdStep2!.id, @@ -651,12 +757,12 @@ async function main() { prompt_text: "Choose the next path:", options: [ { label: "Loop Path", value: "loop", nextStepId: cdStep3!.id }, - { label: "Wait Path", value: "wait", nextStepId: cdStep4!.id } - ] + { label: "Wait Path", value: "wait", nextStepId: cdStep4!.id }, + ], }, pluginId: "hristudio-woz", category: "wizard", - sourceKind: "core" + sourceKind: "core", }, { stepId: cdStep2!.id, @@ -666,8 +772,8 @@ async function main() { parameters: {}, pluginId: "hristudio-core", category: "control", - sourceKind: "core" - } + sourceKind: "core", + }, ]); // --- Step 3 Actions (Loop) --- @@ -686,13 +792,13 @@ async function main() { parameters: { text: "I am looping." }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", - category: "interaction" - } - ] + category: "interaction", + }, + ], }, pluginId: "hristudio-core", category: "control", - sourceKind: "core" + sourceKind: "core", }); // --- Step 4 Actions (Wait) --- @@ -704,7 +810,7 @@ async function main() { parameters: { duration: 3 }, pluginId: "hristudio-core", category: "control", - sourceKind: "core" + sourceKind: "core", }); // --- Step 5 Actions (Conclusion) --- @@ -716,7 +822,7 @@ async function main() { parameters: { text: "Demonstration complete. Returning to start." }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", - category: "interaction" + category: "interaction", }); // 6. Participants (N=20 for study) @@ -729,21 +835,28 @@ async function main() { name: `Participant ${100 + i}`, consentGiven: true, consentGivenAt: new Date(), - notes: i % 2 === 0 ? "Condition: HRIStudio" : "Condition: Choregraphe" + notes: i % 2 === 0 ? "Condition: HRIStudio" : "Condition: Choregraphe", }); } - const insertedParticipants = await db.insert(schema.participants).values(participants).returning(); + const insertedParticipants = await db + .insert(schema.participants) + .values(participants) + .returning(); console.log("\n✅ Database seeded successfully!"); console.log(`Summary:`); console.log(`- 1 Admin User (sean@soconnor.dev)`); console.log(`- Study: 'Comparative WoZ Study'`); - console.log(`- Experiment: 'The Interactive Storyteller' (6 steps created)`); + console.log( + `- Experiment: 'The Interactive Storyteller' (6 steps created)`, + ); console.log(` - Step 1: The Hook (greeting + welcome gesture)`); console.log(` - Step 2: The Narrative (story + gaze sequence)`); console.log(` - Step 3: Comprehension Check (question + wizard wait)`); console.log(` - Step 4a: Branch A - Correct Response (affirmation + nod)`); - console.log(` - Step 4b: Branch B - Incorrect Response (correction + head shake)`); + console.log( + ` - Step 4b: Branch B - Incorrect Response (correction + head shake)`, + ); console.log(` - Step 5: Conclusion (ending + bow)`); console.log(`- ${insertedParticipants.length} Participants`); @@ -751,20 +864,23 @@ async function main() { console.log("📊 Seeding completed trial with analytics data..."); // Pick participant P101 - const p101 = insertedParticipants.find(p => p.participantCode === "P101"); + const p101 = insertedParticipants.find((p) => p.participantCode === "P101"); if (!p101) throw new Error("P101 not found"); const startTime = new Date(); startTime.setMinutes(startTime.getMinutes() - 10); // Started 10 mins ago const endTime = new Date(); // Ended just now - const [analyticsTrial] = await db.insert(schema.trials).values({ - experimentId: experiment!.id, - participantId: p101.id, - status: "completed", - startedAt: startTime, - completedAt: endTime, - }).returning(); + const [analyticsTrial] = await db + .insert(schema.trials) + .values({ + experimentId: experiment!.id, + participantId: p101.id, + status: "completed", + startedAt: startTime, + completedAt: endTime, + }) + .returning(); // Create a series of events const timelineEvents = []; @@ -781,7 +897,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "trial_started", timestamp: new Date(currentTime), - data: { experimentId: experiment!.id, participantId: p101.id } + data: { experimentId: experiment!.id, participantId: p101.id }, }); // 2. Step 1: The Hook @@ -790,7 +906,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "step_changed", timestamp: new Date(currentTime), - data: { stepId: step1!.id, stepName: "The Hook" } + data: { stepId: step1!.id, stepName: "The Hook" }, }); advance(1); @@ -798,7 +914,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "Say Text", text: "Hello..." } + data: { actionName: "Say Text", text: "Hello..." }, }); advance(5); @@ -806,7 +922,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "Move Arm", arm: "right" } + data: { actionName: "Move Arm", arm: "right" }, }); // 3. Step 2: The Narrative @@ -815,7 +931,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "step_changed", timestamp: new Date(currentTime), - data: { stepId: step2!.id, stepName: "The Narrative" } + data: { stepId: step2!.id, stepName: "The Narrative" }, }); advance(2); @@ -823,7 +939,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "Tell Story" } + data: { actionName: "Tell Story" }, }); // Simulate an intervention/wizard action @@ -832,7 +948,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "intervention", timestamp: new Date(currentTime), - data: { type: "pause", reason: "participant_distracted" } + data: { type: "pause", reason: "participant_distracted" }, }); advance(10); // Paused for 10s @@ -840,7 +956,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "intervention", timestamp: new Date(currentTime), - data: { type: "resume" } + data: { type: "resume" }, }); advance(2); @@ -848,7 +964,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "Turn Head", yaw: 1.5 } + data: { actionName: "Turn Head", yaw: 1.5 }, }); // 4. Step 3: Comprehension Check @@ -857,7 +973,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "step_changed", timestamp: new Date(currentTime), - data: { stepId: step3!.id, stepName: "Comprehension Check" } + data: { stepId: step3!.id, stepName: "Comprehension Check" }, }); advance(1); @@ -865,7 +981,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "Say Text", text: "What color..." } + data: { actionName: "Say Text", text: "What color..." }, }); advance(5); @@ -873,7 +989,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "wizard_action", timestamp: new Date(currentTime), - data: { action: "wait_for_response", prompt: "Did they answer Red?" } + data: { action: "wait_for_response", prompt: "Did they answer Red?" }, }); // Wizard selects "Correct" @@ -882,7 +998,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "wizard_response", timestamp: new Date(currentTime), - data: { response: "Correct", variable: "last_wizard_response" } + data: { response: "Correct", variable: "last_wizard_response" }, }); // 5. Branch A @@ -891,7 +1007,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "step_changed", timestamp: new Date(currentTime), - data: { stepId: step4a!.id, stepName: "Branch A: Correct" } + data: { stepId: step4a!.id, stepName: "Branch A: Correct" }, }); advance(1); @@ -899,7 +1015,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "Say Text with Emotion", emotion: "happy" } + data: { actionName: "Say Text with Emotion", emotion: "happy" }, }); // 6. Conclusion @@ -908,7 +1024,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "step_changed", timestamp: new Date(currentTime), - data: { stepId: step5!.id, stepName: "Conclusion" } + data: { stepId: step5!.id, stepName: "Conclusion" }, }); advance(2); @@ -916,7 +1032,7 @@ async function main() { trialId: analyticsTrial!.id, eventType: "action_executed", timestamp: new Date(currentTime), - data: { actionName: "End Story" } + data: { actionName: "End Story" }, }); // Trial Complete @@ -925,12 +1041,15 @@ async function main() { trialId: analyticsTrial!.id, eventType: "trial_completed", timestamp: new Date(currentTime), - data: { durationSeconds: (currentTime.getTime() - startTime.getTime()) / 1000 } + data: { + durationSeconds: (currentTime.getTime() - startTime.getTime()) / 1000, + }, }); await db.insert(schema.trialEvents).values(timelineEvents); - console.log("✅ Seeded 1 completed trial with " + timelineEvents.length + " events."); - + console.log( + "✅ Seeded 1 completed trial with " + timelineEvents.length + " events.", + ); } catch (error) { console.error("❌ Seeding failed:", error); process.exit(1); diff --git a/src/app/(dashboard)/debug/page.tsx b/src/app/(dashboard)/debug/page.tsx index 98af84e..0057301 100755 --- a/src/app/(dashboard)/debug/page.tsx +++ b/src/app/(dashboard)/debug/page.tsx @@ -46,7 +46,10 @@ export default function DebugPage() { const ROS_BRIDGE_URL = "ws://134.82.159.25:9090"; - const addLog = (message: string, type: "info" | "error" | "success" = "info") => { + const addLog = ( + message: string, + type: "info" | "error" | "success" = "info", + ) => { const timestamp = new Date().toLocaleTimeString(); const logEntry = `[${timestamp}] [${type.toUpperCase()}] ${message}`; setLogs((prev) => [...prev.slice(-99), logEntry]); @@ -79,7 +82,9 @@ export default function DebugPage() { setConnectionStatus("connecting"); setConnectionAttempts((prev) => prev + 1); setLastError(null); - addLog(`Attempting connection #${connectionAttempts + 1} to ${ROS_BRIDGE_URL}`); + addLog( + `Attempting connection #${connectionAttempts + 1} to ${ROS_BRIDGE_URL}`, + ); const socket = new WebSocket(ROS_BRIDGE_URL); @@ -96,7 +101,10 @@ export default function DebugPage() { setConnectionStatus("connected"); setRosSocket(socket); setLastError(null); - addLog("✅ WebSocket connection established successfully", "success"); + addLog( + "[SUCCESS] WebSocket connection established successfully", + "success", + ); // Test basic functionality by advertising const advertiseMsg = { @@ -138,16 +146,20 @@ export default function DebugPage() { addLog(`Connection closed normally: ${event.reason || reason}`); } else if (event.code === 1006) { reason = "Connection lost/refused"; - setLastError("ROS Bridge server not responding - check if rosbridge_server is running"); - addLog(`❌ Connection failed: ${reason} (${event.code})`, "error"); + setLastError( + "ROS Bridge server not responding - check if rosbridge_server is running", + ); + addLog(`[ERROR] Connection failed: ${reason} (${event.code})`, "error"); } else if (event.code === 1011) { reason = "Server error"; setLastError("ROS Bridge server encountered an error"); - addLog(`❌ Server error: ${reason} (${event.code})`, "error"); + addLog(`[ERROR] Server error: ${reason} (${event.code})`, "error"); } else { reason = `Code ${event.code}`; - setLastError(`Connection closed with code ${event.code}: ${event.reason || "No reason given"}`); - addLog(`❌ Connection closed: ${reason}`, "error"); + setLastError( + `Connection closed with code ${event.code}: ${event.reason || "No reason given"}`, + ); + addLog(`[ERROR] Connection closed: ${reason}`, "error"); } if (wasConnected) { @@ -160,7 +172,7 @@ export default function DebugPage() { setConnectionStatus("error"); const errorMsg = "WebSocket error occurred"; setLastError(errorMsg); - addLog(`❌ ${errorMsg}`, "error"); + addLog(`[ERROR] ${errorMsg}`, "error"); console.error("WebSocket error details:", error); }; }; @@ -298,7 +310,7 @@ export default function DebugPage() { > {connectionStatus.toUpperCase()} - + Attempts: {connectionAttempts} @@ -306,7 +318,9 @@ export default function DebugPage() { {lastError && ( - {lastError} + + {lastError} + )} @@ -318,7 +332,9 @@ export default function DebugPage() { className="flex-1" > - {connectionStatus === "connecting" ? "Connecting..." : "Connect"} + {connectionStatus === "connecting" + ? "Connecting..." + : "Connect"} ) : ( - - ))} - - - - ))} - - -
-

- Video Tutorials -

-
- {[ - "Introduction to HRIStudio", - "Advanced Flow Control", - "ROS2 Integration Deep Dive", - ].map((title, i) => ( - -
- -
- - {title} - -
- ))} + return ( + +
+ {guides.map((guide, index) => ( + + +
+
+
-
- -
-
- -
-

Still need help?

-

- Contact your system administrator or check the official documentation for technical support. -

-
-
+ {guide.description} + + +
    + {guide.items.map((item, i) => ( +
  • + - -
-
-
- ); + + ))} + + + + ))} +
+ +
+

+ Video Tutorials +

+
+ {[ + "Introduction to HRIStudio", + "Advanced Flow Control", + "ROS2 Integration Deep Dive", + ].map((title, i) => ( + +
+ +
+ + {title} + +
+ ))} +
+
+ +
+
+ +
+

Still need help?

+

+ Contact your system administrator or check the official documentation + for technical support. +

+
+ + +
+
+ + ); } diff --git a/src/app/(dashboard)/nao-test/page.tsx b/src/app/(dashboard)/nao-test/page.tsx index 1c4ae07..212e3aa 100755 --- a/src/app/(dashboard)/nao-test/page.tsx +++ b/src/app/(dashboard)/nao-test/page.tsx @@ -365,7 +365,9 @@ export default function NaoTestPage() {
- +
- +
- +
- + +
{/* Main Content (Left Column) */}
- {/* Personal Information */}
-
- +
+

Personal Information

Contact Details - Update your public profile information + + Update your public profile information + -
- +
+

Security

Password - Ensure your account stays secure + + Ensure your account stays secure + @@ -105,11 +108,10 @@ function ProfileContent({ user }: { user: ProfileUser }) { {/* Sidebar (Right Column) */}
- {/* Permissions */}
-
- +
+

Permissions

@@ -119,30 +121,40 @@ function ProfileContent({ user }: { user: ProfileUser }) { {user.roles.map((roleInfo, index) => (
- {formatRole(roleInfo.role)} - - Since {new Date(roleInfo.grantedAt).toLocaleDateString()} + + {formatRole(roleInfo.role)} + + + Since{" "} + {new Date(roleInfo.grantedAt).toLocaleDateString()}
-

+

{getRoleDescription(roleInfo.role)}

- {index < (user.roles?.length || 0) - 1 && } + {index < (user.roles?.length || 0) - 1 && ( + + )}
))} -
-
+
+
Role Management
- System roles are managed by administrators. Contact support if you need access adjustments. + System roles are managed by administrators. Contact + support if you need access adjustments.
) : ( -
+

No Roles Assigned

-

Contact an admin to request access.

- +

+ Contact an admin to request access. +

+
)} @@ -151,26 +163,42 @@ function ProfileContent({ user }: { user: ProfileUser }) { {/* Data & Privacy */}
-
- +
+

Data & Privacy

- +
-

Export Data

-

Download a copy of your personal data.

-
-

Delete Account

-

This action is irreversible.

- @@ -193,7 +221,11 @@ export default function ProfilePage() { ]); if (status === "loading") { - return
Loading profile...
; + return ( +
+ Loading profile... +
+ ); } if (!session?.user) { diff --git a/src/app/(dashboard)/studies/[id]/analytics/page.tsx b/src/app/(dashboard)/studies/[id]/analytics/page.tsx index a6fd6b7..1f33a86 100755 --- a/src/app/(dashboard)/studies/[id]/analytics/page.tsx +++ b/src/app/(dashboard)/studies/[id]/analytics/page.tsx @@ -20,7 +20,7 @@ export default function StudyAnalyticsPage() { // Fetch list of trials const { data: trialsList, isLoading } = api.trials.list.useQuery( { studyId, limit: 100 }, - { enabled: !!studyId } + { enabled: !!studyId }, ); // Set breadcrumbs @@ -49,19 +49,23 @@ export default function StudyAnalyticsPage() {
Loading analytics...
}> {isLoading ? ( -
-
-
- Loading session data... +
+
+
+ + Loading session data... +
) : ( - ({ - ...t, - startedAt: t.startedAt ? new Date(t.startedAt) : null, - completedAt: t.completedAt ? new Date(t.completedAt) : null, - createdAt: new Date(t.createdAt), - }))} /> + ({ + ...t, + startedAt: t.startedAt ? new Date(t.startedAt) : null, + completedAt: t.completedAt ? new Date(t.completedAt) : null, + createdAt: new Date(t.createdAt), + }))} + /> )}
diff --git a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx index f58fbc7..6cfc7b3 100755 --- a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx +++ b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx @@ -44,7 +44,7 @@ export function DesignerPageClient({ const stepCount = initialDesign.steps.length; const actionCount = initialDesign.steps.reduce( (sum, step) => sum + step.actions.length, - 0 + 0, ); return { stepCount, actionCount }; diff --git a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx index 7ddab9d..f4012ed 100755 --- a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx +++ b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx @@ -20,7 +20,9 @@ export default async function ExperimentDesignerPage({ }: ExperimentDesignerPageProps) { try { const resolvedParams = await params; - const experiment = await api.experiments.get({ id: resolvedParams.experimentId }); + const experiment = await api.experiments.get({ + id: resolvedParams.experimentId, + }); if (!experiment) { notFound(); @@ -36,13 +38,13 @@ export default async function ExperimentDesignerPage({ // Only pass initialDesign if there's existing visual design data let initialDesign: | { - id: string; - name: string; - description: string; - steps: ExperimentStep[]; - version: number; - lastSaved: Date; - } + id: string; + name: string; + description: string; + steps: ExperimentStep[]; + version: number; + lastSaved: Date; + } | undefined; if (existingDesign?.steps && existingDesign.steps.length > 0) { @@ -220,7 +222,9 @@ export default async function ExperimentDesignerPage({ }; }; - const actions: ExperimentAction[] = s.actions.map((a) => hydrateAction(a)); + const actions: ExperimentAction[] = s.actions.map((a) => + hydrateAction(a), + ); return { id: s.id, name: s.name, @@ -278,7 +282,9 @@ export async function generateMetadata({ }> { try { const resolvedParams = await params; - const experiment = await api.experiments.get({ id: resolvedParams.experimentId }); + const experiment = await api.experiments.get({ + id: resolvedParams.experimentId, + }); return { title: `${experiment?.name} - Designer | HRIStudio`, diff --git a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/page.tsx b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/page.tsx index 441e867..00e2c46 100644 --- a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/page.tsx @@ -1,7 +1,15 @@ "use client"; import { formatDistanceToNow } from "date-fns"; -import { Calendar, Clock, Edit, Play, Settings, Users, TestTube } from "lucide-react"; +import { + Calendar, + Clock, + Edit, + Play, + Settings, + Users, + TestTube, +} from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; import { useEffect, useState } from "react"; @@ -9,13 +17,13 @@ import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { PageHeader } from "~/components/ui/page-header"; import { - EntityView, - EntityViewHeader, - EntityViewSection, - EmptyState, - InfoGrid, - QuickActions, - StatsGrid, + EntityView, + EntityViewHeader, + EntityViewSection, + EmptyState, + InfoGrid, + QuickActions, + StatsGrid, } from "~/components/ui/entity-view"; import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; import { api } from "~/trpc/react"; @@ -23,436 +31,443 @@ import { useSession } from "next-auth/react"; import { useStudyManagement } from "~/hooks/useStudyManagement"; interface ExperimentDetailPageProps { - params: Promise<{ id: string; experimentId: string }>; + params: Promise<{ id: string; experimentId: string }>; } const statusConfig = { - draft: { - label: "Draft", - variant: "secondary" as const, - icon: "FileText" as const, - }, - testing: { - label: "Testing", - variant: "outline" as const, - icon: "TestTube" as const, - }, - ready: { - label: "Ready", - variant: "default" as const, - icon: "CheckCircle" as const, - }, - deprecated: { - label: "Deprecated", - variant: "destructive" as const, - icon: "AlertTriangle" as const, - }, + draft: { + label: "Draft", + variant: "secondary" as const, + icon: "FileText" as const, + }, + testing: { + label: "Testing", + variant: "outline" as const, + icon: "TestTube" as const, + }, + ready: { + label: "Ready", + variant: "default" as const, + icon: "CheckCircle" as const, + }, + deprecated: { + label: "Deprecated", + variant: "destructive" as const, + icon: "AlertTriangle" as const, + }, }; type Experiment = { - id: string; - name: string; - description: string | null; - status: string; - createdAt: Date; - updatedAt: Date; - study: { id: string; name: string }; - robot: { id: string; name: string; description: string | null } | null; - protocol?: { blocks: unknown[] } | null; - visualDesign?: unknown; - studyId: string; - createdBy: string; - robotId: string | null; - version: number; + id: string; + name: string; + description: string | null; + status: string; + createdAt: Date; + updatedAt: Date; + study: { id: string; name: string }; + robot: { id: string; name: string; description: string | null } | null; + protocol?: { blocks: unknown[] } | null; + visualDesign?: unknown; + studyId: string; + createdBy: string; + robotId: string | null; + version: number; }; type Trial = { + id: string; + status: string; + createdAt: Date; + duration: number | null; + participant: { id: string; - status: string; - createdAt: Date; - duration: number | null; - participant: { - id: string; - participantCode: string; - name?: string | null; - } | null; - experiment: { name: string } | null; - participantId: string | null; - experimentId: string; - startedAt: Date | null; - completedAt: Date | null; - notes: string | null; - updatedAt: Date; - canAccess: boolean; - userRole: string; + participantCode: string; + name?: string | null; + } | null; + experiment: { name: string } | null; + participantId: string | null; + experimentId: string; + startedAt: Date | null; + completedAt: Date | null; + notes: string | null; + updatedAt: Date; + canAccess: boolean; + userRole: string; }; export default function ExperimentDetailPage({ - params, + params, }: ExperimentDetailPageProps) { - const { data: session } = useSession(); - const [experiment, setExperiment] = useState(null); - const [trials, setTrials] = useState([]); - const [loading, setLoading] = useState(true); - const [resolvedParams, setResolvedParams] = useState<{ id: string; experimentId: string } | null>( - null, - ); - const { selectStudy } = useStudyManagement(); + const { data: session } = useSession(); + const [experiment, setExperiment] = useState(null); + const [trials, setTrials] = useState([]); + const [loading, setLoading] = useState(true); + const [resolvedParams, setResolvedParams] = useState<{ + id: string; + experimentId: string; + } | null>(null); + const { selectStudy } = useStudyManagement(); - useEffect(() => { - const resolveParams = async () => { - const resolved = await params; - setResolvedParams(resolved); - // Ensure study context is synced - if (resolved.id) { - void selectStudy(resolved.id); - } - }; - void resolveParams(); - }, [params, selectStudy]); + useEffect(() => { + const resolveParams = async () => { + const resolved = await params; + setResolvedParams(resolved); + // Ensure study context is synced + if (resolved.id) { + void selectStudy(resolved.id); + } + }; + void resolveParams(); + }, [params, selectStudy]); - const experimentQuery = api.experiments.get.useQuery( - { id: resolvedParams?.experimentId ?? "" }, - { enabled: !!resolvedParams?.experimentId }, - ); + const experimentQuery = api.experiments.get.useQuery( + { id: resolvedParams?.experimentId ?? "" }, + { enabled: !!resolvedParams?.experimentId }, + ); - const trialsQuery = api.trials.list.useQuery( - { experimentId: resolvedParams?.experimentId ?? "" }, - { enabled: !!resolvedParams?.experimentId }, - ); + const trialsQuery = api.trials.list.useQuery( + { experimentId: resolvedParams?.experimentId ?? "" }, + { enabled: !!resolvedParams?.experimentId }, + ); - useEffect(() => { - if (experimentQuery.data) { - setExperiment(experimentQuery.data); - } - }, [experimentQuery.data]); + useEffect(() => { + if (experimentQuery.data) { + setExperiment(experimentQuery.data); + } + }, [experimentQuery.data]); - useEffect(() => { - if (trialsQuery.data) { - setTrials(trialsQuery.data); - } - }, [trialsQuery.data]); + useEffect(() => { + if (trialsQuery.data) { + setTrials(trialsQuery.data); + } + }, [trialsQuery.data]); - useEffect(() => { - if (experimentQuery.isLoading || trialsQuery.isLoading) { - setLoading(true); - } else { - setLoading(false); - } - }, [experimentQuery.isLoading, trialsQuery.isLoading]); + useEffect(() => { + if (experimentQuery.isLoading || trialsQuery.isLoading) { + setLoading(true); + } else { + setLoading(false); + } + }, [experimentQuery.isLoading, trialsQuery.isLoading]); - // Set breadcrumbs - useBreadcrumbsEffect([ - { - label: "Dashboard", - href: "/", - }, - { - label: "Studies", - href: "/studies", - }, - { - label: experiment?.study?.name ?? "Study", - href: `/studies/${experiment?.study?.id}`, - }, - { - label: "Experiments", - href: `/studies/${experiment?.study?.id}/experiments`, - }, - { - label: experiment?.name ?? "Experiment", - }, - ]); + // Set breadcrumbs + useBreadcrumbsEffect([ + { + label: "Dashboard", + href: "/", + }, + { + label: "Studies", + href: "/studies", + }, + { + label: experiment?.study?.name ?? "Study", + href: `/studies/${experiment?.study?.id}`, + }, + { + label: "Experiments", + href: `/studies/${experiment?.study?.id}/experiments`, + }, + { + label: experiment?.name ?? "Experiment", + }, + ]); - if (loading) return
Loading...
; - if (experimentQuery.error) return notFound(); - if (!experiment) return notFound(); + if (loading) return
Loading...
; + if (experimentQuery.error) return notFound(); + if (!experiment) return notFound(); - const displayName = experiment.name ?? "Untitled Experiment"; - const description = experiment.description; + const displayName = experiment.name ?? "Untitled Experiment"; + const description = experiment.description; - // Check if user can edit this experiment - const userRoles = session?.user?.roles?.map((r) => r.role) ?? []; - const canEdit = - userRoles.includes("administrator") || userRoles.includes("researcher"); + // Check if user can edit this experiment + const userRoles = session?.user?.roles?.map((r) => r.role) ?? []; + const canEdit = + userRoles.includes("administrator") || userRoles.includes("researcher"); - const statusInfo = - statusConfig[experiment.status as keyof typeof statusConfig]; + const statusInfo = + statusConfig[experiment.status as keyof typeof statusConfig]; - const studyId = experiment.study.id; - const experimentId = experiment.id; + const studyId = experiment.study.id; + const experimentId = experiment.id; - return ( - - - - -
- ) : undefined - } - /> - -
-
- {/* Basic Information */} - - - {experiment.study.name} - - ) : ( - "No study assigned" - ), - }, - { - label: "Status", - value: statusInfo?.label ?? "Unknown", - }, - { - label: "Created", - value: formatDistanceToNow(experiment.createdAt, { - addSuffix: true, - }), - }, - { - label: "Last Updated", - value: formatDistanceToNow(experiment.updatedAt, { - addSuffix: true, - }), - }, - ]} - /> - - - {/* Protocol Section */} - - - - Edit Protocol - - - ) - } - > - {experiment.protocol && - typeof experiment.protocol === "object" && - experiment.protocol !== null ? ( -
-
- Protocol contains{" "} - {Array.isArray( - (experiment.protocol as { blocks: unknown[] }).blocks, - ) - ? (experiment.protocol as { blocks: unknown[] }).blocks - .length - : 0}{" "} - blocks -
-
- ) : ( - - - Open Designer - - - ) - } - /> - )} -
- - {/* Recent Trials */} - - - View All - - - } - > - {trials.length > 0 ? ( -
- {trials.slice(0, 5).map((trial) => ( -
-
- - Trial #{trial.id.slice(-6)} - - - {trial.status.charAt(0).toUpperCase() + - trial.status.slice(1).replace("_", " ")} - -
-
- - - {formatDistanceToNow(trial.createdAt, { - addSuffix: true, - })} - - {trial.duration && ( - - - {Math.round(trial.duration / 60)} min - - )} - {trial.participant && ( - - - {trial.participant.name ?? - trial.participant.participantCode} - - )} -
-
- ))} -
- ) : ( - - - Start Trial - - - ) - } - /> - )} -
-
- -
- {/* Statistics */} - - t.status === "completed").length, - }, - { - label: "In Progress", - value: trials.filter((t) => t.status === "in_progress") - .length, - }, - ]} - /> - - - {/* Robot Information */} - {experiment.robot && ( - - - - )} - - {/* Quick Actions */} - - - -
+ return ( + + + +
- - ); + ) : undefined + } + /> + +
+
+ {/* Basic Information */} + + + {experiment.study.name} + + ) : ( + "No study assigned" + ), + }, + { + label: "Status", + value: statusInfo?.label ?? "Unknown", + }, + { + label: "Created", + value: formatDistanceToNow(experiment.createdAt, { + addSuffix: true, + }), + }, + { + label: "Last Updated", + value: formatDistanceToNow(experiment.updatedAt, { + addSuffix: true, + }), + }, + ]} + /> + + + {/* Protocol Section */} + + + + Edit Protocol + + + ) + } + > + {experiment.protocol && + typeof experiment.protocol === "object" && + experiment.protocol !== null ? ( +
+
+ Protocol contains{" "} + {Array.isArray( + (experiment.protocol as { blocks: unknown[] }).blocks, + ) + ? (experiment.protocol as { blocks: unknown[] }).blocks + .length + : 0}{" "} + blocks +
+
+ ) : ( + + + Open Designer + + + ) + } + /> + )} +
+ + {/* Recent Trials */} + + + View All + + + } + > + {trials.length > 0 ? ( +
+ {trials.slice(0, 5).map((trial) => ( +
+
+ + Trial #{trial.id.slice(-6)} + + + {trial.status.charAt(0).toUpperCase() + + trial.status.slice(1).replace("_", " ")} + +
+
+ + + {formatDistanceToNow(trial.createdAt, { + addSuffix: true, + })} + + {trial.duration && ( + + + {Math.round(trial.duration / 60)} min + + )} + {trial.participant && ( + + + {trial.participant.name ?? + trial.participant.participantCode} + + )} +
+
+ ))} +
+ ) : ( + + + Start Trial + + + ) + } + /> + )} +
+
+ +
+ {/* Statistics */} + + t.status === "completed").length, + }, + { + label: "In Progress", + value: trials.filter((t) => t.status === "in_progress") + .length, + }, + ]} + /> + + + {/* Robot Information */} + {experiment.robot && ( + + + + )} + + {/* Quick Actions */} + + + +
+
+ + ); } diff --git a/src/app/(dashboard)/studies/[id]/forms/page.tsx b/src/app/(dashboard)/studies/[id]/forms/page.tsx new file mode 100644 index 0000000..79e437b --- /dev/null +++ b/src/app/(dashboard)/studies/[id]/forms/page.tsx @@ -0,0 +1,317 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { notFound } from "next/navigation"; +import { FileText, Loader2, Plus, Download, Edit2, Eye, Save } from "lucide-react"; +import { + EntityView, + EntityViewHeader, + EntityViewSection, + EmptyState, +} from "~/components/ui/entity-view"; +import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; +import { PageHeader } from "~/components/ui/page-header"; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import { Markdown } from 'tiptap-markdown'; +import { Table } from '@tiptap/extension-table'; +import { TableRow } from '@tiptap/extension-table-row'; +import { TableCell } from '@tiptap/extension-table-cell'; +import { TableHeader } from '@tiptap/extension-table-header'; +import { Bold, Italic, List, ListOrdered, Heading1, Heading2, Quote, Table as TableIcon } from "lucide-react"; +import { downloadPdfFromHtml } from "~/lib/pdf-generator"; + +const Toolbar = ({ editor }: { editor: any }) => { + if (!editor) { + return null; + } + + return ( +
+ + +
+ + +
+ + + +
+ +
+ ); +}; + +interface StudyFormsPageProps { + params: Promise<{ + id: string; + }>; +} + +export default function StudyFormsPage({ params }: StudyFormsPageProps) { + const { data: session } = useSession(); + const utils = api.useUtils(); + const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(null); + const [editorTarget, setEditorTarget] = useState(""); + + useEffect(() => { + const resolveParams = async () => { + const resolved = await params; + setResolvedParams(resolved); + }; + void resolveParams(); + }, [params]); + + const { data: study } = api.studies.get.useQuery( + { id: resolvedParams?.id ?? "" }, + { enabled: !!resolvedParams?.id }, + ); + + const { data: activeConsentForm, refetch: refetchConsentForm } = + api.studies.getActiveConsentForm.useQuery( + { studyId: resolvedParams?.id ?? "" }, + { enabled: !!resolvedParams?.id }, + ); + + // Only sync once when form loads to avoid resetting user edits + useEffect(() => { + if (activeConsentForm && !editorTarget) { + setEditorTarget(activeConsentForm.content); + } + }, [activeConsentForm, editorTarget]); + + const editor = useEditor({ + extensions: [ + StarterKit, + Table.configure({ + resizable: true, + }), + TableRow, + TableHeader, + TableCell, + Markdown.configure({ + transformPastedText: true, + }), + ], + content: editorTarget || '', + immediatelyRender: false, + onUpdate: ({ editor }) => { + // @ts-ignore + setEditorTarget(editor.storage.markdown.getMarkdown()); + }, + }); + + // Sync Tiptap when editorTarget is set (e.g., from DB) but make sure not to overwrite active edits + useEffect(() => { + if (editor && editorTarget && editor.isEmpty) { + editor.commands.setContent(editorTarget); + } + }, [editorTarget, editor]); + + const generateConsentMutation = api.studies.generateConsentForm.useMutation({ + onSuccess: (data) => { + toast.success("Default Consent Form Generated!"); + setEditorTarget(data.content); + editor?.commands.setContent(data.content); + void refetchConsentForm(); + void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" }); + }, + onError: (error) => { + toast.error("Error generating consent form", { description: error.message }); + }, + }); + + const updateConsentMutation = api.studies.updateConsentForm.useMutation({ + onSuccess: () => { + toast.success("Consent Form Saved Successfully!"); + void refetchConsentForm(); + void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" }); + }, + onError: (error) => { + toast.error("Error saving consent form", { description: error.message }); + }, + }); + + const handleDownloadConsent = async () => { + if (!activeConsentForm || !study || !editor) return; + + try { + toast.loading("Generating Document...", { id: "pdf-gen" }); + await downloadPdfFromHtml(editor.getHTML(), { + filename: `Consent_Form_${study.name.replace(/\s+/g, "_")}_v${activeConsentForm.version}.pdf` + }); + toast.success("Document Downloaded Successfully!", { id: "pdf-gen" }); + } catch (error) { + toast.error("Error generating PDF", { id: "pdf-gen" }); + console.error(error); + } + }; + + useBreadcrumbsEffect([ + { label: "Dashboard", href: "/dashboard" }, + { label: "Studies", href: "/studies" }, + { label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` }, + { label: "Forms" }, + ]); + + if (!session?.user) { + return notFound(); + } + + if (!study) return
Loading...
; + + return ( + + + +
+ + + {activeConsentForm && ( + + )} +
+ } + > + {activeConsentForm ? ( +
+
+
+

+ {activeConsentForm.title} +

+

+ v{activeConsentForm.version} • Status: Active +

+
+
+ + Active +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ ) : ( + + )} + +
+ + ); +} diff --git a/src/app/(dashboard)/studies/[id]/page.tsx b/src/app/(dashboard)/studies/[id]/page.tsx index 5a95f30..aebac74 100755 --- a/src/app/(dashboard)/studies/[id]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/page.tsx @@ -71,6 +71,7 @@ type Member = { export default function StudyDetailPage({ params }: StudyDetailPageProps) { const { data: session } = useSession(); + const utils = api.useUtils(); const [study, setStudy] = useState(null); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); @@ -176,7 +177,7 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) { { label: statusInfo?.label ?? "Unknown", variant: statusInfo?.variant ?? "secondary", - } + }, ]} actions={
@@ -301,12 +302,18 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
diff --git a/src/app/(dashboard)/studies/[id]/participants/[participantId]/edit/page.tsx b/src/app/(dashboard)/studies/[id]/participants/[participantId]/edit/page.tsx index cd8c3d7..eb01486 100644 --- a/src/app/(dashboard)/studies/[id]/participants/[participantId]/edit/page.tsx +++ b/src/app/(dashboard)/studies/[id]/participants/[participantId]/edit/page.tsx @@ -3,29 +3,29 @@ import { api } from "~/trpc/server"; import { notFound } from "next/navigation"; interface EditParticipantPageProps { - params: Promise<{ - id: string; - participantId: string; - }>; + params: Promise<{ + id: string; + participantId: string; + }>; } export default async function EditParticipantPage({ - params, + params, }: EditParticipantPageProps) { - const { id: studyId, participantId } = await params; + const { id: studyId, participantId } = await params; - const participant = await api.participants.get({ id: participantId }); + const participant = await api.participants.get({ id: participantId }); - if (!participant || participant.studyId !== studyId) { - notFound(); - } + if (!participant || participant.studyId !== studyId) { + notFound(); + } - // Transform data to match form expectations if needed, or pass directly - return ( - - ); + // Transform data to match form expectations if needed, or pass directly + return ( + + ); } diff --git a/src/app/(dashboard)/studies/[id]/participants/[participantId]/page.tsx b/src/app/(dashboard)/studies/[id]/participants/[participantId]/page.tsx index 0892b5d..f678c02 100644 --- a/src/app/(dashboard)/studies/[id]/participants/[participantId]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/participants/[participantId]/page.tsx @@ -1,12 +1,18 @@ import { notFound } from "next/navigation"; import { api } from "~/trpc/server"; import { - EntityView, - EntityViewHeader, - EntityViewSection, + EntityView, + EntityViewHeader, + EntityViewSection, } from "~/components/ui/entity-view"; import { ParticipantDocuments } from "./participant-documents"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "~/components/ui/card"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Button } from "~/components/ui/button"; @@ -17,104 +23,129 @@ import { PageHeader } from "~/components/ui/page-header"; import { ParticipantConsentManager } from "~/components/participants/ParticipantConsentManager"; interface ParticipantDetailPageProps { - params: Promise<{ id: string; participantId: string }>; + params: Promise<{ id: string; participantId: string }>; } export default async function ParticipantDetailPage({ - params, + params, }: ParticipantDetailPageProps) { - const { id: studyId, participantId } = await params; + const { id: studyId, participantId } = await params; - const participant = await api.participants.get({ id: participantId }); + const participant = await api.participants.get({ id: participantId }); - if (!participant) { - notFound(); - } + if (!participant) { + notFound(); + } - // Ensure participant belongs to study - if (participant.studyId !== studyId) { - notFound(); - } + // Ensure participant belongs to study + if (participant.studyId !== studyId) { + notFound(); + } - return ( - - - - - Edit Participant - - - } + return ( + + + + + Edit Participant + + + } + /> + + + + Overview + Files & Documents + + + +
+ + +
+
+ Code + + {participant.participantCode} + +
- - - Overview - Files & Documents - +
+ Name + + {participant.name || "-"} + +
- -
- - -
-
- Code - {participant.participantCode} -
+
+ + Email + + + {participant.email || "-"} + +
-
- Name - {participant.name || "-"} -
+
+ + Added + + + {new Date(participant.createdAt).toLocaleDateString()} + +
-
- Email - {participant.email || "-"} -
+
+ Age + + {(participant.demographics as any)?.age || "-"} + +
-
- Added - {new Date(participant.createdAt).toLocaleDateString()} -
+
+ + Gender + + + {(participant.demographics as any)?.gender?.replace( + "_", + " ", + ) || "-"} + +
+
+
+
+
-
- Age - {(participant.demographics as any)?.age || "-"} -
- -
- Gender - {(participant.demographics as any)?.gender?.replace("_", " ") || "-"} -
-
-
-
-
- - - - - - -
-
- ); + + + + + + +
+ ); } diff --git a/src/app/(dashboard)/studies/[id]/participants/[participantId]/participant-documents.tsx b/src/app/(dashboard)/studies/[id]/participants/[participantId]/participant-documents.tsx index 7372755..cb6f538 100644 --- a/src/app/(dashboard)/studies/[id]/participants/[participantId]/participant-documents.tsx +++ b/src/app/(dashboard)/studies/[id]/participants/[participantId]/participant-documents.tsx @@ -4,184 +4,192 @@ import { useState } from "react"; import { Upload, FileText, Trash2, Download, Loader2 } from "lucide-react"; import { Button } from "~/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "~/components/ui/card"; import { api } from "~/trpc/react"; import { formatBytes } from "~/lib/utils"; import { toast } from "sonner"; interface ParticipantDocumentsProps { - participantId: string; + participantId: string; } -export function ParticipantDocuments({ participantId }: ParticipantDocumentsProps) { - const [isUploading, setIsUploading] = useState(false); - const utils = api.useUtils(); +export function ParticipantDocuments({ + participantId, +}: ParticipantDocumentsProps) { + const [isUploading, setIsUploading] = useState(false); + const utils = api.useUtils(); - const { data: documents, isLoading } = api.files.listParticipantDocuments.useQuery({ + const { data: documents, isLoading } = + api.files.listParticipantDocuments.useQuery({ + participantId, + }); + + const getPresignedUrl = api.files.getPresignedUrl.useMutation(); + const registerUpload = api.files.registerUpload.useMutation(); + const deleteDocument = api.files.deleteDocument.useMutation({ + onSuccess: () => { + toast.success("Document deleted"); + utils.files.listParticipantDocuments.invalidate({ participantId }); + }, + onError: (err) => toast.error(`Failed to delete: ${err.message}`), + }); + + // Since presigned URLs are for PUT, we can use a direct fetch + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setIsUploading(true); + try { + // 1. Get presigned URL + const { url, storagePath } = await getPresignedUrl.mutateAsync({ + filename: file.name, + contentType: file.type || "application/octet-stream", participantId, - }); + }); - const getPresignedUrl = api.files.getPresignedUrl.useMutation(); - const registerUpload = api.files.registerUpload.useMutation(); - const deleteDocument = api.files.deleteDocument.useMutation({ - onSuccess: () => { - toast.success("Document deleted"); - utils.files.listParticipantDocuments.invalidate({ participantId }); + // 2. Upload to MinIO/S3 + const uploadRes = await fetch(url, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type || "application/octet-stream", }, - onError: (err) => toast.error(`Failed to delete: ${err.message}`), - }); + }); - // Since presigned URLs are for PUT, we can use a direct fetch - const handleFileUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; + if (!uploadRes.ok) { + throw new Error("Upload to storage failed"); + } - setIsUploading(true); - try { - // 1. Get presigned URL - const { url, storagePath } = await getPresignedUrl.mutateAsync({ - filename: file.name, - contentType: file.type || "application/octet-stream", - participantId, - }); + // 3. Register in DB + await registerUpload.mutateAsync({ + participantId, + name: file.name, + type: file.type, + storagePath, + fileSize: file.size, + }); - // 2. Upload to MinIO/S3 - const uploadRes = await fetch(url, { - method: "PUT", - body: file, - headers: { - "Content-Type": file.type || "application/octet-stream", - }, - }); + toast.success("File uploaded successfully"); + utils.files.listParticipantDocuments.invalidate({ participantId }); + } catch (error) { + console.error(error); + toast.error("Failed to upload file"); + } finally { + setIsUploading(false); + // Reset input + e.target.value = ""; + } + }; - if (!uploadRes.ok) { - throw new Error("Upload to storage failed"); - } + const handleDownload = async (storagePath: string, filename: string) => { + // We would typically get a temporary download URL here + // For now assuming public bucket or implementing a separate download procedure + // Let's implement a quick procedure call right here via client or assume the server router has it. + // I added getDownloadUrl to the router in previous steps. + try { + const { url } = await utils.client.files.getDownloadUrl.query({ + storagePath, + }); + window.open(url, "_blank"); + } catch (e) { + toast.error("Could not get download URL"); + } + }; - // 3. Register in DB - await registerUpload.mutateAsync({ - participantId, - name: file.name, - type: file.type, - storagePath, - fileSize: file.size, - }); - - toast.success("File uploaded successfully"); - utils.files.listParticipantDocuments.invalidate({ participantId }); - } catch (error) { - console.error(error); - toast.error("Failed to upload file"); - } finally { - setIsUploading(false); - // Reset input - e.target.value = ""; - } - }; - - const handleDownload = async (storagePath: string, filename: string) => { - // We would typically get a temporary download URL here - // For now assuming public bucket or implementing a separate download procedure - // Let's implement a quick procedure call right here via client or assume the server router has it. - // I added getDownloadUrl to the router in previous steps. - try { - const { url } = await utils.client.files.getDownloadUrl.query({ storagePath }); - window.open(url, "_blank"); - } catch (e) { - toast.error("Could not get download URL"); - } - }; - - return ( - - -
-
- Documents - - Manage consent forms and other files for this participant. - -
-
- -
-
-
- - {isLoading ? ( -
- -
- ) : documents?.length === 0 ? ( -
- -

No documents uploaded yet.

-
+ return ( + + +
+
+ Documents + + Manage consent forms and other files for this participant. + +
+
+ - -
-
- ))} -
+ )} - - - ); + Upload PDF + + + +
+
+ + + {isLoading ? ( +
+ +
+ ) : documents?.length === 0 ? ( +
+ +

No documents uploaded yet.

+
+ ) : ( +
+ {documents?.map((doc) => ( +
+
+
+ +
+
+

{doc.name}

+

+ {formatBytes(doc.fileSize ?? 0)} •{" "} + {new Date(doc.createdAt).toLocaleDateString()} +

+
+
+
+ + +
+
+ ))} +
+ )} +
+ + ); } diff --git a/src/app/(dashboard)/studies/[id]/trials/[trialId]/analysis/page.tsx b/src/app/(dashboard)/studies/[id]/trials/[trialId]/analysis/page.tsx index e839948..0b71c99 100644 --- a/src/app/(dashboard)/studies/[id]/trials/[trialId]/analysis/page.tsx +++ b/src/app/(dashboard)/studies/[id]/trials/[trialId]/analysis/page.tsx @@ -13,111 +13,112 @@ import { TrialAnalysisView } from "~/components/trials/views/TrialAnalysisView"; import { api } from "~/trpc/react"; function AnalysisPageContent() { - const params = useParams(); - const studyId: string = typeof params.id === "string" ? params.id : ""; - const trialId: string = - typeof params.trialId === "string" ? params.trialId : ""; + const params = useParams(); + const studyId: string = typeof params.id === "string" ? params.id : ""; + const trialId: string = + typeof params.trialId === "string" ? params.trialId : ""; - const { setSelectedStudyId, selectedStudyId } = useStudyContext(); - const { study } = useSelectedStudyDetails(); + const { setSelectedStudyId, selectedStudyId } = useStudyContext(); + const { study } = useSelectedStudyDetails(); - // Get trial data - const { - data: trial, - isLoading, - error, - } = api.trials.get.useQuery({ id: trialId }, { enabled: !!trialId }); + // Get trial data + const { + data: trial, + isLoading, + error, + } = api.trials.get.useQuery({ id: trialId }, { enabled: !!trialId }); - // Set breadcrumbs - useBreadcrumbsEffect([ - { label: "Dashboard", href: "/dashboard" }, - { label: "Studies", href: "/studies" }, - { label: study?.name ?? "Study", href: `/studies/${studyId}` }, - { label: "Trials", href: `/studies/${studyId}/trials` }, - { - label: trial?.experiment.name ?? "Trial", - href: `/studies/${studyId}/trials`, - }, - { label: "Analysis" }, - ]); + // Set breadcrumbs + useBreadcrumbsEffect([ + { label: "Dashboard", href: "/dashboard" }, + { label: "Studies", href: "/studies" }, + { label: study?.name ?? "Study", href: `/studies/${studyId}` }, + { label: "Trials", href: `/studies/${studyId}/trials` }, + { + label: trial?.experiment.name ?? "Trial", + href: `/studies/${studyId}/trials`, + }, + { label: "Analysis" }, + ]); - // Sync selected study (unified study-context) - useEffect(() => { - if (studyId && selectedStudyId !== studyId) { - setSelectedStudyId(studyId); - } - }, [studyId, selectedStudyId, setSelectedStudyId]); - - if (isLoading) { - return ( -
-
Loading analysis...
-
- ); + // Sync selected study (unified study-context) + useEffect(() => { + if (studyId && selectedStudyId !== studyId) { + setSelectedStudyId(studyId); } + }, [studyId, selectedStudyId, setSelectedStudyId]); - if (error || !trial) { - return ( -
- - - - Back to Trials - - - } - /> -
-
-

- {error ? "Error Loading Trial" : "Trial Not Found"} -

-

- {error?.message || "The requested trial could not be found."} -

-
-
-
- ); - } - - const customTrialData = { - ...trial, - startedAt: trial.startedAt ? new Date(trial.startedAt) : null, - completedAt: trial.completedAt ? new Date(trial.completedAt) : null, - eventCount: (trial as any).eventCount, - mediaCount: (trial as any).mediaCount, - media: trial.media?.map(m => ({ - ...m, - mediaType: m.mediaType ?? "video", - format: m.format ?? undefined, - contentType: m.contentType ?? undefined - })) ?? [], - }; - + if (isLoading) { return ( - +
+
Loading analysis...
+
); + } + + if (error || !trial) { + return ( +
+ + + + Back to Trials + + + } + /> +
+
+

+ {error ? "Error Loading Trial" : "Trial Not Found"} +

+

+ {error?.message || "The requested trial could not be found."} +

+
+
+
+ ); + } + + const customTrialData = { + ...trial, + startedAt: trial.startedAt ? new Date(trial.startedAt) : null, + completedAt: trial.completedAt ? new Date(trial.completedAt) : null, + eventCount: (trial as any).eventCount, + mediaCount: (trial as any).mediaCount, + media: + trial.media?.map((m) => ({ + ...m, + mediaType: m.mediaType ?? "video", + format: m.format ?? undefined, + contentType: m.contentType ?? undefined, + })) ?? [], + }; + + return ( + + ); } export default function TrialAnalysisPage() { - return ( - -
Loading...
-
- } - > - - - ); + return ( + +
Loading...
+
+ } + > + + + ); } diff --git a/src/app/(dashboard)/studies/[id]/trials/[trialId]/page.tsx b/src/app/(dashboard)/studies/[id]/trials/[trialId]/page.tsx index 753e20a..235bb71 100755 --- a/src/app/(dashboard)/studies/[id]/trials/[trialId]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/trials/[trialId]/page.tsx @@ -3,7 +3,14 @@ import { useParams } from "next/navigation"; import { Suspense, useEffect } from "react"; import Link from "next/link"; -import { Play, Zap, ArrowLeft, User, FlaskConical, LineChart } from "lucide-react"; +import { + Play, + Zap, + ArrowLeft, + User, + FlaskConical, + LineChart, +} from "lucide-react"; import { PageHeader } from "~/components/ui/page-header"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; @@ -144,7 +151,7 @@ function TrialDetailContent() { { label: trial.status.replace("_", " ").toUpperCase(), variant: getStatusBadgeVariant(trial.status), - } + }, ]} actions={
@@ -156,13 +163,13 @@ function TrialDetailContent() { )} {(trial.status === "in_progress" || trial.status === "scheduled") && ( - - )} + + )} {trial.status === "completed" && ( -
+
Don't have an account?{" "} Sign up @@ -158,7 +175,7 @@ export default function SignInPage() { {/* Footer */} -
+

© {new Date().getFullYear()} HRIStudio. All rights reserved.

diff --git a/src/app/auth/signout/page.tsx b/src/app/auth/signout/page.tsx index f03ee8a..f085897 100755 --- a/src/app/auth/signout/page.tsx +++ b/src/app/auth/signout/page.tsx @@ -6,11 +6,11 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Button } from "~/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "~/components/ui/card"; export default function SignOutPage() { @@ -44,7 +44,7 @@ export default function SignOutPage() { return (
-
+

Loading...

@@ -79,7 +79,8 @@ export default function SignOutPage() {

- Currently signed in as: {session.user.name ?? session.user.email} + Currently signed in as:{" "} + {session.user.name ?? session.user.email}

diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index fe18614..138c06b 100755 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -9,7 +9,7 @@ import { CardContent, CardDescription, CardHeader, - CardTitle + CardTitle, } from "~/components/ui/card"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; @@ -56,25 +56,30 @@ export default function SignUpPage() { }; return ( -
+
{/* Background Gradients */} -
+
-
+
{/* Header */}
- + -

Create an account

-

+

+ Create an account +

+

Start your journey in HRI research

{/* Sign Up Card */} - + Sign Up @@ -84,7 +89,7 @@ export default function SignUpPage() {
{error && ( -
+
{error}
)} @@ -155,15 +160,17 @@ export default function SignUpPage() { disabled={createUser.isPending} size="lg" > - {createUser.isPending ? "Creating account..." : "Create Account"} + {createUser.isPending + ? "Creating account..." + : "Create Account"} -
+
Already have an account?{" "} Sign in @@ -172,7 +179,7 @@ export default function SignUpPage() { {/* Footer */} -
+

© {new Date().getFullYear()} HRIStudio. All rights reserved.

diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 7959308..32907ae 100755 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -77,7 +77,7 @@ export default function DashboardPage() { const { data: liveTrials } = api.dashboard.getLiveTrials.useQuery( { studyId: studyFilter ?? undefined }, - { refetchInterval: 5000 } + { refetchInterval: 5000 }, ); const { data: recentActivity } = api.dashboard.getRecentActivity.useQuery({ @@ -102,11 +102,14 @@ export default function DashboardPage() { }; return ( -
+
{/* Header Section */} -
+
-

+

{getWelcomeMessage()}

@@ -115,7 +118,12 @@ export default function DashboardPage() {

- - table.getColumn("participantCode")?.setFilterValue(event.target.value) - } - className="max-w-sm" - id="tour-analytics-filter" - /> -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
- - -
-
+ return ( +
+
+ + table + .getColumn("participantCode") + ?.setFilterValue(event.target.value) + } + className="max-w-sm" + id="tour-analytics-filter" + /> +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected.
- ); +
+ + +
+
+
+ ); } diff --git a/src/components/dashboard/app-sidebar.tsx b/src/components/dashboard/app-sidebar.tsx index 4284e60..9a3b82c 100755 --- a/src/components/dashboard/app-sidebar.tsx +++ b/src/components/dashboard/app-sidebar.tsx @@ -21,6 +21,7 @@ import { User, UserCheck, Users, + FileText, } from "lucide-react"; import { useSidebar } from "~/components/ui/sidebar"; @@ -96,6 +97,11 @@ const studyWorkItems = [ url: "/experiments", icon: FlaskConical, }, + { + title: "Forms", + url: "/forms", + icon: FileText, + }, { title: "Analytics", url: "/analytics", @@ -143,10 +149,15 @@ export function AppSidebar({ const isAdmin = userRole === "administrator"; const { state: sidebarState } = useSidebar(); const isCollapsed = sidebarState === "collapsed"; - const { selectedStudyId, userStudies, selectStudy, refreshStudyData, isLoadingUserStudies } = - useStudyManagement(); + const { + selectedStudyId, + userStudies, + selectStudy, + refreshStudyData, + isLoadingUserStudies, + } = useStudyManagement(); - const { startTour } = useTour(); + const { startTour, isTourActive } = useTour(); // Reference to track if we've already attempted auto-selection to avoid fighting with manual clearing const hasAutoSelected = useRef(false); @@ -170,12 +181,7 @@ export function AppSidebar({ hasAutoSelected.current = true; } } - }, [ - isLoadingUserStudies, - selectedStudyId, - userStudies, - selectStudy, - ]); + }, [isLoadingUserStudies, selectedStudyId, userStudies, selectStudy]); // Debug API call const { data: debugData } = api.dashboard.debug.useQuery(undefined, { @@ -309,6 +315,17 @@ export function AppSidebar({ + {isTourActive && !isCollapsed && ( +
+
+ + + + + Tutorial Active +
+
+ )} @@ -324,7 +341,10 @@ export function AppSidebar({ - + {selectedStudy?.name ?? "Select Study"} @@ -373,7 +393,10 @@ export function AppSidebar({ ) : ( - + {selectedStudy?.name ?? "Select Study"} @@ -576,22 +599,23 @@ export function AppSidebar({ {helpItems.map((item) => { const isActive = pathname.startsWith(item.url); - const menuButton = item.action === "tour" ? ( - startTour("full_platform")} - isActive={false} - > - - {item.title} - - ) : ( - - + const menuButton = + item.action === "tour" ? ( + startTour("full_platform")} + isActive={false} + > {item.title} - - - ); + + ) : ( + + + + {item.title} + + + ); return ( diff --git a/src/components/experiments/ExperimentForm.tsx b/src/components/experiments/ExperimentForm.tsx index fcf2d6e..838125b 100755 --- a/src/components/experiments/ExperimentForm.tsx +++ b/src/components/experiments/ExperimentForm.tsx @@ -87,33 +87,33 @@ export function ExperimentForm({ mode, experimentId }: ExperimentFormProps) { { label: "Studies", href: "/studies" }, ...(selectedStudyId ? [ - { - label: experiment?.study?.name ?? "Study", - href: `/studies/${selectedStudyId}`, - }, - { label: "Experiments", href: "/experiments" }, - ...(mode === "edit" && experiment - ? [ - { - label: experiment.name, - href: `/studies/${selectedStudyId}/experiments/${experiment.id}`, - }, - { label: "Edit" }, - ] - : [{ label: "New Experiment" }]), - ] + { + label: experiment?.study?.name ?? "Study", + href: `/studies/${selectedStudyId}`, + }, + { label: "Experiments", href: "/experiments" }, + ...(mode === "edit" && experiment + ? [ + { + label: experiment.name, + href: `/studies/${selectedStudyId}/experiments/${experiment.id}`, + }, + { label: "Edit" }, + ] + : [{ label: "New Experiment" }]), + ] : [ - { label: "Experiments", href: "/experiments" }, - ...(mode === "edit" && experiment - ? [ - { - label: experiment.name, - href: `/studies/${experiment.studyId}/experiments/${experiment.id}`, - }, - { label: "Edit" }, - ] - : [{ label: "New Experiment" }]), - ]), + { label: "Experiments", href: "/experiments" }, + ...(mode === "edit" && experiment + ? [ + { + label: experiment.name, + href: `/studies/${experiment.studyId}/experiments/${experiment.id}`, + }, + { label: "Edit" }, + ] + : [{ label: "New Experiment" }]), + ]), ]; useBreadcrumbsEffect(breadcrumbs); @@ -153,14 +153,18 @@ export function ExperimentForm({ mode, experimentId }: ExperimentFormProps) { ...data, estimatedDuration: data.estimatedDuration ?? undefined, }); - router.push(`/studies/${data.studyId}/experiments/${newExperiment.id}/designer`); + router.push( + `/studies/${data.studyId}/experiments/${newExperiment.id}/designer`, + ); } else { const updatedExperiment = await updateExperimentMutation.mutateAsync({ id: experimentId!, ...data, estimatedDuration: data.estimatedDuration ?? undefined, }); - router.push(`/studies/${experiment?.studyId ?? data.studyId}/experiments/${updatedExperiment.id}`); + router.push( + `/studies/${experiment?.studyId ?? data.studyId}/experiments/${updatedExperiment.id}`, + ); } } catch (error) { setError( diff --git a/src/components/experiments/ExperimentsGrid.tsx b/src/components/experiments/ExperimentsGrid.tsx index 90154fc..950fa6b 100755 --- a/src/components/experiments/ExperimentsGrid.tsx +++ b/src/components/experiments/ExperimentsGrid.tsx @@ -1,7 +1,17 @@ "use client"; import { formatDistanceToNow } from "date-fns"; -import { Calendar, FlaskConical, Plus, Settings, Users } from "lucide-react"; +import { + Calendar, + FlaskConical, + Plus, + Settings, + Users, + FileEdit, + TestTube, + CheckCircle2, + Trash2, +} from "lucide-react"; import Link from "next/link"; import { Badge } from "~/components/ui/badge"; @@ -45,22 +55,22 @@ const statusConfig = { draft: { label: "Draft", className: "bg-gray-100 text-gray-800 hover:bg-gray-200", - icon: "📝", + icon: FileEdit, }, testing: { label: "Testing", className: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200", - icon: "🧪", + icon: TestTube, }, ready: { label: "Ready", className: "bg-green-100 text-green-800 hover:bg-green-200", - icon: "✅", + icon: CheckCircle2, }, deprecated: { label: "Deprecated", className: "bg-red-100 text-red-800 hover:bg-red-200", - icon: "🗑️", + icon: Trash2, }, }; @@ -98,7 +108,7 @@ function ExperimentCard({ experiment }: ExperimentCardProps) {
- {statusInfo.icon} + {statusInfo.label}
@@ -158,10 +168,16 @@ function ExperimentCard({ experiment }: ExperimentCardProps) { {/* Actions */}
-
+
{leftPanel}
)} {/* Center Panel (Workspace) */} -
-
+
+
{leftCollapsed && ( {rightCollapsed && (
)}
-
+
{centerPanel}
@@ -1273,11 +1336,13 @@ export function DesignerRoot({ {/* Right Panel (Inspector) */} {!rightCollapsed && ( -
-
+
+
Inspector
-
+
{rightPanel}
@@ -1298,35 +1363,38 @@ export function DesignerRoot({ {dragOverlayAction ? ( // Library Item Drag -
+
{dragOverlayAction.name}
- ) : activeSortableItem?.type === 'action' ? ( + ) : activeSortableItem?.type === "action" ? ( // Existing Action Sort -
+
{ }} - onDeleteAction={() => { }} + onSelectAction={() => {}} + onDeleteAction={() => {}} dragHandle={true} />
- ) : activeSortableItem?.type === 'step' ? ( + ) : activeSortableItem?.type === "step" ? ( // Existing Step Sort -
- +
+
) : null} diff --git a/src/components/experiments/designer/PropertiesPanel.tsx b/src/components/experiments/designer/PropertiesPanel.tsx index 8141af9..8c49122 100755 --- a/src/components/experiments/designer/PropertiesPanel.tsx +++ b/src/components/experiments/designer/PropertiesPanel.tsx @@ -173,8 +173,8 @@ export function PropertiesPanelBase({ let def = registry.getAction(selectedAction.type); // Fallback: If action not found in registry, try without plugin prefix - if (!def && selectedAction.type.includes('.')) { - const baseType = selectedAction.type.split('.').pop(); + if (!def && selectedAction.type.includes(".")) { + const baseType = selectedAction.type.split(".").pop(); if (baseType) { def = registry.getAction(baseType); } @@ -187,9 +187,9 @@ export function PropertiesPanelBase({ type: selectedAction.type, name: selectedAction.name, description: `Action type: ${selectedAction.type}`, - category: selectedAction.category || 'control', - icon: 'Zap', - color: '#6366f1', + category: selectedAction.category || "control", + icon: "Zap", + color: "#6366f1", parameters: [], source: selectedAction.source, }; @@ -225,12 +225,15 @@ export function PropertiesPanelBase({ const ResolvedIcon: React.ComponentType<{ className?: string }> = def?.icon && iconComponents[def.icon] ? (iconComponents[def.icon] as React.ComponentType<{ - className?: string; - }>) + className?: string; + }>) : Zap; return ( -
+
{/* Header / Metadata */}
@@ -305,17 +308,23 @@ export function PropertiesPanelBase({ {/* Branching Configuration (Special Case) */} {selectedAction.type === "branch" ? (
-
+
Branch Options
- {(((containingStep.trigger.conditions as any).options as any[]) || []).map((opt: any, idx: number) => ( -
+ {( + ((containingStep.trigger.conditions as any).options as any[]) || + [] + ).map((opt: any, idx: number) => ( +
{ - const currentOptions = ((containingStep.trigger.conditions as any).options as any[]) || []; + const currentOptions = + ((containingStep.trigger.conditions as any) + .options as any[]) || []; const newOpts = [...currentOptions]; - newOpts[idx] = { ...newOpts[idx], label: e.target.value }; + newOpts[idx] = { + ...newOpts[idx], + label: e.target.value, + }; onStepUpdate(containingStep.id, { trigger: { ...containingStep.trigger, - conditions: { ...containingStep.trigger.conditions, options: newOpts } - } + conditions: { + ...containingStep.trigger.conditions, + options: newOpts, + }, + }, }); onActionUpdate(containingStep.id, selectedAction.id, { - parameters: { ...selectedAction.parameters, options: newOpts } + parameters: { + ...selectedAction.parameters, + options: newOpts, + }, }); }} className="h-7 text-xs" @@ -370,34 +396,53 @@ export function PropertiesPanelBase({
{design.steps.length <= 1 ? ( -
+
No linkable steps
) : ( { - const currentOptions = ((containingStep.trigger.conditions as any).options as any[]) || []; + const currentOptions = + ((containingStep.trigger.conditions as any) + .options as any[]) || []; const newOpts = [...currentOptions]; newOpts[idx] = { ...newOpts[idx], variant: val }; onStepUpdate(containingStep.id, { trigger: { ...containingStep.trigger, - conditions: { ...containingStep.trigger.conditions, options: newOpts } - } + conditions: { + ...containingStep.trigger.conditions, + options: newOpts, + }, + }, }); onActionUpdate(containingStep.id, selectedAction.id, { - parameters: { ...selectedAction.parameters, options: newOpts } + parameters: { + ...selectedAction.parameters, + options: newOpts, + }, }); }} > @@ -430,7 +483,9 @@ export function PropertiesPanelBase({ Default (Next) - Destructive (Red) + + Destructive (Red) + Outline @@ -438,20 +493,28 @@ export function PropertiesPanelBase({
))} - {(!(((containingStep.trigger.conditions as any).options as any[])?.length)) && ( -
- No options defined.
Click + to add a branch. + {!((containingStep.trigger.conditions as any).options as any[]) + ?.length && ( +
+ No options defined. +
+ Click + to add a branch.
)}
@@ -478,7 +544,7 @@ export function PropertiesPanelBase({ {/* Iterations */}
-
+
- + {Number(selectedAction.parameters.iterations || 1)}
- ) : ( - /* Standard Parameters */ - def?.parameters.length ? ( + ) : /* Standard Parameters */ + def?.parameters.length ? ( +
+
+ Parameters +
-
- Parameters -
-
- {def.parameters.map((param) => ( - { - onActionUpdate(containingStep.id, selectedAction.id, { - parameters: { - ...selectedAction.parameters, - [param.id]: val, - }, - }); - }} - onCommit={() => { }} - /> - ))} -
+ {def.parameters.map((param) => ( + { + onActionUpdate(containingStep.id, selectedAction.id, { + parameters: { + ...selectedAction.parameters, + [param.id]: val, + }, + }); + }} + onCommit={() => {}} + /> + ))}
- ) : ( -
- No parameters for this action. -
- ) +
+ ) : ( +
+ No parameters for this action. +
)}
); @@ -539,7 +603,10 @@ export function PropertiesPanelBase({ /* --------------------------- Step Properties View --------------------------- */ if (selectedStep) { return ( -
+

- Steps always execute sequentially. Use control flow actions for parallel/conditional logic. + Steps always execute sequentially. Use control flow actions + for parallel/conditional logic.

@@ -697,7 +765,7 @@ const ParameterEditor = React.memo(function ParameterEditor({ param, value: rawValue, onUpdate, - onCommit + onCommit, }: ParameterEditorProps) { // Local state for immediate feedback const [localValue, setLocalValue] = useState(rawValue); @@ -708,19 +776,22 @@ const ParameterEditor = React.memo(function ParameterEditor({ setLocalValue(rawValue); }, [rawValue]); - const handleUpdate = useCallback((newVal: unknown, immediate = false) => { - setLocalValue(newVal); + const handleUpdate = useCallback( + (newVal: unknown, immediate = false) => { + setLocalValue(newVal); - if (debounceRef.current) clearTimeout(debounceRef.current); + if (debounceRef.current) clearTimeout(debounceRef.current); - if (immediate) { - onUpdate(newVal); - } else { - debounceRef.current = setTimeout(() => { + if (immediate) { onUpdate(newVal); - }, 300); - } - }, [onUpdate]); + } else { + debounceRef.current = setTimeout(() => { + onUpdate(newVal); + }, 300); + } + }, + [onUpdate], + ); const handleCommit = useCallback(() => { if (localValue !== rawValue) { @@ -772,13 +843,22 @@ const ParameterEditor = React.memo(function ParameterEditor({
); } else if (param.type === "number") { - const numericVal = typeof localValue === "number" ? localValue : (param.min ?? 0); + const numericVal = + typeof localValue === "number" ? localValue : (param.min ?? 0); if (param.min !== undefined || param.max !== undefined) { const min = param.min ?? 0; - const max = param.max ?? Math.max(min + 1, Number.isFinite(numericVal) ? numericVal : min + 1); + const max = + param.max ?? + Math.max(min + 1, Number.isFinite(numericVal) ? numericVal : min + 1); const range = max - min; - const step = param.step ?? (range <= 5 ? 0.1 : range <= 50 ? 0.5 : Math.max(1, Math.round(range / 100))); + const step = + param.step ?? + (range <= 5 + ? 0.1 + : range <= 50 + ? 0.5 + : Math.max(1, Math.round(range / 100))); control = (
@@ -792,7 +872,9 @@ const ParameterEditor = React.memo(function ParameterEditor({ onPointerUp={() => handleUpdate(localValue)} // Commit on release /> - {step < 1 ? Number(numericVal).toFixed(2) : Number(numericVal).toString()} + {step < 1 + ? Number(numericVal).toFixed(2) + : Number(numericVal).toString()}
diff --git a/src/components/experiments/designer/SettingsModal.tsx b/src/components/experiments/designer/SettingsModal.tsx index 92a0f2c..d5e4b1f 100644 --- a/src/components/experiments/designer/SettingsModal.tsx +++ b/src/components/experiments/designer/SettingsModal.tsx @@ -2,52 +2,52 @@ import { SettingsTab } from "./tabs/SettingsTab"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, } from "~/components/ui/dialog"; interface SettingsModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - experiment: { - id: string; - name: string; - description: string | null; - status: string; - studyId: string; - createdAt: Date; - updatedAt: Date; - study: { - id: string; - name: string; - }; - }; - designStats?: { - stepCount: number; - actionCount: number; + open: boolean; + onOpenChange: (open: boolean) => void; + experiment: { + id: string; + name: string; + description: string | null; + status: string; + studyId: string; + createdAt: Date; + updatedAt: Date; + study: { + id: string; + name: string; }; + }; + designStats?: { + stepCount: number; + actionCount: number; + }; } export function SettingsModal({ - open, - onOpenChange, - experiment, - designStats, + open, + onOpenChange, + experiment, + designStats, }: SettingsModalProps) { - return ( - - - - Experiment Settings - - Configure experiment metadata and status - - - - - - ); + return ( + + + + Experiment Settings + + Configure experiment metadata and status + + + + + + ); } diff --git a/src/components/experiments/designer/ValidationPanel.tsx b/src/components/experiments/designer/ValidationPanel.tsx index 288f123..881f106 100755 --- a/src/components/experiments/designer/ValidationPanel.tsx +++ b/src/components/experiments/designer/ValidationPanel.tsx @@ -106,8 +106,6 @@ function flattenIssues(issuesMap: Record) { return flattened; } - - /* -------------------------------------------------------------------------- */ /* Issue Item Component */ /* -------------------------------------------------------------------------- */ @@ -145,7 +143,7 @@ function IssueItem({
-

+

{issue.message}

@@ -248,8 +246,6 @@ export function ValidationPanel({ console.log("[ValidationPanel] issues", issues, { flatIssues, counts }); }, [issues, flatIssues, counts]); - - return (
setSeverityFilter("error")} aria-pressed={severityFilter === "error"} @@ -305,7 +301,7 @@ export function ValidationPanel({ className={cn( "h-7 justify-start gap-1 text-[11px]", severityFilter === "warning" && - "bg-amber-500 text-white hover:opacity-90", + "bg-amber-500 text-white hover:opacity-90", )} onClick={() => setSeverityFilter("warning")} aria-pressed={severityFilter === "warning"} @@ -321,7 +317,7 @@ export function ValidationPanel({ className={cn( "h-7 justify-start gap-1 text-[11px]", severityFilter === "info" && - "bg-blue-600 text-white hover:opacity-90", + "bg-blue-600 text-white hover:opacity-90", )} onClick={() => setSeverityFilter("info")} aria-pressed={severityFilter === "info"} diff --git a/src/components/experiments/designer/flow/ActionChip.tsx b/src/components/experiments/designer/flow/ActionChip.tsx index dcf6a1b..07f5119 100644 --- a/src/components/experiments/designer/flow/ActionChip.tsx +++ b/src/components/experiments/designer/flow/ActionChip.tsx @@ -5,16 +5,16 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useDroppable } from "@dnd-kit/core"; import { - ChevronRight, - Trash2, - Clock, - GitBranch, - Repeat, - Layers, - List, - AlertCircle, - Play, - HelpCircle + ChevronRight, + Trash2, + Clock, + GitBranch, + Repeat, + Layers, + List, + AlertCircle, + Play, + HelpCircle, } from "lucide-react"; import { cn } from "~/lib/utils"; import { type ExperimentAction } from "~/lib/experiment-designer/types"; @@ -24,480 +24,530 @@ import { Badge } from "~/components/ui/badge"; import { useDesignerStore } from "../state/store"; export interface ActionChipProps { - stepId: string; - action: ExperimentAction; - parentId: string | null; - selectedActionId: string | null | undefined; - onSelectAction: (stepId: string, actionId: string | undefined) => void; - onDeleteAction: (stepId: string, actionId: string) => void; - onReorderAction?: (stepId: string, actionId: string, direction: 'up' | 'down') => void; - dragHandle?: boolean; - isFirst?: boolean; - isLast?: boolean; + stepId: string; + action: ExperimentAction; + parentId: string | null; + selectedActionId: string | null | undefined; + onSelectAction: (stepId: string, actionId: string | undefined) => void; + onDeleteAction: (stepId: string, actionId: string) => void; + onReorderAction?: ( + stepId: string, + actionId: string, + direction: "up" | "down", + ) => void; + dragHandle?: boolean; + isFirst?: boolean; + isLast?: boolean; } export interface ActionChipVisualsProps { - action: ExperimentAction; - isSelected?: boolean; - isDragging?: boolean; - isOverNested?: boolean; - onSelect?: (e: React.MouseEvent) => void; - onDelete?: (e: React.MouseEvent) => void; - onReorder?: (direction: 'up' | 'down') => void; - dragHandleProps?: React.HTMLAttributes; - children?: React.ReactNode; - isFirst?: boolean; - isLast?: boolean; - validationStatus?: "error" | "warning" | "info"; + action: ExperimentAction; + isSelected?: boolean; + isDragging?: boolean; + isOverNested?: boolean; + onSelect?: (e: React.MouseEvent) => void; + onDelete?: (e: React.MouseEvent) => void; + onReorder?: (direction: "up" | "down") => void; + dragHandleProps?: React.HTMLAttributes; + children?: React.ReactNode; + isFirst?: boolean; + isLast?: boolean; + validationStatus?: "error" | "warning" | "info"; } /** * Helper to determine visual style based on action type/category */ function getActionVisualStyle(action: ExperimentAction) { - const def = actionRegistry.getAction(action.type); - const category = def?.category || "other"; + const def = actionRegistry.getAction(action.type); + const category = def?.category || "other"; - // Specific Control Types - if (action.type === "hristudio-core.wait" || action.type === "wait") { - return { - variant: "wait", - icon: Clock, - bg: "bg-amber-500/10 hover:bg-amber-500/20", - border: "border-amber-200 dark:border-amber-800", - text: "text-amber-700 dark:text-amber-400", - accent: "bg-amber-500", - }; - } - - if (action.type === "hristudio-core.branch" || action.type === "branch") { - return { - variant: "branch", - icon: GitBranch, - bg: "bg-orange-500/10 hover:bg-orange-500/20", - border: "border-orange-200 dark:border-orange-800", - text: "text-orange-700 dark:text-orange-400", - accent: "bg-orange-500", - }; - } - - if (action.type === "hristudio-core.loop" || action.type === "loop") { - return { - variant: "loop", - icon: Repeat, - bg: "bg-purple-500/10 hover:bg-purple-500/20", - border: "border-purple-200 dark:border-purple-800", - text: "text-purple-700 dark:text-purple-400", - accent: "bg-purple-500", - }; - } - - if (action.type === "hristudio-core.parallel" || action.type === "parallel") { - return { - variant: "parallel", - icon: Layers, - bg: "bg-emerald-500/10 hover:bg-emerald-500/20", - border: "border-emerald-200 dark:border-emerald-800", - text: "text-emerald-700 dark:text-emerald-400", - accent: "bg-emerald-500", - }; - } - - - - // General Categories - if (category === "wizard") { - return { - variant: "wizard", - icon: HelpCircle, - bg: "bg-indigo-500/5 hover:bg-indigo-500/10", - border: "border-indigo-200 dark:border-indigo-800", - text: "text-indigo-700 dark:text-indigo-300", - accent: "bg-indigo-500", - }; - } - - if ((category as string) === "robot" || (category as string) === "movement" || (category as string) === "speech") { - return { - variant: "robot", - icon: Play, // Or specific robot icon if available - bg: "bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700", - border: "border-slate-200 dark:border-slate-700", - text: "text-slate-700 dark:text-slate-300", - accent: "bg-slate-500", - } - } - - // Default + // Specific Control Types + if (action.type === "hristudio-core.wait" || action.type === "wait") { return { - variant: "default", - icon: undefined, - bg: "bg-muted/40 hover:bg-accent/40", - border: "border-border", - text: "text-foreground", - accent: "bg-muted-foreground", + variant: "wait", + icon: Clock, + bg: "bg-amber-500/10 hover:bg-amber-500/20", + border: "border-amber-200 dark:border-amber-800", + text: "text-amber-700 dark:text-amber-400", + accent: "bg-amber-500", }; + } + + if (action.type === "hristudio-core.branch" || action.type === "branch") { + return { + variant: "branch", + icon: GitBranch, + bg: "bg-orange-500/10 hover:bg-orange-500/20", + border: "border-orange-200 dark:border-orange-800", + text: "text-orange-700 dark:text-orange-400", + accent: "bg-orange-500", + }; + } + + if (action.type === "hristudio-core.loop" || action.type === "loop") { + return { + variant: "loop", + icon: Repeat, + bg: "bg-purple-500/10 hover:bg-purple-500/20", + border: "border-purple-200 dark:border-purple-800", + text: "text-purple-700 dark:text-purple-400", + accent: "bg-purple-500", + }; + } + + if (action.type === "hristudio-core.parallel" || action.type === "parallel") { + return { + variant: "parallel", + icon: Layers, + bg: "bg-emerald-500/10 hover:bg-emerald-500/20", + border: "border-emerald-200 dark:border-emerald-800", + text: "text-emerald-700 dark:text-emerald-400", + accent: "bg-emerald-500", + }; + } + + // General Categories + if (category === "wizard") { + return { + variant: "wizard", + icon: HelpCircle, + bg: "bg-indigo-500/5 hover:bg-indigo-500/10", + border: "border-indigo-200 dark:border-indigo-800", + text: "text-indigo-700 dark:text-indigo-300", + accent: "bg-indigo-500", + }; + } + + if ( + (category as string) === "robot" || + (category as string) === "movement" || + (category as string) === "speech" + ) { + return { + variant: "robot", + icon: Play, // Or specific robot icon if available + bg: "bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700", + border: "border-slate-200 dark:border-slate-700", + text: "text-slate-700 dark:text-slate-300", + accent: "bg-slate-500", + }; + } + + // Default + return { + variant: "default", + icon: undefined, + bg: "bg-muted/40 hover:bg-accent/40", + border: "border-border", + text: "text-foreground", + accent: "bg-muted-foreground", + }; } - export function ActionChipVisuals({ - action, - isSelected, - isDragging, - isOverNested, - onSelect, - onDelete, - onReorder, - dragHandleProps, - children, - isFirst, - isLast, - validationStatus, + action, + isSelected, + isDragging, + isOverNested, + onSelect, + onDelete, + onReorder, + dragHandleProps, + children, + isFirst, + isLast, + validationStatus, }: ActionChipVisualsProps) { - const def = actionRegistry.getAction(action.type); - const style = getActionVisualStyle(action); - const Icon = style.icon; + const def = actionRegistry.getAction(action.type); + const style = getActionVisualStyle(action); + const Icon = style.icon; - return ( + return ( +
+ {/* Accent Bar logic for control flow */} + {style.variant !== "default" && style.variant !== "robot" && (
+ )} + +
+
+ {Icon && ( + + )} + + {action.name} + + + {/* Inline Info for Control Actions */} + {style.variant === "wait" && !!action.parameters.duration && ( + + {String(action.parameters.duration ?? "")}s + + )} + {style.variant === "loop" && ( + + {String(action.parameters.iterations || 1)}x + + )} + {style.variant === "loop" && + action.parameters.requireApproval !== false && ( + + + Ask + + )} + + {validationStatus === "error" && ( +
+ )} + {validationStatus === "warning" && ( +
+ )} +
+ +
+ + +
+ + +
-
-
- {Icon && } - - {action.name} - + {/* Description / Subtext */} + {def?.description && ( +
+ {def.description} +
+ )} - {/* Inline Info for Control Actions */} - {style.variant === "wait" && !!action.parameters.duration && ( - - {String(action.parameters.duration ?? "")}s - - )} - {style.variant === "loop" && ( - - {String(action.parameters.iterations || 1)}x - - )} - {style.variant === "loop" && action.parameters.requireApproval !== false && ( - - - Ask - - )} + {/* Tags for parameters (hide for specialized control blocks that show inline) */} + {def?.parameters?.length && + (style.variant === "default" || style.variant === "robot") ? ( +
+ {def.parameters.slice(0, 3).map((p) => ( + + {p.name} + + ))} + {def.parameters.length > 3 && ( + + +{def.parameters.length - 3} + + )} +
+ ) : null} - {validationStatus === "error" && ( -
- )} - {validationStatus === "warning" && ( -
- )} -
- -
- - -
- - -
- - {/* Description / Subtext */} - { - def?.description && ( -
- {def.description} -
- ) - } - - {/* Tags for parameters (hide for specialized control blocks that show inline) */} - { - def?.parameters?.length && (style.variant === 'default' || style.variant === 'robot') ? ( -
- {def.parameters.slice(0, 3).map((p) => ( - - {p.name} - - ))} - {def.parameters.length > 3 && ( - +{def.parameters.length - 3} - )} -
- ) : null - } - - {children} -
- ); + {children} +
+ ); } export function SortableActionChip({ - stepId, - action, - parentId, - selectedActionId, - onSelectAction, - onDeleteAction, - onReorderAction, - dragHandle, - isFirst, - isLast, + stepId, + action, + parentId, + selectedActionId, + onSelectAction, + onDeleteAction, + onReorderAction, + dragHandle, + isFirst, + isLast, }: ActionChipProps) { - const isSelected = selectedActionId === action.id; + const isSelected = selectedActionId === action.id; - const insertionProjection = useDesignerStore((s) => s.insertionProjection); - const steps = useDesignerStore((s) => s.steps); - const currentStep = steps.find((s) => s.id === stepId); + const insertionProjection = useDesignerStore((s) => s.insertionProjection); + const steps = useDesignerStore((s) => s.steps); + const currentStep = steps.find((s) => s.id === stepId); - // Branch Options Visualization - const branchOptions = useMemo(() => { - if (!action.type.includes("branch") || !currentStep) return null; + // Branch Options Visualization + const branchOptions = useMemo(() => { + if (!action.type.includes("branch") || !currentStep) return null; - const options = (currentStep.trigger as any)?.conditions?.options; - if (!options?.length && !(currentStep.trigger as any)?.conditions?.nextStepId) { - return ( -
- No branches configured. Add options in properties. -
- ); - } - - // Combine explicit options and unconditional nextStepId - // The original FlowWorkspace logic iterated options. logic there: - // (step.trigger.conditions as any).options.map... - - return ( -
- {options?.map((opt: any, idx: number) => { - // Resolve ID to name for display - let targetName = "Unlinked"; - let targetIndex = -1; - - if (opt.nextStepId) { - const target = steps.find(s => s.id === opt.nextStepId); - if (target) { - targetName = target.name; - targetIndex = target.order; - } - } else if (typeof opt.nextStepIndex === 'number') { - targetIndex = opt.nextStepIndex; - targetName = `Step #${targetIndex + 1}`; - } - - return ( -
-
- - {opt.label} - - -
- -
- - {targetName} - - {targetIndex !== -1 && ( - - #{targetIndex + 1} - - )} -
-
- ); - })} - - {/* Visual indicator for unconditional jump if present and no options matched (though usually logic handles this) */} - {/* For now keeping parity with FlowWorkspace which only showed options */} -
- ); - }, [action.type, currentStep, steps]); - - const displayChildren = useMemo(() => { - if ( - insertionProjection?.stepId === stepId && - insertionProjection.parentId === action.id - ) { - const copy = [...(action.children || [])]; - copy.splice(insertionProjection.index, 0, insertionProjection.action); - return copy; - } - return action.children || []; - }, [action.children, action.id, stepId, insertionProjection]); - - /* ------------------------------------------------------------------------ */ - /* Main Sortable Logic */ - /* ------------------------------------------------------------------------ */ - const isPlaceholder = action.id === "projection-placeholder"; - - // Compute validation status - const issues = useDesignerStore((s) => s.validationIssues[action.id]); - const validationStatus = useMemo(() => { - if (!issues?.length) return undefined; - if (issues.some((i) => i.severity === "error")) return "error"; - if (issues.some((i) => i.severity === "warning")) return "warning"; - return "info"; - }, [issues]); - - /* ------------------------------------------------------------------------ */ - /* Sortable (Local) DnD Monitoring */ - /* ------------------------------------------------------------------------ */ - // useSortable disabled per user request to remove action drag-and-drop - // const { ... } = useSortable(...) - - // Use local dragging state or passed prop - const isDragging = dragHandle || false; - - /* ------------------------------------------------------------------------ */ - /* Nested Droppable (for control flow containers) */ - /* ------------------------------------------------------------------------ */ - const def = actionRegistry.getAction(action.type); - const nestedDroppableId = `container-${action.id}`; - const { - isOver: isOverNested, - setNodeRef: setNestedNodeRef - } = useDroppable({ - id: nestedDroppableId, - disabled: !def?.nestable || isPlaceholder, // Disable droppable for placeholder - data: { - type: "container", - stepId, - parentId: action.id, - action // Pass full action for projection logic - } - }); - - const shouldRenderChildren = !!def?.nestable; - - if (isPlaceholder) { - return ( -
-
- - {action.name} - -
-
- ); + const options = (currentStep.trigger as any)?.conditions?.options; + if ( + !options?.length && + !(currentStep.trigger as any)?.conditions?.nextStepId + ) { + return ( +
+ No branches configured. Add options in properties. +
+ ); } - return ( - { - e.stopPropagation(); - onSelectAction(stepId, action.id); - }} - onDelete={(e) => { - e.stopPropagation(); - onDeleteAction(stepId, action.id); - }} - onReorder={(direction) => onReorderAction?.(stepId, action.id, direction)} - isFirst={isFirst} - isLast={isLast} - validationStatus={validationStatus} - > - {/* Branch Options Visualization */} - {branchOptions} + // Combine explicit options and unconditional nextStepId + // The original FlowWorkspace logic iterated options. logic there: + // (step.trigger.conditions as any).options.map... - {/* Nested Children Rendering (e.g. for Loops/Parallel) */} - {shouldRenderChildren && ( -
+ {options?.map((opt: any, idx: number) => { + // Resolve ID to name for display + let targetName = "Unlinked"; + let targetIndex = -1; + + if (opt.nextStepId) { + const target = steps.find((s) => s.id === opt.nextStepId); + if (target) { + targetName = target.name; + targetIndex = target.order; + } + } else if (typeof opt.nextStepIndex === "number") { + targetIndex = opt.nextStepIndex; + targetName = `Step #${targetIndex + 1}`; + } + + return ( +
+
+ - {displayChildren?.length === 0 ? ( -
- Empty container -
- ) : ( - displayChildren?.map((child, idx) => ( - - )) - )} -
- )} - + {opt.label} + + +
+ +
+ + {targetName} + + {targetIndex !== -1 && ( + + #{targetIndex + 1} + + )} +
+
+ ); + })} + + {/* Visual indicator for unconditional jump if present and no options matched (though usually logic handles this) */} + {/* For now keeping parity with FlowWorkspace which only showed options */} +
); + }, [action.type, currentStep, steps]); + + const displayChildren = useMemo(() => { + if ( + insertionProjection?.stepId === stepId && + insertionProjection.parentId === action.id + ) { + const copy = [...(action.children || [])]; + copy.splice(insertionProjection.index, 0, insertionProjection.action); + return copy; + } + return action.children || []; + }, [action.children, action.id, stepId, insertionProjection]); + + /* ------------------------------------------------------------------------ */ + /* Main Sortable Logic */ + /* ------------------------------------------------------------------------ */ + const isPlaceholder = action.id === "projection-placeholder"; + + // Compute validation status + const issues = useDesignerStore((s) => s.validationIssues[action.id]); + const validationStatus = useMemo(() => { + if (!issues?.length) return undefined; + if (issues.some((i) => i.severity === "error")) return "error"; + if (issues.some((i) => i.severity === "warning")) return "warning"; + return "info"; + }, [issues]); + + /* ------------------------------------------------------------------------ */ + /* Sortable (Local) DnD Monitoring */ + /* ------------------------------------------------------------------------ */ + // useSortable disabled per user request to remove action drag-and-drop + // const { ... } = useSortable(...) + + // Use local dragging state or passed prop + const isDragging = dragHandle || false; + + /* ------------------------------------------------------------------------ */ + /* Nested Droppable (for control flow containers) */ + /* ------------------------------------------------------------------------ */ + const def = actionRegistry.getAction(action.type); + const nestedDroppableId = `container-${action.id}`; + const { isOver: isOverNested, setNodeRef: setNestedNodeRef } = useDroppable({ + id: nestedDroppableId, + disabled: !def?.nestable || isPlaceholder, // Disable droppable for placeholder + data: { + type: "container", + stepId, + parentId: action.id, + action, // Pass full action for projection logic + }, + }); + + const shouldRenderChildren = !!def?.nestable; + + if (isPlaceholder) { + return ( +
+
+ + {action.name} + +
+
+ ); + } + + return ( + { + e.stopPropagation(); + onSelectAction(stepId, action.id); + }} + onDelete={(e) => { + e.stopPropagation(); + onDeleteAction(stepId, action.id); + }} + onReorder={(direction) => onReorderAction?.(stepId, action.id, direction)} + isFirst={isFirst} + isLast={isLast} + validationStatus={validationStatus} + > + {/* Branch Options Visualization */} + {branchOptions} + + {/* Nested Children Rendering (e.g. for Loops/Parallel) */} + {shouldRenderChildren && ( +
+ {displayChildren?.length === 0 ? ( +
+ Empty container +
+ ) : ( + displayChildren?.map((child, idx) => ( + + )) + )} +
+ )} +
+ ); } diff --git a/src/components/experiments/designer/flow/FlowWorkspace.tsx b/src/components/experiments/designer/flow/FlowWorkspace.tsx index 409a3fa..e6f2a01 100755 --- a/src/components/experiments/designer/flow/FlowWorkspace.tsx +++ b/src/components/experiments/designer/flow/FlowWorkspace.tsx @@ -97,8 +97,12 @@ interface StepRowProps { onDeleteAction: (stepId: string, actionId: string) => void; setRenamingStepId: (id: string | null) => void; registerMeasureRef: (stepId: string, el: HTMLDivElement | null) => void; - onReorderStep: (stepId: string, direction: 'up' | 'down') => void; - onReorderAction?: (stepId: string, actionId: string, direction: 'up' | 'down') => void; + onReorderStep: (stepId: string, direction: "up" | "down") => void; + onReorderAction?: ( + stepId: string, + actionId: string, + direction: "up" | "down", + ) => void; isChild?: boolean; } @@ -157,12 +161,12 @@ function StepRow({ ref={(el) => registerMeasureRef(step.id, el)} className={cn( "relative px-3 py-4 transition-all duration-300", - isChild && "ml-8 pl-0" + isChild && "ml-8 pl-0", )} data-step-id={step.id} > {isChild && ( -
+
)} @@ -172,7 +176,7 @@ function StepRow({ "mb-2 rounded-lg border shadow-sm transition-colors", selectedStepId === step.id ? "border-border bg-accent/30" - : "hover:bg-accent/30" + : "hover:bg-accent/30", )} >
{ e.stopPropagation(); - onReorderStep(step.id, 'up'); + onReorderStep(step.id, "up"); }} disabled={item.index === 0} aria-label="Move step up" @@ -281,58 +285,69 @@ function StepRow({ -
- - {/* Conditional Branching Visualization */} - {/* Loop Visualization */} {step.type === "loop" && ( -
-
+
+
Loop Logic
-
+
Repeat: - {(step.trigger.conditions as any).loop?.iterations || 1} times + {(step.trigger.conditions as any).loop?.iterations || 1}{" "} + times
Approval: - - {(step.trigger.conditions as any).loop?.requireApproval !== false ? "Required" : "Auto-proceed"} + + {(step.trigger.conditions as any).loop?.requireApproval !== + false + ? "Required" + : "Auto-proceed"}
)} - - {/* Action List (Collapsible/Virtual content) */} {step.expanded && (
@@ -342,7 +357,7 @@ function StepRow({ >
{displayActions.length === 0 ? ( -
+
Drop actions here
) : ( @@ -367,7 +382,7 @@ function StepRow({ )}
-
+
); } @@ -375,15 +390,21 @@ function StepRow({ /* Step Card Preview (for DragOverlay) */ /* -------------------------------------------------------------------------- */ -export function StepCardPreview({ step, dragHandle }: { step: ExperimentStep; dragHandle?: boolean }) { +export function StepCardPreview({ + step, + dragHandle, +}: { + step: ExperimentStep; + dragHandle?: boolean; +}) { return (
-
+
@@ -401,13 +422,13 @@ export function StepCardPreview({ step, dragHandle }: { step: ExperimentStep; dr {step.actions.length} actions
-
+
{/* Preview optional: show empty body hint or just the header? Header is usually enough for sorting. */} -
- +
+ {step.actions.length} actions hidden while dragging
@@ -423,8 +444,6 @@ function generateStepId(): string { return `step-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } - - function sortableStepId(stepId: string) { return `s-step-${stepId}`; } @@ -447,7 +466,7 @@ function StepDroppableArea({ stepId }: { stepId: string }) { const { isOver, setNodeRef } = useDroppable({ id: `step-${stepId}`, - disabled: isStepDragging + disabled: isStepDragging, }); if (isStepDragging) return null; @@ -459,14 +478,12 @@ function StepDroppableArea({ stepId }: { stepId: string }) { className={cn( "pointer-events-none absolute inset-0 rounded-md transition-colors", isOver && - "bg-blue-50/40 ring-2 ring-blue-400/60 ring-offset-0 dark:bg-blue-950/20", + "bg-blue-50/40 ring-2 ring-blue-400/60 ring-offset-0 dark:bg-blue-950/20", )} /> ); } - - /* -------------------------------------------------------------------------- */ /* FlowWorkspace Component */ /* -------------------------------------------------------------------------- */ @@ -520,7 +537,10 @@ export function FlowWorkspace({ const childStepIds = useMemo(() => { const children = new Set(); for (const step of steps) { - if (step.type === 'conditional' && (step.trigger.conditions as any)?.options) { + if ( + step.type === "conditional" && + (step.trigger.conditions as any)?.options + ) { for (const opt of (step.trigger.conditions as any).options) { if (opt.nextStepId) { children.add(opt.nextStepId); @@ -695,26 +715,33 @@ export function FlowWorkspace({ ); const handleReorderStep = useCallback( - (stepId: string, direction: 'up' | 'down') => { - console.log('handleReorderStep', stepId, direction); + (stepId: string, direction: "up" | "down") => { + console.log("handleReorderStep", stepId, direction); const currentIndex = steps.findIndex((s) => s.id === stepId); - console.log('currentIndex', currentIndex, 'total', steps.length); + console.log("currentIndex", currentIndex, "total", steps.length); if (currentIndex === -1) return; - const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; - console.log('newIndex', newIndex); + const newIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; + console.log("newIndex", newIndex); if (newIndex < 0 || newIndex >= steps.length) return; reorderStep(currentIndex, newIndex); }, - [steps, reorderStep] + [steps, reorderStep], ); const handleReorderAction = useCallback( - (stepId: string, actionId: string, direction: 'up' | 'down') => { - const step = steps.find(s => s.id === stepId); + (stepId: string, actionId: string, direction: "up" | "down") => { + const step = steps.find((s) => s.id === stepId); if (!step) return; - const findInTree = (list: ExperimentAction[], pId: string | null): { list: ExperimentAction[], parentId: string | null, index: number } | null => { - const idx = list.findIndex(a => a.id === actionId); + const findInTree = ( + list: ExperimentAction[], + pId: string | null, + ): { + list: ExperimentAction[]; + parentId: string | null; + index: number; + } | null => { + const idx = list.findIndex((a) => a.id === actionId); if (idx !== -1) return { list, parentId: pId, index: idx }; for (const a of list) { @@ -730,16 +757,15 @@ export function FlowWorkspace({ if (!context) return; const { parentId, index, list } = context; - const newIndex = direction === 'up' ? index - 1 : index + 1; + const newIndex = direction === "up" ? index - 1 : index + 1; if (newIndex < 0 || newIndex >= list.length) return; moveAction(stepId, actionId, parentId, newIndex); }, - [steps, moveAction] + [steps, moveAction], ); - /* ------------------------------------------------------------------------ */ /* Sortable (Local) DnD Monitoring */ /* ------------------------------------------------------------------------ */ @@ -768,9 +794,11 @@ export function FlowWorkspace({ const overData = over.data.current; if ( - activeData && overData && + activeData && + overData && activeData.stepId === overData.stepId && - activeData.type === 'action' && overData.type === 'action' + activeData.type === "action" && + overData.type === "action" ) { const stepId = activeData.stepId as string; // Fix: SortableActionChip puts 'id' directly on data, not inside 'action' property @@ -809,8 +837,8 @@ export function FlowWorkspace({ if ( activeData && overData && - activeData.type === 'action' && - overData.type === 'action' + activeData.type === "action" && + overData.type === "action" ) { // Fix: Access 'id' directly from data payload const activeActionId = activeData.id; @@ -825,12 +853,17 @@ export function FlowWorkspace({ if (activeParentId !== overParentId || activeStepId !== overStepId) { // Determine new index // verification of safe move handled by store - moveAction(overStepId, activeActionId, overParentId, overData.sortable.index); + moveAction( + overStepId, + activeActionId, + overParentId, + overData.sortable.index, + ); } } } }, - [moveAction] + [moveAction], ); useDndMonitor({ @@ -960,4 +993,3 @@ export function FlowWorkspace({ // Wrap in React.memo to prevent unnecessary re-renders causing flashing export default React.memo(FlowWorkspace); - diff --git a/src/components/experiments/designer/layout/BottomStatusBar.tsx b/src/components/experiments/designer/layout/BottomStatusBar.tsx index f0e53e5..9dd7aa6 100755 --- a/src/components/experiments/designer/layout/BottomStatusBar.tsx +++ b/src/components/experiments/designer/layout/BottomStatusBar.tsx @@ -82,7 +82,7 @@ export function BottomStatusBar({ ); default: return ( -
+
Unvalidated
@@ -102,7 +102,7 @@ export function BottomStatusBar({ const savingIndicator = pendingSave || saving ? ( -
+
Saving...
@@ -117,7 +117,7 @@ export function BottomStatusBar({ )} > {/* Status Indicators */} -
+
{validationBadge} {unsavedBadge} {savingIndicator} diff --git a/src/components/experiments/designer/layout/PanelsContainer.tsx b/src/components/experiments/designer/layout/PanelsContainer.tsx index 098ade6..dff12a0 100755 --- a/src/components/experiments/designer/layout/PanelsContainer.tsx +++ b/src/components/experiments/designer/layout/PanelsContainer.tsx @@ -64,29 +64,30 @@ export interface PanelsContainerProps { * - Resize handles are absolutely positioned over the grid at the left and right boundaries. * - Fractions are clamped with configurable min/max so panels remain usable at all sizes. */ -const Panel: React.FC> = ({ - className: panelCls, - panelClassName, - contentClassName, - children, -}) => ( -
+> = ({ className: panelCls, panelClassName, contentClassName, children }) => ( +
+
-
- {children} -
-
- ); + {children} +
+

+); export function PanelsContainer({ left, @@ -178,7 +179,7 @@ export function PanelsContainer({ minRightPct, maxRightPct, leftCollapsed, - rightCollapsed + rightCollapsed, ], ); @@ -206,7 +207,16 @@ export function PanelsContainer({ setRightPct(nextRight); } }, - [hasLeft, hasRight, minLeftPct, maxLeftPct, minRightPct, maxRightPct, leftCollapsed, rightCollapsed], + [ + hasLeft, + hasRight, + minLeftPct, + maxLeftPct, + minRightPct, + maxRightPct, + leftCollapsed, + rightCollapsed, + ], ); const endDrag = React.useCallback(() => { @@ -270,10 +280,10 @@ export function PanelsContainer({ // We use FR units instead of % to let the browser handle exact pixel fitting without rounding errors causing overflow const styleVars: React.CSSProperties & Record = hasCenter ? { - "--col-left": `${hasLeft ? l : 0}fr`, - "--col-center": `${c}fr`, - "--col-right": `${hasRight ? r : 0}fr`, - } + "--col-left": `${hasLeft ? l : 0}fr`, + "--col-center": `${c}fr`, + "--col-right": `${hasRight ? r : 0}fr`, + } : {}; // Explicit grid template depending on which side panels exist @@ -299,19 +309,17 @@ export function PanelsContainer({ const centerDividers = showDividers && hasCenter ? cn({ - "border-l": hasLeft, - "border-r": hasRight, - }) + "border-l": hasLeft, + "border-r": hasRight, + }) : undefined; - - return ( <> {/* Mobile Layout (Flex + Sheets) */} -
+
{/* Mobile Header/Toolbar for access to panels */} -
+
{hasLeft && ( @@ -321,9 +329,7 @@ export function PanelsContainer({ -
- {left} -
+
{left}
)} @@ -338,16 +344,14 @@ export function PanelsContainer({ -
- {right} -
+
{right}
)}
{/* Main Content (Center) */} -
+
{center}
@@ -357,15 +361,31 @@ export function PanelsContainer({ ref={rootRef} aria-label={ariaLabel} className={cn( - "relative hidden md:grid h-full min-h-0 w-full max-w-full overflow-hidden select-none", + "relative hidden h-full min-h-0 w-full max-w-full overflow-hidden select-none md:grid", // 2-3-2 ratio for left-center-right panels when all visible - hasLeft && hasRight && !leftCollapsed && !rightCollapsed && "grid-cols-[2fr_3fr_2fr]", + hasLeft && + hasRight && + !leftCollapsed && + !rightCollapsed && + "grid-cols-[2fr_3fr_2fr]", // Left collapsed: center + right (3:2 ratio) - hasLeft && hasRight && leftCollapsed && !rightCollapsed && "grid-cols-[3fr_2fr]", + hasLeft && + hasRight && + leftCollapsed && + !rightCollapsed && + "grid-cols-[3fr_2fr]", // Right collapsed: left + center (2:3 ratio) - hasLeft && hasRight && !leftCollapsed && rightCollapsed && "grid-cols-[2fr_3fr]", + hasLeft && + hasRight && + !leftCollapsed && + rightCollapsed && + "grid-cols-[2fr_3fr]", // Both collapsed: center only - hasLeft && hasRight && leftCollapsed && rightCollapsed && "grid-cols-1", + hasLeft && + hasRight && + leftCollapsed && + rightCollapsed && + "grid-cols-1", // Only left and center hasLeft && !hasRight && !leftCollapsed && "grid-cols-[2fr_3fr]", hasLeft && !hasRight && leftCollapsed && "grid-cols-1", @@ -409,7 +429,7 @@ export function PanelsContainer({ {hasLeft && !leftCollapsed && (