diff --git a/esbuild.ui.config.mjs b/esbuild.ui.config.mjs new file mode 100644 index 0000000..155f1a2 --- /dev/null +++ b/esbuild.ui.config.mjs @@ -0,0 +1,27 @@ +import { build } from "esbuild"; +import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const presets = createPluginBundlerPresets({ + pluginRoot: __dirname, + uiEntry: "src/ui/index.tsx", + outdir: "dist", + sourcemap: true, + minify: false, +}); + +if (!presets.esbuild.ui) { + throw new Error("UI preset missing — check createPluginBundlerPresets input"); +} + +await build({ + ...presets.esbuild.ui, + // Ensure JSX runtime is bundled-resolved at host runtime through React peer. + jsx: "automatic", + logLevel: "info", +}); + +console.log("[esbuild] UI bundle written to dist/ui/index.js"); diff --git a/package-lock.json b/package-lock.json index eda7697..b79b1c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,14 @@ "name": "@marcusgroup/plugin-legal-ai", "version": "0.1.0", "dependencies": { - "@paperclipai/plugin-sdk": "^2026.525.0" + "@paperclipai/plugin-sdk": "^2026.525.0", + "react": "^19.0.0" }, "devDependencies": { "@biomejs/biome": "2.4.11", "@types/node": "^25.5.0", + "@types/react": "^19.0.0", + "esbuild": "^0.28.0", "typescript": "^6.0.2" } }, @@ -179,6 +182,448 @@ "node": ">=14.21.3" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@paperclipai/plugin-sdk": { "version": "2026.525.0", "resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.525.0.tgz", @@ -219,6 +664,74 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/typescript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", diff --git a/package.json b/package.json index 6c8b9c9..9677ab5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "manifest": "dist/manifest.js" }, "scripts": { - "build": "tsc", + "build": "tsc && node esbuild.ui.config.mjs", + "build:worker": "tsc", + "build:ui": "node esbuild.ui.config.mjs", "dev": "paperclip-plugin-dev-server", "format": "biome format --write src/", "format:check": "biome format src/", @@ -15,11 +17,14 @@ "biome:fix": "biome check --write src/" }, "dependencies": { - "@paperclipai/plugin-sdk": "^2026.525.0" + "@paperclipai/plugin-sdk": "^2026.525.0", + "react": "^19.0.0" }, "devDependencies": { "@biomejs/biome": "2.4.11", "@types/node": "^25.5.0", + "@types/react": "^19.0.0", + "esbuild": "^0.28.0", "typescript": "^6.0.2" } } diff --git a/src/manifest.ts b/src/manifest.ts index 9b0ef47..c900536 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -26,9 +26,12 @@ export default { "companies.read", "projects.read", "webhooks.receive", + "ui.detailTab.register", + "ui.dashboardWidget.register", ] as const, entrypoints: { worker: "dist/worker.js", + ui: "dist/ui", }, instanceConfigSchema: { type: "object" as const, @@ -184,4 +187,21 @@ export default { "מקבל עדכוני סטטוס מ-legal-ai ומפרסם תגובה על ה-issue המקושר", }, ], + ui: { + slots: [ + { + type: "detailTab" as const, + id: "legal-case-tab", + displayName: "ערר", + exportName: "LegalCaseTab", + entityTypes: ["issue" as const], + }, + { + type: "dashboardWidget" as const, + id: "legal-cases-widget", + displayName: "תיקי ערר", + exportName: "LegalCasesWidget", + }, + ], + }, }; diff --git a/src/ui/LegalCaseTab.tsx b/src/ui/LegalCaseTab.tsx new file mode 100644 index 0000000..5b2bf98 --- /dev/null +++ b/src/ui/LegalCaseTab.tsx @@ -0,0 +1,248 @@ +import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui"; +import { usePluginData } from "@paperclipai/plugin-sdk/ui/hooks"; +import type { + LegalArgument, + LegalArgumentsResponse, + LegalCaseDetails, + MissingPrecedentRow, + PrecedentRow, +} from "./types.js"; + +const containerStyle: React.CSSProperties = { + padding: 16, + fontFamily: + "system-ui, -apple-system, 'Segoe UI', 'Noto Sans Hebrew', sans-serif", + color: "inherit", +}; + +const sectionStyle: React.CSSProperties = { + marginBottom: 24, +}; + +const headingStyle: React.CSSProperties = { + fontSize: 16, + fontWeight: 600, + margin: "0 0 8px 0", + borderBottom: "1px solid rgba(127,127,127,0.25)", + paddingBottom: 4, +}; + +const metaListStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "max-content 1fr", + columnGap: 12, + rowGap: 4, + fontSize: 14, + margin: 0, +}; + +const detailsBlockStyle: React.CSSProperties = { + margin: "6px 0", + padding: "6px 10px", + background: "rgba(127,127,127,0.08)", + borderRadius: 6, +}; + +const ulStyle: React.CSSProperties = { + listStyle: "none", + paddingInlineStart: 0, + margin: 0, +}; + +const liStyle: React.CSSProperties = { + padding: "4px 0", + fontSize: 14, + borderBottom: "1px dotted rgba(127,127,127,0.2)", +}; + +const mutedStyle: React.CSSProperties = { + color: "rgba(127,127,127,0.8)", + fontSize: 13, +}; + +function PartySection({ + party, + args, +}: { + party: string; + args: LegalArgument[]; +}) { + if (!args || args.length === 0) return null; + return ( +
+ {party} + {args.map((a, idx) => { + const key = a.id ?? `${party}-${a.claim_index ?? idx}`; + const title = + a.argument_title || + (a.argument_body || "").slice(0, 80) || + "(ללא כותרת)"; + const body = a.argument_body || ""; + return ( +
+ + {title} + + {body && ( +

+ {body} +

+ )} +
+ ); + })} +
+ ); +} + +export function LegalCaseTab({ context }: PluginDetailTabProps) { + const issueId = context.entityId; + + const summary = usePluginData("legal-case-summary", { + issueId, + }); + const args = usePluginData( + "legal-case-arguments", + { issueId }, + ); + const precedents = usePluginData( + "legal-case-precedents", + { issueId }, + ); + const missing = usePluginData( + "legal-case-missing-precedents", + { issueId }, + ); + + if (summary.loading) { + return ( +
+

טוען נתוני תיק…

+
+ ); + } + + if (summary.error) { + return ( +
+

+ שגיאה בטעינת התיק: {summary.error.message} +

+
+ ); + } + + if (!summary.data) { + return ( +
+

+ אין תיק ערר מקושר ל-issue זה. ניתן ליצור תיק חדש דרך CEO או דרך כלי + המערכת. +

+
+ ); + } + + const caseDetails = summary.data; + const byParty = args.data?.by_party ?? {}; + const argsTotal = args.data?.total ?? 0; + const precedentsList = precedents.data ?? []; + const missingList = missing.data ?? []; + + return ( +
+
+

+ תיק {caseDetails.case_number} +

+
+ כותרת: + {caseDetails.title} + סטטוס: + + {caseDetails.status} + + {caseDetails.practice_area && ( + <> + תחום: + {caseDetails.practice_area} + + )} + {caseDetails.appeal_subtype && ( + <> + סוג ערר: + {caseDetails.appeal_subtype} + + )} + {caseDetails.expected_outcome && ( + <> + תוצאה צפויה: + {caseDetails.expected_outcome} + + )} +
+
+ +
+

טיעונים משפטיים ({argsTotal})

+ {args.loading &&

טוען טיעונים…

} + {args.error && ( +

שגיאה: {args.error.message}

+ )} + {!args.loading && argsTotal === 0 && ( +

לא חולצו עדיין טיעונים מהמסמכים.

+ )} + {Object.entries(byParty).map(([party, list]) => ( + + ))} +
+ +
+

פסיקה מצורפת ({precedentsList.length})

+ {precedents.loading &&

טוען פסיקה…

} + {precedents.error && ( +

שגיאה: {precedents.error.message}

+ )} + {!precedents.loading && precedentsList.length === 0 && ( +

לא צורפה עדיין פסיקה לתיק.

+ )} + {precedentsList.length > 0 && ( +
    + {precedentsList.map((p) => ( +
  • + {p.citation} + {p.practice_area && ( + — {p.practice_area} + )} +
  • + ))} +
+ )} +
+ +
+

פסיקה חסרה ({missingList.length})

+ {missing.loading &&

טוען רשימת חסרים…

} + {missing.error && ( +

שגיאה: {missing.error.message}

+ )} + {!missing.loading && missingList.length === 0 && ( +

אין פסיקה חסרה פתוחה בתיק זה.

+ )} + {missingList.length > 0 && ( +
    + {missingList.map((m) => ( +
  • + {m.citation} + — {m.status} + {m.legal_topic && ( + ({m.legal_topic}) + )} +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/src/ui/LegalCasesWidget.tsx b/src/ui/LegalCasesWidget.tsx new file mode 100644 index 0000000..5cadbe4 --- /dev/null +++ b/src/ui/LegalCasesWidget.tsx @@ -0,0 +1,132 @@ +import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui"; +import { usePluginData } from "@paperclipai/plugin-sdk/ui/hooks"; +import type { DashboardStats } from "./types.js"; + +const widgetStyle: React.CSSProperties = { + padding: 12, + fontFamily: + "system-ui, -apple-system, 'Segoe UI', 'Noto Sans Hebrew', sans-serif", + color: "inherit", +}; + +const headingStyle: React.CSSProperties = { + margin: "0 0 8px 0", + fontSize: 15, + fontWeight: 600, +}; + +const totalStyle: React.CSSProperties = { + fontSize: 22, + fontWeight: 700, + margin: "4px 0", +}; + +const ulStyle: React.CSSProperties = { + listStyle: "none", + paddingInlineStart: 0, + margin: 0, + display: "grid", + gridTemplateColumns: "1fr max-content", + rowGap: 2, + fontSize: 13, +}; + +const labelStyle: React.CSSProperties = { + color: "rgba(127,127,127,0.85)", +}; + +const STATUS_LABELS: Record = { + new: "תיק חדש", + uploading: "העלאת מסמכים", + processing: "עיבוד מסמכים", + documents_ready: "מסמכים מוכנים", + outcome_set: "תוצאה הוזנה", + brainstorming: "סיעור מוחות", + direction_approved: "כיוון אושר", + drafting: "כתיבה", + qa_review: "בדיקת איכות", + drafted: "טיוטה מוכנה", + exported: "DOCX נוצר", + reviewed: "נבדק", + final: "סופי", +}; + +export function LegalCasesWidget(_props: PluginWidgetProps) { + const { data, loading, error } = usePluginData( + "legal-dashboard-stats", + {}, + ); + + if (loading) { + return ( +
+

תיקי ערר

+

טוען…

+
+ ); + } + + if (error) { + return ( +
+

תיקי ערר

+

{error.message}

+
+ ); + } + + if (!data) { + return ( +
+

תיקי ערר

+

אין נתונים זמינים.

+
+ ); + } + + const entries = Object.entries(data.byStatus).sort(([, a], [, b]) => b - a); + + return ( +
+

תיקי ערר

+
{data.totalCases}
+
+ סה"כ תיקים פעילים +
+
+ {entries.length > 0 ? ( +
    + {entries.map(([status, count]) => ( +
  • + {STATUS_LABELS[status] ?? status} + {count} +
  • + ))} +
+ ) : ( +

אין תיקים פעילים.

+ )} +
+

+ פעילות שבועית: + {data.weekActivity} +

+
+ ); +} diff --git a/src/ui/index.tsx b/src/ui/index.tsx new file mode 100644 index 0000000..1f9c4ff --- /dev/null +++ b/src/ui/index.tsx @@ -0,0 +1,10 @@ +/** + * UI bundle entrypoint for the legal-ai Paperclip plugin. + * + * Each named export here corresponds to a `slot.exportName` in the manifest's + * `ui.slots` declaration. The host loads this module as an ES module and + * mounts the matching component into its slot. + */ + +export { LegalCasesWidget } from "./LegalCasesWidget.js"; +export { LegalCaseTab } from "./LegalCaseTab.js"; diff --git a/src/ui/types.ts b/src/ui/types.ts new file mode 100644 index 0000000..4a9572a --- /dev/null +++ b/src/ui/types.ts @@ -0,0 +1,72 @@ +/** + * Shared types for the legal-ai plugin UI bundle. + * + * Mirrors a minimal subset of the legal-ai backend response shapes so the UI + * components can type their props/data without pulling the full LegalApi + * client (which is a worker-side dependency). + */ + +export interface LegalCaseSummary { + case_number: string; + title: string; + status: string; + practice_area?: string | null; + appeal_subtype?: string | null; + proceeding_type?: string | null; + updated_at?: string | null; + archived_at?: string | null; +} + +export interface LegalCaseDetails { + id: string; + case_number: string; + title: string; + status: string; + practice_area?: string | null; + appeal_subtype?: string | null; + appellants?: string[]; + respondents?: string[]; + subject?: string; + property_address?: string; + expected_outcome?: string; + [key: string]: unknown; +} + +export interface LegalArgument { + id?: string; + party: string; + argument_title?: string; + argument_body?: string; + claim_index?: number; + [key: string]: unknown; +} + +export interface LegalArgumentsResponse { + case_number: string; + total: number; + by_party: Record; + arguments: LegalArgument[]; +} + +export interface PrecedentRow { + id: string; + citation: string; + section_id?: string | null; + practice_area?: string | null; + [key: string]: unknown; +} + +export interface MissingPrecedentRow { + id: string; + citation: string; + status: string; + legal_topic?: string | null; + cited_by_party?: string | null; + [key: string]: unknown; +} + +export interface DashboardStats { + byStatus: Record; + weekActivity: number; + totalCases: number; +} diff --git a/src/worker.ts b/src/worker.ts index b69a369..57468fb 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -842,6 +842,172 @@ const plugin = definePlugin({ } }); + // ── Data handlers (UI bridge) ────────────────────────────────── + // These back `usePluginData(key, params)` calls from the React UI bundle + // in `dist/ui/index.js`. Errors are caught and rethrown with a clear + // message so the UI shows them through PluginBridgeError. + + // Resolve the legal-ai case number linked to an issue via plugin state. + async function resolveCaseNumber(issueId: string): Promise { + const value = await ctx.state.get({ + scopeKind: "issue", + scopeId: issueId, + stateKey: "legal-case-number", + }); + return typeof value === "string" && value.length > 0 ? value : null; + } + + ctx.data.register("legal-case-summary", async (params) => { + const issueId = String((params as { issueId?: string }).issueId ?? ""); + if (!issueId) return null; + const caseNumber = await resolveCaseNumber(issueId); + if (!caseNumber) return null; + try { + return await api.getCase(caseNumber); + } catch (err) { + ctx.logger.warn("legal-case-summary: getCase failed", { + caseNumber, + error: String(err), + }); + throw err; + } + }); + + ctx.data.register("legal-case-arguments", async (params) => { + const issueId = String((params as { issueId?: string }).issueId ?? ""); + if (!issueId) return null; + const caseNumber = await resolveCaseNumber(issueId); + if (!caseNumber) return null; + const config2 = await ctx.config.get(); + const apiBase = + (config2.legalApiBaseUrl as string) ?? "http://localhost:8085"; + try { + const resp = await ctx.http.fetch( + `${apiBase}/api/cases/${encodeURIComponent(caseNumber)}/legal-arguments`, + ); + if (!resp.ok) { + ctx.logger.warn("legal-case-arguments: API error", { + caseNumber, + status: resp.status, + }); + return { + case_number: caseNumber, + total: 0, + by_party: {}, + arguments: [], + }; + } + return await resp.json(); + } catch (err) { + ctx.logger.warn("legal-case-arguments: fetch failed", { + caseNumber, + error: String(err), + }); + return { + case_number: caseNumber, + total: 0, + by_party: {}, + arguments: [], + }; + } + }); + + ctx.data.register("legal-case-precedents", async (params) => { + const issueId = String((params as { issueId?: string }).issueId ?? ""); + if (!issueId) return []; + const caseNumber = await resolveCaseNumber(issueId); + if (!caseNumber) return []; + const config2 = await ctx.config.get(); + const apiBase = + (config2.legalApiBaseUrl as string) ?? "http://localhost:8085"; + try { + const resp = await ctx.http.fetch( + `${apiBase}/api/cases/${encodeURIComponent(caseNumber)}/precedents`, + ); + if (!resp.ok) { + ctx.logger.warn("legal-case-precedents: API error", { + caseNumber, + status: resp.status, + }); + return []; + } + const data = await resp.json(); + // Endpoint may return a flat array or `{ precedents: [...] }`. + if (Array.isArray(data)) return data; + if ( + data && + Array.isArray((data as { precedents?: unknown[] }).precedents) + ) { + return (data as { precedents: unknown[] }).precedents; + } + return []; + } catch (err) { + ctx.logger.warn("legal-case-precedents: fetch failed", { + caseNumber, + error: String(err), + }); + return []; + } + }); + + ctx.data.register("legal-case-missing-precedents", async (params) => { + const issueId = String((params as { issueId?: string }).issueId ?? ""); + if (!issueId) return []; + const caseNumber = await resolveCaseNumber(issueId); + if (!caseNumber) return []; + const config2 = await ctx.config.get(); + const apiBase = + (config2.legalApiBaseUrl as string) ?? "http://localhost:8085"; + try { + const url = new URL(`${apiBase}/api/missing-precedents`); + url.searchParams.set("case_number", caseNumber); + url.searchParams.set("status", "open"); + const resp = await ctx.http.fetch(url.toString()); + if (!resp.ok) { + ctx.logger.warn("legal-case-missing-precedents: API error", { + caseNumber, + status: resp.status, + }); + return []; + } + const data = (await resp.json()) as { items?: unknown[] }; + return Array.isArray(data.items) ? data.items : []; + } catch (err) { + ctx.logger.warn("legal-case-missing-precedents: fetch failed", { + caseNumber, + error: String(err), + }); + return []; + } + }); + + ctx.data.register("legal-dashboard-stats", async () => { + try { + const cases = await api.listCases(); + const byStatus: Record = {}; + let weekActivity = 0; + const weekAgoMs = Date.now() - 7 * 24 * 60 * 60 * 1000; + for (const c of cases) { + byStatus[c.status] = (byStatus[c.status] ?? 0) + 1; + const updated = (c as { updated_at?: string | null }).updated_at; + if (updated) { + const t = Date.parse(updated); + if (!Number.isNaN(t) && t >= weekAgoMs) weekActivity += 1; + } + } + return { + byStatus, + weekActivity, + totalCases: cases.length, + }; + } catch (err) { + ctx.logger.warn("legal-dashboard-stats: listCases failed", { + error: String(err), + }); + return { byStatus: {}, weekActivity: 0, totalCases: 0 }; + } + }); + ctx.logger.info("Legal AI plugin ready"); }, diff --git a/tsconfig.json b/tsconfig.json index 21a9814..66ec067 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,9 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "declaration": true, + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"] }, "include": ["src"] }