feat(ui): Phase 2 — issue detail tab + dashboard widget
Plugin now mounts two React components inside Paperclip via the
SDK's UI slot mechanism. Both are read-only views over data the
plugin worker fetches from legal-ai on demand.
## Slots
* **LegalCaseTab** (type: detailTab, entityTypes: ["issue"])
Mounted as a "ערר" tab on every issue page. Shows case summary
(status / practice_area / appeal_subtype), legal_arguments
grouped by party (עוררים/ועדה/משיבה/מבקשי היתר), attached
precedents, and open missing_precedents.
* **LegalCasesWidget** (type: dashboardWidget)
Dashboard tile with case counts by status + 7-day activity.
## Worker handlers (ctx.data.register)
Five handlers added at the end of setup() — all read-only over the
existing legal-ai HTTP API, all wrapped in try/catch so a transient
failure shows a placeholder instead of crashing the host:
- legal-case-summary → /api/cases/{n}/details
- legal-case-arguments → /api/cases/{n}/legal-arguments
- legal-case-precedents → /api/cases/{n}/precedents
- legal-case-missing-precedents → /api/missing-precedents?case_number=&status=open
- legal-dashboard-stats → in-memory aggregation over /api/cases
case_number is resolved from plugin state (scopeKind=issue,
stateKey=legal-case-number) — populated by legal_case_create.
## Build pipeline
- esbuild.ui.config.mjs uses createPluginBundlerPresets from the SDK
to build src/ui/index.tsx → dist/ui/index.js (13.5kb, react +
@paperclipai/plugin-sdk/ui externalized)
- package.json: build = "build:worker" (tsc) + "build:ui" (esbuild)
- tsconfig.json: jsx=react-jsx, lib += DOM
- New deps: react@19, @types/react, esbuild
## Manifest
- capabilities += ui.detailTab.register, ui.dashboardWidget.register
- entrypoints.ui = "dist/ui"
- ui.slots declared with entityTypes (not "entities" — fixed against
PluginUiSlotDeclaration validator)
## Verified
- tsc + esbuild + biome clean
- Plugin re-installs (20 capabilities) and activates with worker
+ 8 tools + 3 jobs + 1 webhook + 2 event subs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
27
esbuild.ui.config.mjs
Normal file
27
esbuild.ui.config.mjs
Normal file
@@ -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");
|
||||||
515
package-lock.json
generated
515
package-lock.json
generated
@@ -8,11 +8,14 @@
|
|||||||
"name": "@marcusgroup/plugin-legal-ai",
|
"name": "@marcusgroup/plugin-legal-ai",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paperclipai/plugin-sdk": "^2026.525.0"
|
"@paperclipai/plugin-sdk": "^2026.525.0",
|
||||||
|
"react": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.11",
|
"@biomejs/biome": "2.4.11",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^6.0.2"
|
"typescript": "^6.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -179,6 +182,448 @@
|
|||||||
"node": ">=14.21.3"
|
"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": {
|
"node_modules/@paperclipai/plugin-sdk": {
|
||||||
"version": "2026.525.0",
|
"version": "2026.525.0",
|
||||||
"resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.525.0.tgz",
|
"resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.525.0.tgz",
|
||||||
@@ -219,6 +664,74 @@
|
|||||||
"undici-types": "~7.18.0"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"manifest": "dist/manifest.js"
|
"manifest": "dist/manifest.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"dev": "paperclip-plugin-dev-server",
|
||||||
"format": "biome format --write src/",
|
"format": "biome format --write src/",
|
||||||
"format:check": "biome format src/",
|
"format:check": "biome format src/",
|
||||||
@@ -15,11 +17,14 @@
|
|||||||
"biome:fix": "biome check --write src/"
|
"biome:fix": "biome check --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paperclipai/plugin-sdk": "^2026.525.0"
|
"@paperclipai/plugin-sdk": "^2026.525.0",
|
||||||
|
"react": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.11",
|
"@biomejs/biome": "2.4.11",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^6.0.2"
|
"typescript": "^6.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,12 @@ export default {
|
|||||||
"companies.read",
|
"companies.read",
|
||||||
"projects.read",
|
"projects.read",
|
||||||
"webhooks.receive",
|
"webhooks.receive",
|
||||||
|
"ui.detailTab.register",
|
||||||
|
"ui.dashboardWidget.register",
|
||||||
] as const,
|
] as const,
|
||||||
entrypoints: {
|
entrypoints: {
|
||||||
worker: "dist/worker.js",
|
worker: "dist/worker.js",
|
||||||
|
ui: "dist/ui",
|
||||||
},
|
},
|
||||||
instanceConfigSchema: {
|
instanceConfigSchema: {
|
||||||
type: "object" as const,
|
type: "object" as const,
|
||||||
@@ -184,4 +187,21 @@ export default {
|
|||||||
"מקבל עדכוני סטטוס מ-legal-ai ומפרסם תגובה על ה-issue המקושר",
|
"מקבל עדכוני סטטוס מ-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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
248
src/ui/LegalCaseTab.tsx
Normal file
248
src/ui/LegalCaseTab.tsx
Normal file
@@ -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 (
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<strong style={{ fontSize: 14 }}>{party}</strong>
|
||||||
|
{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 (
|
||||||
|
<details key={key} style={detailsBlockStyle}>
|
||||||
|
<summary style={{ cursor: "pointer", fontSize: 14 }}>
|
||||||
|
{title}
|
||||||
|
</summary>
|
||||||
|
{body && (
|
||||||
|
<p style={{ margin: "6px 0 0 0", whiteSpace: "pre-wrap" }}>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LegalCaseTab({ context }: PluginDetailTabProps) {
|
||||||
|
const issueId = context.entityId;
|
||||||
|
|
||||||
|
const summary = usePluginData<LegalCaseDetails | null>("legal-case-summary", {
|
||||||
|
issueId,
|
||||||
|
});
|
||||||
|
const args = usePluginData<LegalArgumentsResponse | null>(
|
||||||
|
"legal-case-arguments",
|
||||||
|
{ issueId },
|
||||||
|
);
|
||||||
|
const precedents = usePluginData<PrecedentRow[] | null>(
|
||||||
|
"legal-case-precedents",
|
||||||
|
{ issueId },
|
||||||
|
);
|
||||||
|
const missing = usePluginData<MissingPrecedentRow[] | null>(
|
||||||
|
"legal-case-missing-precedents",
|
||||||
|
{ issueId },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (summary.loading) {
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={containerStyle}>
|
||||||
|
<p>טוען נתוני תיק…</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.error) {
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={containerStyle}>
|
||||||
|
<p style={{ color: "crimson" }}>
|
||||||
|
שגיאה בטעינת התיק: {summary.error.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!summary.data) {
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={containerStyle}>
|
||||||
|
<p style={mutedStyle}>
|
||||||
|
אין תיק ערר מקושר ל-issue זה. ניתן ליצור תיק חדש דרך CEO או דרך כלי
|
||||||
|
המערכת.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div dir="rtl" style={containerStyle}>
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h2 style={{ margin: "0 0 8px 0", fontSize: 20 }}>
|
||||||
|
תיק {caseDetails.case_number}
|
||||||
|
</h2>
|
||||||
|
<div style={metaListStyle}>
|
||||||
|
<span style={mutedStyle}>כותרת:</span>
|
||||||
|
<span>{caseDetails.title}</span>
|
||||||
|
<span style={mutedStyle}>סטטוס:</span>
|
||||||
|
<span>
|
||||||
|
<code>{caseDetails.status}</code>
|
||||||
|
</span>
|
||||||
|
{caseDetails.practice_area && (
|
||||||
|
<>
|
||||||
|
<span style={mutedStyle}>תחום:</span>
|
||||||
|
<span>{caseDetails.practice_area}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{caseDetails.appeal_subtype && (
|
||||||
|
<>
|
||||||
|
<span style={mutedStyle}>סוג ערר:</span>
|
||||||
|
<span>{caseDetails.appeal_subtype}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{caseDetails.expected_outcome && (
|
||||||
|
<>
|
||||||
|
<span style={mutedStyle}>תוצאה צפויה:</span>
|
||||||
|
<span>{caseDetails.expected_outcome}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h3 style={headingStyle}>טיעונים משפטיים ({argsTotal})</h3>
|
||||||
|
{args.loading && <p style={mutedStyle}>טוען טיעונים…</p>}
|
||||||
|
{args.error && (
|
||||||
|
<p style={{ color: "crimson" }}>שגיאה: {args.error.message}</p>
|
||||||
|
)}
|
||||||
|
{!args.loading && argsTotal === 0 && (
|
||||||
|
<p style={mutedStyle}>לא חולצו עדיין טיעונים מהמסמכים.</p>
|
||||||
|
)}
|
||||||
|
{Object.entries(byParty).map(([party, list]) => (
|
||||||
|
<PartySection key={party} party={party} args={list} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h3 style={headingStyle}>פסיקה מצורפת ({precedentsList.length})</h3>
|
||||||
|
{precedents.loading && <p style={mutedStyle}>טוען פסיקה…</p>}
|
||||||
|
{precedents.error && (
|
||||||
|
<p style={{ color: "crimson" }}>שגיאה: {precedents.error.message}</p>
|
||||||
|
)}
|
||||||
|
{!precedents.loading && precedentsList.length === 0 && (
|
||||||
|
<p style={mutedStyle}>לא צורפה עדיין פסיקה לתיק.</p>
|
||||||
|
)}
|
||||||
|
{precedentsList.length > 0 && (
|
||||||
|
<ul style={ulStyle}>
|
||||||
|
{precedentsList.map((p) => (
|
||||||
|
<li key={p.id} style={liStyle}>
|
||||||
|
<strong>{p.citation}</strong>
|
||||||
|
{p.practice_area && (
|
||||||
|
<span style={mutedStyle}> — {p.practice_area}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h3 style={headingStyle}>פסיקה חסרה ({missingList.length})</h3>
|
||||||
|
{missing.loading && <p style={mutedStyle}>טוען רשימת חסרים…</p>}
|
||||||
|
{missing.error && (
|
||||||
|
<p style={{ color: "crimson" }}>שגיאה: {missing.error.message}</p>
|
||||||
|
)}
|
||||||
|
{!missing.loading && missingList.length === 0 && (
|
||||||
|
<p style={mutedStyle}>אין פסיקה חסרה פתוחה בתיק זה.</p>
|
||||||
|
)}
|
||||||
|
{missingList.length > 0 && (
|
||||||
|
<ul style={ulStyle}>
|
||||||
|
{missingList.map((m) => (
|
||||||
|
<li key={m.id} style={liStyle}>
|
||||||
|
<strong>{m.citation}</strong>
|
||||||
|
<span style={mutedStyle}> — {m.status}</span>
|
||||||
|
{m.legal_topic && (
|
||||||
|
<span style={mutedStyle}> ({m.legal_topic})</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
src/ui/LegalCasesWidget.tsx
Normal file
132
src/ui/LegalCasesWidget.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
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<DashboardStats | null>(
|
||||||
|
"legal-dashboard-stats",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={widgetStyle}>
|
||||||
|
<h3 style={headingStyle}>תיקי ערר</h3>
|
||||||
|
<p style={labelStyle}>טוען…</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={widgetStyle}>
|
||||||
|
<h3 style={headingStyle}>תיקי ערר</h3>
|
||||||
|
<p style={{ color: "crimson", fontSize: 13 }}>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={widgetStyle}>
|
||||||
|
<h3 style={headingStyle}>תיקי ערר</h3>
|
||||||
|
<p style={labelStyle}>אין נתונים זמינים.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(data.byStatus).sort(([, a], [, b]) => b - a);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir="rtl" style={widgetStyle}>
|
||||||
|
<h3 style={headingStyle}>תיקי ערר</h3>
|
||||||
|
<div style={totalStyle}>{data.totalCases}</div>
|
||||||
|
<div style={{ fontSize: 12, color: "rgba(127,127,127,0.85)" }}>
|
||||||
|
סה"כ תיקים פעילים
|
||||||
|
</div>
|
||||||
|
<hr
|
||||||
|
style={{
|
||||||
|
margin: "8px 0",
|
||||||
|
border: 0,
|
||||||
|
borderTop: "1px solid rgba(127,127,127,0.25)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{entries.length > 0 ? (
|
||||||
|
<ul style={ulStyle}>
|
||||||
|
{entries.map(([status, count]) => (
|
||||||
|
<li
|
||||||
|
key={status}
|
||||||
|
style={{ display: "contents" }}
|
||||||
|
aria-label={`${STATUS_LABELS[status] ?? status}: ${count}`}
|
||||||
|
>
|
||||||
|
<span style={labelStyle}>{STATUS_LABELS[status] ?? status}</span>
|
||||||
|
<span>{count}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p style={labelStyle}>אין תיקים פעילים.</p>
|
||||||
|
)}
|
||||||
|
<hr
|
||||||
|
style={{
|
||||||
|
margin: "8px 0",
|
||||||
|
border: 0,
|
||||||
|
borderTop: "1px solid rgba(127,127,127,0.25)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{ margin: 0, fontSize: 13 }}>
|
||||||
|
<span style={labelStyle}>פעילות שבועית: </span>
|
||||||
|
<strong>{data.weekActivity}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/ui/index.tsx
Normal file
10
src/ui/index.tsx
Normal file
@@ -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";
|
||||||
72
src/ui/types.ts
Normal file
72
src/ui/types.ts
Normal file
@@ -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<string, LegalArgument[]>;
|
||||||
|
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<string, number>;
|
||||||
|
weekActivity: number;
|
||||||
|
totalCases: number;
|
||||||
|
}
|
||||||
166
src/worker.ts
166
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<string | null> {
|
||||||
|
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<string, number> = {};
|
||||||
|
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");
|
ctx.logger.info("Legal AI plugin ready");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user